Swing
※ Java8 から Swing から JavaFX への移行を推奨している。
- Swingとは [2014-10-08]
- Swingはスレッドセーフではない [2014-10-08]
- 時間がかかる処理はSwingWorkerを使う [2014-10-08]
- EDT上でスレッドを固めない SecondaryLoop (モーダルダイアログの仕組み) [2015-02-08]
- JTextArea の行間を変える( JTextPane や JEditorPane は使用しない) [2015-02-11]
Swingとは
Swingは、プログラミング言語 Java のGUIツールキット。
同じく Java の GUI ツールキットである AWT を拡張したもの。
AWT はオペレーティングシステムのウィンドウシステムに準じたデザインになるのに対し、
Swing で作成した GUI は Java プログラム上で描画されるので、より柔軟な設計が可能となる。
AWT に対し Swing のようなコンポーネントを軽量コンポーネントと呼ぶ。
プラグイン可能なルック・アンド・フィールを持っているので、簡単にルック・アンド・フィールを切り替えることができる。
また AWT には無かった、表、スライダー、スピナ、ツリー表示をするコンポーネントなど高度なコンポーネントが用意されている。
Swingはスレッドセーフではない
Swingは、シングルスレッド設計となっていて、スレッドセーフではない。
Swingのコンポネント描画やイベント処理などは、イベントディスパッチスレッド(Event Dispatch Thread, EDT)上で行なうルールとなっている。
また、EDTは、1つのJVMに対して、1スレッドしか存在していない。
EDT以外のスレッドから、Swingのコンポネント描画やイベント処理を行う(シングルスレッドルールを違反する)と、
想定していないバグやJVMクラッシュを引き起こす可能性がある(NullPointer, ArrayIndexOutOfBoundException)。
特に setVisible(true) などで部品を可視化した後は、EDT上で部品にアクセスするように意識して設計したほうが良い。
参考:Lesson: Concurrency in Swing
参考:Threads and Swing
例えば、Swingのユーティリティクラス(SwingUtilities)を使用して明示的にEDT上で実行させる。
EDTは、1スレッドしか存在しないため、EDT上で処理させることでマルチスレッド競合が発生しない。
また、ボタン押下時などに実行されるイベントの処理などは、EDT上で実行される仕組みとなっている。
/** * ×悪い例) * 以下のように記述すると、 * 通常スレッド上でコンポネント生成と画面描画が行われてしまう。 * これらの処理は、EDTスレッド上で行われるべき。 */ public static void main(String[] args){ // ほとんどはコンストラクタ内でコンポネントを生成している final HogeHogeFrame hogeFrame = new HogeHogeFrame(); // そしてsetVisibleで画面描画する hogeFrame.setVisible(true); } /** * ○良い例) * SwingUtilities.invokeLaterを使用して、 * 明示的にEDTスレッド上で実行する。 */ public static void main(String[] args){ // EDTの処理キューにRunnableを登録し、 // 非同期でEDT上から実行される。 SwingUtilities.invokeLater(new Runnable(){ public void run(){ // EDT内で実行している final HogeHogeFrame hogeFrame = new HogeHogeFrame(); hogeFrame.setVisible(true); } }); } /** 以下はTips */ // カレントスレッドがEDTかどうかを調べる場合。 SwingUtilities.isEventDispatchThread(); // EDTのスレッド名。数字(0)の部分は動作環境に依存する。 AWT-EventQueue-0参考:JavaDoc:SwingUtilitiesクラス
時間がかかる処理はSwingWorkerを使う
何もかもEDT上で処理させてしまうと、その重い処理が終わるまでは他の画面描画処理が行われない。
その場合、画面がフリーズしたような現象となる。
なので、EDT上では画面描画以外の処理はなるべく行わないことが理想となる。
上記をSwingUtilitiesだけで実現するとなると、ソースコードがとても煩雑になり、可読性や保守性が下がる。
そこでSwingWorkerクラスを使用する。
また、SwingWorkerはスレッドプール管理されていて、デフォルトプールサイズは 10 となっている。
以下は、SwingWorkerの使用例。
doInBackgroundメソッドは、通常スレッドで実行されるところに注意(EDT上ではない)。
画面描画はdoneメソッドやprocessメソッドで行うこと。
public class HogeWorkerImpl extends SwingWorker<String, Object> { /** * doInbackgroundメソッドは通常スレッド上で実行される。 * このメソッド内で重い処理を実行した後、doneメソッド内で画面描画を行う。 **/ @Override public String doInBackground() { /** 重い処理 */ // get()で取得できるオブジェクトを返却する return retStr; } /** * doneメソッドは、EDT上で実行される。 * 画面操作はこのメソッド内で行う。 * ※doInbackgroundメソッドが実行された後に呼ばれる **/ @Override protected void done() { try { // 画面操作する label.setText(get()); } catch (Exception e) { // ignore } } }参考:JavaDoc:SwingWorkerクラス
EDT上でスレッドを固めない SecondaryLoop (モーダルダイアログの仕組み)
Javaでは、EDT上で sleep や wait を使用すると画面全体がフリーズしてしまう。
しかし、EDT上でモーダルダイアログを表示しても何故か固まらない。
それはなぜかというと、モーダルダイアログ生成時に EDTスレッド を固めないような実装となっているからだ。
Java7 からは SecondaryLoop というクラスが追加されており、
EDTスレッドを固めない仕組み(モーダルダイアログみたいな仕組み)が簡単に実装できるようになった。
// SecondaryLoop は配管などで使われる手法らしい SecondaryLoop loop; // 例えばボタンを生成してイベントを登録する JButton jButton = new JButton("Button"); jButton.addActionListener(new ActionListener() { /** 本処理はEDT上で行われる */ @Override public void actionPerformed(ActionEvent e) { // ToolkitからEventQueueを取得する Toolkit tk = Toolkit.getDefaultToolkit(); EventQueue eq = tk.getSystemEventQueue(); // Secondary Loopを生成する loop = eq.createSecondaryLoop(); // サンプルで新しいスレッドをスタートさせる // 実際にはこのスレッドで「やりたい処理」を行う Thread worker = new WorkerThread(); worker.start(); // ここで待ち受けるが、EDT上で実行してもスレッド自体は固まらず // あたかも次のEDT処理が継続されているような動きとなる if (!loop.enter()) { // エラー処理を記述する // ※既にloop.enterが実行されていた場合などは false となる } } }); class WorkerThread extends Thread { @Override public void run() { // 実際の処理 doSomethingUseful(); // loop.enter で待ち受けていた処理を終了する loop.exit(); } }
JTextArea の行間を変える( JTextPane や JEditorPane は使用しない)
ここで紹介しているのはhackでの解決手法。
通常はテキストエリアの行間( line spacing )などを変更する場合は、JTextPane や JEditorPane を使用するのが正しいやり方。
なぜやろうと思ったのかというと、 Java6 と Java7 ではテキストエリアの行間が異なり、Java7 の方が広くなる(Java7はAsentがでかい)。
これはJava6とJava7でFontMetricsから取得できるAscentなどのサイズが異なるのが原因だった(なぜ変わったんだろう)。
上記の差を JTextArea継承クラスで吸収できれば「幸せになれる」ためコードを書いてみた。
/** メソッド定義は省略 */ // JTextArea継承して、「getFontMetrics」をオーバーライドする。 final JTextArea textArea = new JTextArea() { /** FontMetricsのラッパークラス用意して戻り値とする */ @Override public FontMetrics getFontMetrics(Font font) { // 例は以下のメソッドをさらにオーバーライドで変更している return new FontMetricsWrapper(SwingUtilities2.getFontMetrics(this, font)) { @Override public int getHeight() { // 以下は参考値。ここで文字の高さを以下調整する(カーソル位置にも影響する) return super.getHeight() - 3; } @Override public int getAscent() { // 以下は参考値。ここを変えると、実際に描画される文字位置が変更できる return super.getAscent() + 2; } }; } ・・・ /** * FontMetricsWrapperクラスのサンプル * コンポジションパターンで作ると簡単。 **/ public class FontMetricsWrapper extends FontMetrics { private final FontMetrics orgFontMetrics; public FontMetricsWrapper(FontMetrics arg0) { super(arg0.getFont()); this.orgFontMetrics = arg0; } /** * 以降は無限ループを防ぐためFontMetricsのメソッドをオーバーライドすること。 * その中でも、以下メソッドは必ず実装しておく(FontMetricsのJavaDoc参考)。 * ・getAscent() * ・getLeading() * ・getMaxAdvance() * ・charWidth(char) * ・charsWidth(char[], int, int) **/ // 以下のようにすべてのメソッドをオーバーライドしておくと良い。 // コンポジションパターンを使うと簡単。 @Override public Font getFont() { // TODO 自動生成されたメソッド・スタブ return orgFontMetrics.getFont(); } }