この章では、Java SE Swing API で発生する可能性のあるもっとも一般的な問題のいくつかをトラブルシューティングするための、具体的な手順に関する情報と指針を提供します。
Java SE 6 では Swing のペイントインフラストラクチャーが大幅に変更されました。Java SE 6 以降のリリースに固有のペイントアーティファクトに気づいた場合は、新機能をオフにしてみることができます。これはプロパティー swing.bufferPerWindow で行えます。
なにかしらのメニューがポップアップされた状態で実行される Swing コードのデバッグを行う場合、デバッガをリモートで使用することをお勧めします。それ以外の場合、デバッグプロセスとアプリケーション実行とが互いにブロックし合い、システムでの後続の作業が行えなくなります。それが発生した場合に取れる唯一のアクションは、Linux および Solaris の X サーバーを終了させることです。詳細については、バグデータベース内の次のバグを参照してください。
http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6517045
Swing のいくつかの一般的な問題:
ペイントの問題。
レンダラ。
間違ったスレッドからのモデルの更新。
ハングアップ。
応答性。
再ペイントの問題。
isOpaque の使用。
起動:小さなヒープ、不要なクラスのロードが原因である可能性があります。
いくつかの考慮すべき事項:
ウィンドウごとのバッファー機能。
ネイティブ Look & Feel の忠実度:Gnome 対 Windows
Swing アプリケーションのフットプリント。
JTable、JTree、および JList はすべてレンダラを使用します。
カスタムレンダラの処理を極力減らすようにします。
モデルの更新は、イベントディスパッチスレッドからしか行わないようにします。それ以外の場合、モデルの状態が表示に反映されなくなります。
不正なレンダラの特定:
動作の遅いアプリケーション (特にスクロール時)。
オプティマイザを使ってペイント呼び出しを監視し、getTableCellTRendererComponent の呼び出しを探します。
次の各サブセクションでは、Swing の問題をトラブルシューティングするためのいくつかのヒントを示します。
例外やペイント問題がランダムに発生するのは通常、Swing の不正なスレッド使用の結果です。Swing コンポーネントへのすべてのアクセスは、Javadoc で特に明記されていないかぎり、イベントディスパッチスレッド上で行われる必要があります。これには、Swing コンポーネントに接続されるすべてのモデル (TableModel や ListModel など) も含まれます。
Swing の不正な使用をチェックする最良の方法は、次のコードで示される計測 RepaintManager を使用することです。
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 への参照を保持しているため、ウィンドウ内のコンポーネントはすべて、ガベージコレクションの対象外となります。たとえば、次のコードを考えてみましょう。
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 を使用するには、Look & Feel を構成する完全な 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、およびその他の各種メソッドは、コンテンツペインにリダイレクトされません。
この変更は、処理対象のコンポーネントが 1 つだけである LayoutManager (GroupLayout や BoxLayout など) に影響を与えます。たとえば、new GroupLayout(frame) は動作しません。代わりに、new 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 の内容が表示されますが、この内容は、最初のステップで入力した This is the contents of MyFile.txt になるべきです。