SwingWorkerでスレッドからGUI操作

EventQueue#invokeLaterなどを使えということなのですが、実際の処理をいちいちinvokeLaterすると処理の記述が分断して大変みにくくなります。記述の美しさよりも処理の安全さ、とはいえ、これはあんまり。
ということで、Java SE 6からはSwingWorkerというクラスが用意されていて、便利に使えます。
使い方を図解すると、こう


関係ないけど、今回のサンプルはムダにNimbusです。


SwingWorkerは、Genericsパラメータをふたつ指定します。一つ目が処理結果の型で、二つ目が途中経過の型です。

new SwingWorker<Integer, int[]>()


SwingWorkerで実装するべきなのは、doInBackgroundメソッドです。ここでは、別スレッドで動かしたい処理を記述します。戻り値の型は、Genericsで一つ目に指定した型になります。

@Override
Integer doInBackground() throws Exception {


processメソッドは、途中経過を表示するメソッドです。引数にはGenericsで二つ目に指定した型のListが渡されます。

@Override
void process(List<int[]> chunks) {


publishメソッドで途中経過を渡すと、適当なタイミングでprocessメソッドが呼び出されます。

publish(new int[]{i, sum});


processメソッドが呼び出される頻度よりもpublishメソッドの方が多いとき、複数の出力がたまっている可能性もあります。そこで、出力はまとめて行うとなんとなく得した気分になります。

StringBuilder sb = new StringBuilder();
for(int[] values : chunks){
    sb.append(String.format(
            "%dを足して%d%n", values[0], values[1]));
}
taOutput.append(sb.toString());

出力は文字列で行うことが多いですが、最終的な出力文字列の生成はprocessメソッドで行ったほうがいいと思います。


処理が終わったら、doneメソッドが呼び出されます。

@Override
void done()

ここで、getメソッドを使うと処理結果が受け取れます。

int result = get();

このgetをdoneメソッド以外で呼び出した場合、doInBackgroundの処理が終わるまでブロックします。


処理の進捗はsetProgressメソッドで指定します。

setProgress(i * 10);


進捗が変わったときは、PropertyChangeEventが発生するので、PropertyChangeListenerを登録しておく必要があります。このときのイベントは"progress"です。

sw.addPropertyChangeListener(new PropertyChangeListener() {
    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        if("progress".equals(evt.getPropertyName())){
            progressBar.setValue((Integer)evt.getNewValue());
        }
    }
});


executeメソッドを呼び出すと、処理が開始します。

sw.execute();


ということで、ソース

import java.awt.BorderLayout;
import java.awt.event.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.swing.*;
import javax.swing.SwingWorker;

public class SwingWorkerSample {
    public static void main(String[] args){
        //なんとなくNimbusを使う
        try {
            UIManager.setLookAndFeel(
                    "com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
        } catch (Exception ex) {
        }
        //ウィンドウ
        JFrame f = new JFrame("SwingWorkerサンプル");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //テキストエリア
        final JTextArea taOutput = new JTextArea(15, 30);
        JScrollPane sp = new JScrollPane(taOutput);
        f.add(sp);
        //プログレスバー
        final JProgressBar progressBar = new JProgressBar();
        f.add(BorderLayout.SOUTH, progressBar);
        //ボタン
        JButton b = new JButton("開始");
        f.add(BorderLayout.NORTH, b);
        
        //実行結果Integer, 処理経過データint[]のSwingWorker
        final SwingWorker<Integer, int[]> sw = new SwingWorker<Integer, int[]>(){
            /** バックグラウンド処理 */
            @Override
            protected Integer doInBackground() throws Exception {
                int sum = 0;
                for(int i = 1; i <= 10; ++i){
                    sum += i;
                    publish(new int[]{i, sum});//結果出力
                    setProgress(i * 10);//進捗
                    Thread.sleep(1000);
                }
                return sum;
            }

            /** 途中経過の表示 */
            @Override
            protected void process(List<int[]> chunks) {
                StringBuilder sb = new StringBuilder();
                for(int[] values : chunks){
                    sb.append(String.format(
                            "%dを足して%d%n", values[0], values[1]));
                }
                taOutput.append(sb.toString());
            }

            /** 処理終了 */
            @Override
            protected void done() {
                try {
                    int result = get();
                    taOutput.append("終了。" + result + "でした\n");
                } catch (InterruptedException ex) {
                } catch (ExecutionException ex) {
                }
            }

        };
        //プログレスバーの処理
        sw.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if("progress".equals(evt.getPropertyName())){
                    progressBar.setValue((Integer)evt.getNewValue());
                }
            }
        });

        //ボタンが押されたときの処理
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                sw.execute();//SwingWorkderの実行
            }
        });

        //ウィンドウ表示
        f.pack();
        f.setVisible(true);
    }
}