このトピックでは、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になるはずです。