chofutaroメモ

ソフトウェアエンジニアによるソフトウェアエンジニアのためのメモ書き

SwingWorkerを使ってみる

SwingWorkerを使って時間のかかる処理の状況をプログレスバーで表示してみました。主な課題は、この手の処理のコールドスポット(⇔ホットスポット。共通部品になりそうな部位)はどこか自分なりに感じ取ることです。

  • JDK6u24、NetBeans7.0β2

ネットを検索するとSwingWorkerに関するサンプルはたくさん見つけられるのですが、これらのコードは、ほとんどのケースで時間のかかる処理をGUI(Swing部品を取り扱っている部位)に直接埋め込んでいます。しかし、時間のかかる処理というのは本来GUIとは無関係であることが多いと思いますので、時間のかかる処理を別立てにすることを目標とします。

20110224180146

今回は、時間のかかる処理をEratosthenes#calculate()に作ってあります(MAX_NUMBER迄の素数を求める)。SwingWorker#doInBackground()内で時間のかかる処理を実行します。このメソッドはイベントディスパッチスレッド (EDT) ではありませんので、このメソッドで実行します。

EratosthenesオブジェクトはGUIやSwingWorkerに依存しません。共通部品のAbstractProgressListener(末尾”所感”参照)を通じてGUI側のオブジェクトに処理を委譲します。AbstractProgressListener派生の無名派生クラス#actionPerformed()がそれです。

actionPerformed()の中の処理は、他のサンプルにも見られる処理と同様で、Eratosthenes#calculate()にて実行中の処理状況をSwingWorker#publish(getValue())しています。それを受けて、SwingWorker#process()でプログレスバーを更新しています。これ以降は、通常のSwingWorkerの使い方同様だと思います。

public class AppPanel extends javax.swing.JPanel {

    private int MAX_NUMBER = 1000 * 1000;   //  対象数(最大側)
    private SwingWorker<Void, Integer> worker = null;

    private void startButtonActionPerformed(java.awt.event.ActionEvent evt) {
        worker = new SwingWorker<Void, Integer>() {
            @Override
            public Void doInBackground() throws Exception {
                // Eratosthenes.calculate()は時間のかかる処理でGUIとは無関係
                Eratosthenes.calculate(new AbstractProgressListener() {

                    @Override
                    public boolean actionPerformed() {
                        boolean canceled = isCancelled();
                        if (!canceled) {
                            publish(getValue());
                        }
                        return !canceled;
                    }
                }, MAX_NUMBER);
                return null;
            }
            @Override
            public void process(java.util.List<Integer> chunks) {
                setProgress(chunks.get(chunks.size() - 1));
            }
        };
        worker.addPropertyChangeListener(new ProgressListener(progressBar));
        worker.execute();
    }                                           

    private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {
        if (worker != null && !worker.isDone()) {
            worker.cancel(true);
        }
        worker = null;
    }
}

ちなみに、時間がかかる処理の本体であるEratosthenes#calculate()は、以下のようになっています。Swingには全く依存していません。

public class Eratosthenes {
    public static void calculate(AbstractProgressListener listener, int count) {
        :
        do {
            :
            //  GUIに状況を表示できるようパーセンテージをセットする
            listener.putValue(処理状況を表すパーセンテージ);
            //  GUI側に制御のトリガを与える。
            //  キャンセルならば終了するためbreak
            if (!listener.actionPerformed()) break;
        } while (まだ素数判断する数値が残っている);
        :
    }
}

これらの関係をクラス図にしてみます。

20110224180145

蛇足ですが、AbstractProgressListenerクラスはActionクラスで良いのかもしれません。今回は、私がうまくActionクラスを使いこなせなかったため、AbstractProgressListenerクラスを作成しましたが、既存のクラスで十分ならば既存のクラスを使うべきだと思います。

サンプルコード

所感

SwingWorker#doInBackground()の中で、時間のかかる処理を(が)委譲して(されて)います。その際にGUI側であるSwingWorkerと非GUI側であるEratosthenesの間を取り持つ橋渡し役は、Swingに依存しないクラスを使用すると、時間のかかる処理をSwingから切り離すことができました。
こうすることで、時間のかかる処理をCUI等Swingに依存しない部位にて再利用することができると思います。
また、SwingWorkerを包含するAppPanel側は、時間のかかる処理(ホットスポット)をGUIから引き離すことができました。今回は手が届きませんでしたが、時間がかかる処理の時に表示するプログレス・ダイアログをジェネリックなクラスでひとつ作っておけば、こちらも共通部品として再利用可能と思われます。