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アプリケーションのフットプリント。
-
JTable
、JTree
、およびJList
はすべてレンダラを使用します。 -
カスタム・レンダラの処理を極力減らすようにします。
-
モデルの更新は、イベント・ディスパッチ・スレッドからしか行わないようにします。それ以外の場合、モデルの状態が表示に反映されなくなります。
次は、不適切なレンダラを識別します:
-
動作の遅いアプリケーション(特にスクロール時)。
-
オプティマイザを使ってペイント呼出しを監視し、
getTableCellTRendererComponent
の呼出しを探します。
Swingの具体的なデバッグ・ヒント
次のトピックでは、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の子のオーバーラップ
JComponent
の子同士のオーバーラップを許可した場合にも、ペイント問題が発生する可能性があります。
この場合、親がisOptimizedDrawingEnabled
をオーバーライドしてfalse
を返すようにする必要があります。isOptimizedDrawingEnabled
をオーバーライドしないと、どのコンポーネントで再ペイントが呼び出されたかに応じて、コンポーネントが他のコンポーネントの上にランダムに表示される可能性があります。
表示の更新
表示を更新する必要があるときに再ペイントを正しく呼び出さなかった場合にも、ペイント問題が発生する可能性があります。
Swingコンポーネントの可視プロパティ(フォントなど)を変更すると、再ペイントまたは再有効化がトリガーされます。カスタム・コンポーネントを記述する場合には、表示やサイズ設定の情報が更新されるたびに再ペイント(とおそらく再有効化)を呼び出す必要があります。そうしないと、再ペイントが次回どこかでトリガーされるまで表示が更新されません。
これを診断する良い方法は、ウィンドウのサイズを変更することです。サイズ変更後にコンテンツが表示された場合、コンポーネントが再ペイントや再有効化を正しく呼び出さなかったことを意味しています。
モデルの変更
Swingコンポーネントの可視プロパティの変更時にはrepaint
を呼び出しますが、モデルの変更時にrepaint
を呼び出す必要はありません。
モデルが正しい変更通知を送出すると、JComponent
が必要に応じてrepaint
やrevalidate
を呼び出します。
ただし、モデルを変更したのに通知を送出しなければ、再ペイント・イベントが動作すらしない可能性があります。特に、これはJTree
では動作しません。適切なモデル通知を送信するのが正しい方法です。その診断は通常、ウィンドウのサイズを変更し、表示が正しく更新されなかったことを確認することで行えます。
不透明のオーバーライド
ペイント問題の別の潜在的な領域は、コンポーネントが不透明をオーバーライドしない場合です。
さらに、実装を呼び出さない場合は、不透明プロパティを尊重する必要があります(このコンポーネントが不透明な場合は、不透明でない色でバックグラウンドを完全に塗りつぶす必要があります)。不透明プロパティを尊重しない場合は、視覚的なアーティファクトが見える場合があります。
これをチェックする唯一の方法は、コンポーネントが再ペイントを呼び出す際に一貫性のある視覚的なアーティファクトを探すことです。
グラフィックへの永続的な変更
paint
、paintComponent
またはpaintChildren
に渡されるGraphics
オブジェクトに対する永続的な変更は、一切行わないでください。
ノート:
サブクラスのグラフィックをオーバーライドする場合は、Graphics
オブジェクトで渡されるpaint
、paintComponent
または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
が通知を受け取ることは決してありません。
コンテンツ・ペインへのコンポーネントの追加
JFrame
、JWindow
またはJDialog
コンポーネントをコンテンツ・ペインに追加する必要があります。
トップ・レベルのSwingコンポーネントに追加されるコンポーネントはコンテンツ・ペインに追加される必要がありますが、JFrame
、JWindow
およびJDialog
クラスのadd
メソッド(およびいくつかのほかのメソッド)はコンテンツ・ペインにリダイレクトされます。つまり、frame.getContentPane().add(component)はframe.add(component)と同じです。
次のメソッドはコンテンツ・ペインに自動的にリダイレクトされます。add (およびそのバリアント)、remove (およびそのバリアント)、setLayout。
これは非常に便利ですが、混乱を招く恐れもあります。特に、getChildren
、getLayout
、およびその他の各種メソッドは、コンテンツ・ペインにリダイレクトされません。
この変更は、GroupLayout
やBoxLayout
などの1つのコンポーネントのみを処理するLayoutManager
に影響を及ぼします。たとえば、新しいGroupLayout(frame)は機能しません。かわりに、GroupLayout(frame.getContentPane())を使用する必要があります。
コンポーネントの親は1つ
コンポーネントは一度に1つの親の中にしか存在できません。
メニュー間でメニュー項目を共有すると、問題が発生します。たとえば、JMenuItem
はコンポーネントなので、一度に1つのメニューの中にしか存在できません。
JFileChooserとWindowsショートカットの問題
JFileChooser
クラスは、Windows OSのショートカット(.lnkファイル)をサポートしていません。
JFileChooser
では標準のWindowsファイル・チューザと違って、ファイル・システムの参照時にWindowsショートカットを辿ることができません。ファイルへの正しいパスが表示されないからです。
問題を再現するためのステップ:
- デスクトップで、たとえば
MyFile.txt
という名前のテキスト・ファイルを作成します。テキスト・ファイルを開き、なんらかのテキスト、たとえばThis is the contents of MyFile.txt
を入力します。 - 新しいテキスト・ファイルへのショートカットを、次のようにして作成します。マウスの右ボタンでファイルをデスクトップの別の場所にドラッグし、「ショートカットをここに作成」を選択します。
JfileChooser
テスト・アプリケーションを実行し、デスクトップを参照して「MyFile.txt
へのショートカット」を選択し、「開く」をクリックします。- 結果ファイルは
デスクトップへのパス\
になっていますが、これはMyFile.txt
へのショートカット.lnkデスクトップへのパス\MyFile.txt
である必要があります。 - さらに、テキスト領域の結果ファイルの内容として、
MyFile.txt
へのショートカット.lnkファイルの内容が表示されますが、この内容は、ステップ1
で入力したThis is the contents of MyFile.txtになるはずです。