このトピックでは、Swingのいくつかの具体的なデバッグ・ヒントを示し、起こり得る問題および回避方法について例を挙げて説明します。次のトピックでは、Swingの問題およびトラブルシューティング手法を説明します。
例外やペイント問題がランダムに発生するのは通常、Swingの不正なスレッド使用の結果です。Swingコンポーネントへのすべてのアクセスは、Javadocで特に明記されていないかぎり、イベント・ディスパッチ・スレッド上で行われる必要があります。これには、Swingコンポーネントに接続されるすべてのモデル(TableModel
やListModel
など)も含まれます。
Swingの不適切な使用をチェックするための最良の方法は、例13-1に示すように、インストゥルメント化されたRepaintManager
を使用することです。
例13-1 不適切なスレッド
public class CheckThreadViolationRepaintManager extends RepaintManager { // it is recommended to pass the complete check private boolean completeCheck = true; public boolean isCompleteCheck() { return completeCheck; } public void setCompleteCheck(boolean completeCheck) { this.completeCheck = completeCheck; } public synchronized void addInvalidComponent(JComponent component) { checkThreadViolations(component); super.addInvalidComponent(component); } public void addDirtyRegion(JComponent component, int x, int y, int w, int h) { checkThreadViolations(component); super.addDirtyRegion(component, x, y, w, h); } private void checkThreadViolations(JComponent c) { if (!SwingUtilities.isEventDispatchThread() && (completeCheck || c.isShowing())) { Exception exception = new Exception(); boolean repaint = false; boolean fromSwing = false; StackTraceElement[] stackTrace = exception.getStackTrace(); for (StackTraceElement st : stackTrace) { if (repaint && st.getClassName().startsWith("javax.swing.")) { fromSwing = true; } if ("repaint".equals(st.getMethodName())) { repaint = true; } } if (repaint && !fromSwing) { //no problems here, since repaint() is thread safe return; } exception.printStackTrace(); } } }
JComponent
の子同士のオーバーラップを許可した場合にも、ペイント問題が発生する可能性があります。この場合、親がisOptimizedDrawingEnabled
をオーバーライドしてfalse
を返すようにする必要があります。isOptimizedDrawingEnabled
をオーバーライドしなかった場合、どのコンポーネントで再ペイントが呼び出されたかに応じて、コンポーネントがほかのコンポーネントの上にランダムに表示される可能性があります。
表示を更新する必要があるときに再ペイントを正しく呼び出さなかった場合にも、ペイント問題が発生する可能性があります。Swingコンポーネントの可視プロパティ(フォントなど)を変更すると、再ペイントまたは再有効化がトリガーされます。カスタム・コンポーネントを記述する場合には、表示やサイズ設定の情報が更新されるたびに再ペイント(とおそらく再有効化)を呼び出す必要があります。そうしないと、再ペイントが次回どこかでトリガーされるまで表示が更新されません。
これを診断する良い方法は、ウィンドウのサイズを変更することです。サイズ変更後にコンテンツが表示された場合、コンポーネントが再ペイントや再有効化を正しく呼び出さなかったことを意味しています。
Swingコンポーネントの可視プロパティの変更時に再ペイントを呼び出す必要がないのと同じく、モデルの変更時にも再ペイントを呼び出す必要はありません。モデルが正しい変更通知を送出すると、JComponent
が必要に応じて再ペイントや再有効化を呼び出します。
ただし、モデルを変更したのに通知を送出しなければ、再ペイント・イベントが動作すらしない可能性があります。特に、これはJTree
では動作しません。行うべき正しいことは、適切なモデル通知を送出することです。その診断は通常、やはりウィンドウのサイズを変更し、表示が正しく更新されなかったことを確認することで行えます。
コンポーネントを追加または削除した場合は、再ペイントまたは再有効化を呼び出す必要があります。SwingとAWTはこうした状況では再ペイントや再有効化を呼び出さないため、ユーザー自身がそれらを呼び出す必要があります。
ペイント問題の別の潜在的な領域は、コンポーネントが不透明をオーバーライドしない場合です。
さらに、superの実装を呼び出さない場合は、不透明プロパティを尊重する必要があります(このコンポーネントが不透明な場合は、不透明でない色のバックグラウンドを完全に塗りつぶす必要があります)。不透明プロパティを尊重しない場合は、視覚的なアーティファクトが見える場合があります。
これをチェックする唯一の方法は、コンポーネントが再ペイントを呼び出す際に一貫性のある視覚的なアーティファクトを探すことです。
paint
、paintComponent
、またはpaintChildren
に渡されるGraphics
に対する永続的な変更は、一切行わないでください。これについて警告するドキュメントを、次に示します。
このメソッドをサブクラスでオーバーライドする場合は、渡されたGraphics
に永続的な変更を行わないようにしてください。たとえば、クリップRectangle
を変更したり、変換を変更したりするべきではありません。このような操作が必要な場合は、渡されたGraphics
から新しいGraphics
を作成し、それを操作するほうが容易でしょう。
この制限を尊重しない場合は、クリッピングなどの奇妙な視覚的アーティファクトが発生します。
paint
をオーバーライドし、そのオーバーライド内でカスタム・ペイントを行うことも可能ですが、代わりにpaintComponent
をオーバーライドすべきです。JComponent.paint
メソッドは、ペイントがダブル・バッファに対して発生することを保証します。paint
を直接オーバーライドすると、ダブル・バッファリングが失われる可能性があります。
Swingのペイント・アーキテクチャでは不透明なコンテンツ・ペインが必要です。次にドキュメントを示します。
Swingのペイント・アーキテクチャでは、不透明なJComponent
が包含関係の階層の中でほかのすべてのコンポーネントの上に存在する必要があります。通常、これはコンテンツ・ペインによって提供されます。コンテンツ・ペインを置き換える場合は、setOpaque(true)
によってコンテンツ・ペインを不透明にすることをお薦めします。また、コンテンツ・ペインによってpaintComponent
がオーバーライドされる場合は、バックグラウンドをpaintComponent
の不透明な色で完全に塗りつぶす必要があります。
レンダラはセルごとにペイントされるので、レンダラの処理は極力少なくしてください。レンダラ内での速度低下はすべて、すべてのセルにわたって拡大されます。たとえば、50x20の可視セルから成る表の可視領域を再ペイントすると、レンダラが1000回呼び出されます。
モデルのライフ・サイクルが、モデルを使用するコンポーネントを含むウィンドウのライフ・サイクルよりも長い場合、Swingコンポーネントのモデルを明示的にnullに設定する必要があります。モデルをnullに設定しなかった場合、モデルがComponent
への参照を保持しているため、ウィンドウ内のコンポーネントはすべて、ガベージ・コレクションの対象外となります。例13-2を参照してください。
例13-2 Swingでのリークの可能性
TableModel myModel = ...; JFrame frame = new JFrame(); frame.setContentPane(new JScrollPane(new JTable(myModel))); frame.dispose();
アプリケーションがまだmyModel
への参照を保持している場合、frame
とそのすべての子は引き続き、JTable
によってmyModel
にインストールされたリスナー経由で到達できます。解決方法は、table.setModel(new DefaultTableModel())
を呼び出すことです。
重量コンポーネントと軽量コンポーネントの混在は、そもそも重量コンポーネントが既存のSwingコンポーネントと重複さえしなければ、特定のシナリオでうまく機能することができます。たとえば、重量コンポーネントは内部フレームでは機能しません。ユーザーが内部フレームの周辺をドラッグすると、他の内部フレームと重複してしまうためです。重量コンポーネントを使用する場合は、次のメソッドを呼び出します。
JPopupMenu.setDefaultLightWeightPopupEnabled(false)
ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false)
Synth
は空のキャンバスです。Synth
を使用するには、ルック・アンド・フィールを構成する完全なXMLファイルを用意するか、あるいはSynthLookAndFeel
を拡張して独自のSynthStyleFactory
を提供します。
Swingアプリケーションがイベント・ディスパッチ・スレッド上で実行を試みる処理が多すぎると、アプリケーションの動作が遅くなり、無反応になります。
この状況を検出する方法の1つは、処理時間の長すぎるイベントが発生した際にロギング情報を出力できる、新しいEventQueue
をプッシュすることです。このアプローチは、フォーカス・イベントやモーダリティに問題があるので完璧とは言えませんが、アドホックなテストには有効です。
1つのSwingコンポーネント上に様々なデフォルト・レイアウト・マネージャ・クラスがあると、問題が生じる可能性があります。たとえば、JPanel
クラスのデフォルトはFlowLayout
ですが、JFrame
クラスのデフォルトはBorderLayout
です。この状況は、1つのLayoutManager
を指定することで簡単に解決されます。
MouseListener
オブジェクトは、MouseListener
オブジェクトを持つ(またはMouseEvent
オブジェクトを有効化した)もっとも深いコンポーネントにディスパッチされます。このため、ユーザーがMouseListener
をコンポーネントに接続しても、そのコンポーネントにMouseListener
オブジェクトを持つ子孫が含まれていれば、ユーザーのMouseListener
オブジェクトが呼び出されることは決してありません。
このことは、編集可能なJComboBox
のような複合コンポーネントで容易に再現されます。JComboBox
にはMouseListener
を持つ子コンポーネントが含まれているため、編集可能なJComboBox
に接続されたMouseListener
が通知を受け取ることは決してありません。
ユーザーのMouseListener
が突然イベントを取得しなくなった場合、それは、アプリケーション内で何らかの変化が生じ、それによって子孫コンポーネントの1つがMouseListener
を持つようになった結果である可能性があります。これをチェックする良い方法は、子孫に対して繰返し処理を実行し、各子孫がマウス・リスナーを持っているかどうかを確認することです。
KeyListener
クラスでも似たようなシナリオが発生します。KeyListener
オブジェクトは、フォーカスされたコンポーネントにしかディスパッチされません。
JComboBox
のケースはこの状況のもう1つの例です。編集可能なJComboBox
の場合、JComboBox
ではなくエディタがフォーカスを得ます。結果として、編集可能なJComboBox
に接続されたKeyListener
が通知を受け取ることは決してありません。
J2SE 1.5より前は、JFrame
、JWindow
、JDialog
、またはJApplet
にコンポーネントを追加することはできませんでした。代わりに、コンテンツ・ペインにコンポーネントを追加する必要がありました。J2SE 1.5以降でも、トップ・レベルのSwingコンポーネントに追加されるコンポーネントはコンテンツ・ペインに追加される必要があることに変わりはありませんが、これらのクラスのaddメソッド(およびいくつかのほかのメソッド)はコンテンツ・ペインにリダイレクトされます。つまり、frame.getContentPane().add(component)
はframe.add(component)
と同じです。
次のメソッドはコンテンツ・ペインに自動的にリダイレクトされます。add
(およびそのバリアント)、remove
(およびそのバリアント)、setLayout
。
これは非常に便利ですが、混乱を招く恐れもあります。特に、getChildren
、getLayout
、およびその他の各種メソッドは、コンテンツ・ペインにリダイレクトされません。
この変更は、GroupLayout
やBoxLayout
などの1つのコンポーネントのみを処理するLayoutManager
に影響を及ぼします。たとえば、新しいGroupLayout(frame)
は機能しません。かわりに、新しいGroupLayout(frame.getContentPane())
を実行する必要があります。
Swingを使用する際は、Swingのドラッグ・アンド・ドロップ・サポート(TransferHandler
によって提供されるもの)を使用するようにしてください。それ以外の場合、選択やその他のさまざまな問題を管理する必要が生じます。
コンポーネントは一度に1つの親の中にしか存在できません。メニュー間でメニュー項目を共有しようとすると、問題が発生します。たとえば、JMenuItem
はコンポーネントなので、一度に1つのメニューの中にしか存在できません。
JFileChooser
クラスは、Windows OSのショートカット(.lnkファイル)をサポートしていません。JFileChooser
では標準のWindowsファイル・チューザと違って、ファイル・システムの参照時にWindowsショートカットを辿ることができません。ファイルへの正しいパスが表示されないからです。
問題を再現するための手順:
デスクトップで、たとえばMyFile.txtという名前のテキスト・ファイルを作成します。テキスト・ファイルを開き、なんらかのテキスト、たとえばThis is the contents of MyFile.txt
を入力します。
新しいテキスト・ファイルへのショートカットを、次のようにして作成します。マウスの右ボタンでファイルをデスクトップの別の場所にドラッグし、「ショートカットをここに作成」を選択します。
JfileChooser
テスト・アプリケーションを実行し、デスクトップを参照して「MyFile.txtへのショートカット」を選択し、「開く」をクリックします。
結果のFileはデスクトップへのパス\MyFile.txtへのショートカット.lnkになっていますが、これはデスクトップへのパス\MyFile.txtであるべきです。
さらに、テキスト領域の結果のFileの内容として、MyFile.txtへのショートカット.lnkファイルの内容が表示されますが、この内容は、手順1
で入力したThis is the contents of MyFile.txtになるはずです。