13 Swing

この章では、Java SE Swing APIで発生する可能性のあるもっとも一般的な問題のいくつかをトラブルシューティングするための、具体的な手順に関する情報と指針を提供します。

この章の構成は、次のとおりです。

Swingの一般的なデバッグ・ヒント

Java SE 6ではSwingのペイント・インフラストラクチャが大幅に変更されました。Java SE 6以降のリリースに固有のペイント・アーティファクトに気づいた場合は、新機能をオフにしてみることができます。これはプロパティswing.bufferPerWindowで行えます。

なにかしらのメニューがポップアップされた状態で実行されるSwingコードのデバッグを行う場合、デバッガをリモートで使用することをお薦めします。それ以外の場合、デバッグ・プロセスとアプリケーション実行とが互いにブロックし合い、システムでの後続の作業が行えなくなります。それが発生した場合に取れる唯一のアクションは、Oracle SolarisおよびLinuxのXサーバーを終了させることです。バグ・データベースに関する項を参照してください。

次は、いくつかの一般的なSwingの問題です。

  • ペイント。

  • レンダラ。

  • 間違ったスレッドからのモデルの更新。

  • ハング・アップ。

  • 応答性。

  • 再ペイントの問題。

  • isOpaqueの使用。

  • 起動: 小さなヒープ、不要なクラスのロードが原因である可能性があります。

次は、いくつかの検討事項です:

  • ウィンドウごとのバッファ機能。

  • ネイティブ・ルック・アンド・フィールの忠実度: Gnome対Windows

  • Swingアプリケーションのフットプリント。

  • JTableJTree、およびJListはすべてレンダラを使用します。

  • カスタム・レンダラの処理を極力減らすようにします。

  • モデルの更新は、イベント・ディスパッチ・スレッドからしか行わないようにします。それ以外の場合、モデルの状態が表示に反映されなくなります。

次は、不適切なレンダラを識別します:

  • 動作の遅いアプリケーション(特にスクロール時)。

  • オプティマイザを使ってペイント呼出しを監視し、getTableCellTRendererComponentの呼出しを探します。

Swingの具体的なデバッグ・ヒント

次のトピックでは、Swingの問題およびトラブルシューティング手法を説明します。

不正なスレッド処理

例外やペイント問題がランダムに発生するのは通常、Swingによる不正なスレッド使用の結果です。

Swingコンポーネントへのすべてのアクセスは、Javadocで特に明記されていないかぎり、イベント・ディスパッチ・スレッド上で行われる必要があります。これには、Swingコンポーネントに接続されるすべてのモデル(TableModelListModelなど)も含まれます。

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の子のオーバーラップ

JComponentの子同士のオーバーラップを許可した場合にも、ペイント問題が発生する可能性があります。

この場合、親がisOptimizedDrawingEnabledをオーバーライドしてfalseを返すようにする必要があります。isOptimizedDrawingEnabledをオーバーライドしないと、どのコンポーネントで再ペイントが呼び出されたかに応じて、コンポーネントが他のコンポーネントの上にランダムに表示される可能性があります。

表示の更新

表示を更新する必要があるときに再ペイントを正しく呼び出さなかった場合にも、ペイント問題が発生する可能性があります。

Swingコンポーネントの可視プロパティ(フォントなど)を変更すると、再ペイントまたは再有効化がトリガーされます。カスタム・コンポーネントを記述する場合には、表示やサイズ設定の情報が更新されるたびに再ペイント(とおそらく再有効化)を呼び出す必要があります。そうしないと、再ペイントが次回どこかでトリガーされるまで表示が更新されません。

これを診断する良い方法は、ウィンドウのサイズを変更することです。サイズ変更後にコンテンツが表示された場合、コンポーネントが再ペイントや再有効化を正しく呼び出さなかったことを意味しています。

モデルの変更

Swingコンポーネントの可視プロパティの変更時にはrepaintを呼び出しますが、モデルの変更時にrepaintを呼び出す必要はありません。

モデルが正しい変更通知を送出すると、JComponentが必要に応じてrepaintrevalidateを呼び出します。

ただし、モデルを変更したのに通知を送出しなければ、再ペイント・イベントが動作すらしない可能性があります。特に、これはJTreeでは動作しません。適切なモデル通知を送信するのが正しい方法です。その診断は通常、ウィンドウのサイズを変更し、表示が正しく更新されなかったことを確認することで行えます。

コンポーネントの追加または削除

コンポーネントを追加または削除した場合は、再ペイントを手動で呼び出すか、SwingおよびAWTを再有効化する必要があります。

不透明のオーバーライド

ペイント問題の別の潜在的な領域は、コンポーネントが不透明をオーバーライドしない場合です。

さらに、実装を呼び出さない場合は、不透明プロパティを尊重する必要があります(このコンポーネントが不透明な場合は、不透明でない色でバックグラウンドを完全に塗りつぶす必要があります)。不透明プロパティを尊重しない場合は、視覚的なアーティファクトが見える場合があります。

これをチェックする唯一の方法は、コンポーネントが再ペイントを呼び出す際に一貫性のある視覚的なアーティファクトを探すことです。

グラフィックへの永続的な変更

paintpaintComponentまたはpaintChildrenに渡されるGraphicsオブジェクトに対する永続的な変更は、一切行わないでください。

ノート:

サブクラスのグラフィックをオーバーライドする場合は、Graphicsオブジェクトで渡されるpaintpaintComponentまたはpaintChildrenを永続的に変更しないでください。たとえば、クリップ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とそのすべての子には、引き続きmyModelにインストールされたリスナーJTable経由で到達できます。解決方法は、table.setModel(new DefaultTableModel())を呼び出すことです。

重量コンポーネントと軽量コンポーネントの混在

重量コンポーネントと軽量コンポーネントが混在しても、重量コンポーネントが既存のSwingコンポーネントと重複しなければ、特定のシナリオでは動作する場合があります。

たとえば、重量コンポーネントは内部フレームでは機能しません。ユーザーが内部フレームの周辺をドラッグすると、他の内部フレームと重複してしまうためです。重量コンポーネントを使用する場合は、次のメソッドを呼び出します。

  • JPopupMenu.setDefaultLightWeightPopupEnabled(false)
  • ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false)

Synthの使用

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が通知を受け取ることは決してありません。

コンテンツ・ペインへのコンポーネントの追加

JFrameJWindowまたはJDialogコンポーネントをコンテンツ・ペインに追加する必要があります。

トップ・レベルのSwingコンポーネントに追加されるコンポーネントはコンテンツ・ペインに追加される必要がありますが、JFrameJWindowおよびJDialogクラスのaddメソッド(およびいくつかのほかのメソッド)はコンテンツ・ペインにリダイレクトされます。つまり、frame.getContentPane().add(component)frame.add(component)と同じです。

次のメソッドはコンテンツ・ペインに自動的にリダイレクトされます。add (およびそのバリアント)、remove (およびそのバリアント)、setLayout

これは非常に便利ですが、混乱を招く恐れもあります。特に、getChildrengetLayout、およびその他の各種メソッドは、コンテンツ・ペインにリダイレクトされません。

この変更は、GroupLayoutBoxLayoutなどの1つのコンポーネントのみを処理するLayoutManagerに影響を及ぼします。たとえば、新しいGroupLayout(frame)は機能しません。かわりに、GroupLayout(frame.getContentPane())を使用する必要があります。

ドラッグ・アンド・ドロップのサポート

Swingを使用する際は、Swingのドラッグ・アンド・ドロップ・サポート(TransferHandlerによって提供されるもの)を使用するようにしてください。

コンポーネントの親は1つ

コンポーネントは一度に1つの親の中にしか存在できません。

メニュー間でメニュー項目を共有すると、問題が発生します。たとえば、JMenuItemはコンポーネントなので、一度に1つのメニューの中にしか存在できません。

JFileChooserとWindowsショートカットの問題

JFileChooserクラスは、Windows OSのショートカット(.lnkファイル)をサポートしていません。

JFileChooserでは標準のWindowsファイル・チューザと違って、ファイル・システムの参照時にWindowsショートカットを辿ることができません。ファイルへの正しいパスが表示されないからです。

問題を再現するためのステップ:

  1. デスクトップで、たとえばMyFile.txtという名前のテキスト・ファイルを作成します。テキスト・ファイルを開き、なんらかのテキスト、たとえばThis is the contents of MyFile.txtを入力します。
  2. 新しいテキスト・ファイルへのショートカットを、次のようにして作成します。マウスの右ボタンでファイルをデスクトップの別の場所にドラッグし、「ショートカットをここに作成」を選択します。
  3. JfileChooserテスト・アプリケーションを実行し、デスクトップを参照して「MyFile.txtへのショートカット」を選択し、「開く」をクリックします。
  4. 結果ファイルはデスクトップへのパス\MyFile.txtへのショートカット.lnkになっていますが、これはデスクトップへのパス\MyFile.txtである必要があります。
  5. さらに、テキスト領域の結果ファイルの内容として、MyFile.txtへのショートカット.lnkファイルの内容が表示されますが、この内容は、ステップ1で入力したThis is the contents of MyFile.txtになるはずです。