AWTでUIのイベント以外のスレッドからUI更新する

「UIを更新するようなスレッドは、AWTのイベントキューに入れておいて、素直にUIを処理しているスレッド（DispatchThread）にまかせてあげましょう」ってだけのお話です。

GUIアプリは基本イベント・ドリブンなので、UIイベント処理で画面更新処理を書くことがおおいですが、バックグラウンド処理のスレッドからステータスバーを更新するなど、UIのイベント以外でUI表示を更新することもあると思います。

このとき、シングルスレッドで動いているAWTを邪魔するコードを見かけることが多々ありました。また、下手ににマルチスレッド化して、UIスレッドと同期が取れずに思わぬエラーの原因になっていた事例があったので備忘録も兼ねて書いてみます。

※ちなみに４月からJava8がメインになったので、ラムダつかってます。java7で動かしたい場合は匿名実装してください。

##サンプル
ダミーのバックグラウンド処理ではつまらんので、適当にアニメーションでメッセージを入れ替えるラベルを作ってみます。パネル上に縦３つ並べたラベルを上下に動かすだけ。UI操作でなくタイマーでUIを更新する処理を実装してみます。

具体的には、java.util.Timerjava.awt.EventQueue.invokeLaterでアニメーション用のタイマーをスケジュールする処理を、AWTのイベントキューに入れてあげます。

###コード

アニメーションは面倒なのでラベルのTop位置の軌跡を配列にしただけ。なんとなく４種類つくってみました。

```java:MyAnimation.java
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を拡張します。

```java:AnimationThread.java
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を拡張してラベル３つ貼っただけ。

ここで、タイマーのスレッドをイベントキユーに入れるstartAnimationメソッドを作って、コンストラクタで呼び出すようにしています。

```java:AnimationLabel.java
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を貼り付けてみます。

```java:FrameMain.java
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を。

```java:AppMain.java
package sample;

public class AppMain {
	public static void main(String[] args) {
		java.awt.EventQueue.invokeLater(() -> new FrameMain().setVisible(true));
	}
}
```

###実行
![ani.png](https://qiita-image-store.s3.amazonaws.com/0/69798/dad7b126-7d8e-4c5b-5f10-32bc3cffd5cd.png)

animation中でもWindowClosingイベントは安全に処理してくれるため、安心して閉じれます。