「UIを更新するようなスレッドは、AWTのイベントキューに入れておいて、素直にUIを処理しているスレッド(DispatchThread)にまかせてあげましょう」ってだけのお話です。
GUIアプリは基本イベント・ドリブンなので、UIイベント処理で画面更新処理を書くことがおおいですが、バックグラウンド処理のスレッドからステータスバーを更新するなど、UIのイベント以外でUI表示を更新することもあると思います。
このとき、シングルスレッドで動いているAWTを邪魔するコードを見かけることが多々ありました。また、下手ににマルチスレッド化して、UIスレッドと同期が取れずに思わぬエラーの原因になっていた事例があったので備忘録も兼ねて書いてみます。
※ちなみに4月からJava8がメインになったので、ラムダつかってます。java7で動かしたい場合は匿名実装してください。
サンプル
ダミーのバックグラウンド処理ではつまらんので、適当にアニメーションでメッセージを入れ替えるラベルを作ってみます。パネル上に縦3つ並べたラベルを上下に動かすだけ。UI操作でなくタイマーでUIを更新する処理を実装してみます。
具体的には、java.util.Timerjava.awt.EventQueue.invokeLaterでアニメーション用のタイマーをスケジュールする処理を、AWTのイベントキューに入れてあげます。
コード
アニメーションは面倒なのでラベルのTop位置の軌跡を配列にしただけ。なんとなく4種類つくってみました。
package sample;
public class MyAnimation {
// 線形
public static final int[] LINEAR = {0, -1, -2, -3, -4, -5, -6, -7, -8, -9,
-10, -11, -12, -13, -14, -15, -16, -17, -18, -19, -20};
// イージング
public static final int[] EASE = {0, 0, 0, 0, -1, -2, -3, -4, -6, -8, -10,
-11, -13, -15, -16, -17, -18, -19, -19, -19, -20};
// 跳ねる
public static final int[] POP = {0, -3, -7, -10, -13, -16, -18, -20, -22,
-23, -25, -25, -26, -26, -26, -26, -25, -24, -23, -21, -20};
// ラッチ
public static final int[] LATCH = {0, 0, 0, 0, -1, -2, -3, -4, -6, -8, -10,
-11, -12, -13, -14, -16, -18, -21, -24, -22, -20};
}
アニメーション用スレッド。タイマー起動なので、抽象クラスjava.util.TimerTaskを拡張します。
package sample;
import java.awt.Component;
import java.awt.Label;
import java.awt.Panel;
import java.util.TimerTask;
public class AnimationThread extends TimerTask {
private static final int INTERVAL = 5000;
private Panel panel;
private Label label1, label2, label3;
private int[] animation;
private int count = 0;
// constructor
public AnimationThread(Component component) {
panel = (Panel) component;
label1 = (Label) panel.getComponent(0);
label2 = (Label) panel.getComponent(1);
label3 = (Label) panel.getComponent(2);
animation = MyAnimation.LINEAR;
}
@Override
public void run() {
// animation completed
if (count == animation.length) {
// 静止させるためにここでウエイトしてみた。
Thread.currentThread();
try {
Thread.sleep(INTERVAL);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 実用するなら表示メッセージ送りのロジックは外部化を。
String str = label1.getText();
label1.setText(label2.getText());
label2.setText(label3.getText());
label3.setText(str);
count = 0;
};
// 動かします
label1.setLocation(0, animation[count]);
label2.setLocation(0, animation[count] + 20);
label3.setLocation(0, animation[count] + 40);
panel.repaint();
count++;
}
// Accessor
public int[] getAnimation() {
return animation;
}
public void setAnimation(int[] animation) {
this.animation = animation;
}
}
アニメーションするラベルを、UIコンポーネントにしてしまいます。Panelを拡張してラベル3つ貼っただけ。
ここで、タイマーのスレッドをイベントキユーに入れるstartAnimationメソッドを作って、コンストラクタで呼び出すようにしています。
package sample;
import java.awt.Label;
import java.awt.Panel;
import java.util.Timer;
public class AnimationLabel extends Panel {
private static final long serialVersionUID = 1L;
private AnimationThread aniThread;
public AnimationLabel() {
init();
aniThread = new AnimationThread(this);
startAnimation();
}
private void init() {
Label label1 = new Label();
Label label2 = new Label();
Label label3 = new Label();
label1.setLocation(0, 0);
label2.setLocation(0, 20);
label3.setLocation(0, 40);
label1.setSize(300, 20);
label2.setSize(300, 20);
label3.setSize(300, 20);
label1.setText("sample message 1");
label2.setText("sample message 2");
label3.setText("sample message 3");
setLayout(null);
add(label1);
add(label2);
add(label3);
}
public void setAnimation(int[] animation) {
this.aniThread.setAnimation(animation);
}
public void startAnimation() {
// attach animation thread
java.awt.EventQueue.invokeLater(() -> new Timer().schedule(aniThread,
100, 20));
}
}
これでAnimationするラベルができたので、あたらしいFrameをつくって、作成したAnimationLabelを貼り付けてみます。
package sample;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.Panel;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
public class FrameMain extends Frame {
private static final long serialVersionUID = 1L;
//
public FrameMain() {
super("Animation Sample");
init();
bindEvents();
}
// initializer
private void init() {
Panel panel = new Panel();
AnimationLabel label1 = new AnimationLabel();
label1.setSize(150, 20);
label1.setAnimation(MyAnimation.LATCH);
// panel
panel.add(label1);
// frame
add(panel);
setSize(150, 64);
setLayout(new FlowLayout());
setResizable(false);
setVisible(true);
}
// binding event handlers
// これもラムダにできたらいいのに…
private void bindEvents() {
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
}
そして最後にmainを。
package sample;
public class AppMain {
public static void main(String[] args) {
java.awt.EventQueue.invokeLater(() -> new FrameMain().setVisible(true));
}
}
実行
animation中でもWindowClosingイベントは安全に処理してくれるため、安心して閉じれます。