いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Swingスレッド処理-005(処理をキャンセル可能にする)


前回までの記事


SwingWorkerを利用して、処理をキャンセル可能にします。
基本的な手順は前回と同じく、以下のようにします。

  1. 重い処理(時間のかかる処理)はSwingWorkerを利用してバックグラウンドで行う。
  2. 進捗状況、ステータスの更新は、SwingWorkerのprocess,doneメソッドなどを用いてEvent Dispatch Threadで処理。
  3. ユーザがキャンセルを行った場合は、SwingWorker.cancel()をコール。(SwingWorkerはjava.util.concurrentのFutureを実装している)
  4. その他、バックグラウンド処理開始時、終了時などに行いたい処理はSwingWorkerにPropertyChangeListenerを追加する。

以下サンプルです。
このアプリケーションは、開始ボタンが押下されるとJava EE 5のチュートリアルのPDFを
ダウンロードします。(実際には、データの読み込みのみをおこなってるだけで、ファイルの保存はしてません)
このPDFは大体9MBくらいありますので、それなりに早いネットワークでも時間がかかると思います。


画面は、以下のような感じ。


[起動時]

[開始ボタン押下時]

[ダウンロード完了]

[キャンセル時]

このアプリケーションは、以下の名前で各コンポーネントを定義しています。

  • ダウンロード先URL:downloadUrlLabel
  • 開始ボタン:startButton
  • キャンセルボタン:cancelButton
  • プログレスバー:progressBar
  • ステータスバーラベル:statusLabel


まずは、フォーム本体の部分。
以下のようになります。

package gsf.samples.swing.swingworker;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import javax.swing.JOptionPane;
import javax.swing.Timer;
import org.jdesktop.swingworker.SwingWorker;

/**
 * アプリケーションのメインフォームクラスです.<br/>
 * このフォームは、開始ボタンが押下されるとJava EE 5チュートリアルPDFの<br/>
 * ダウンロードを行います。<br/>
 *
 * @author  gsf_zero1
 */
public class MainForm extends javax.swing.JFrame {
    
    /** バックグラウンド処理を実行するワーカーオブジェクト */
    private SwingWorker<String, String> _worker;
    
    /** 
     * コンストラクタ.<br/>
     *
     */
    public MainForm() {
        initComponents();        
    }

    private void initComponents() {
        //
        // 省略....
        //
    }

    /**
     * フォームが最初にオープンされた際にコールバックされるリスナーメソッドです.<br/>
     *
     * @param evt イベントオブジェクト
     */
    private void formWindowOpened(java.awt.event.WindowEvent evt) {

        startButton.setEnabled(true);
        cancelButton.setEnabled(false);
        
    }

    /**
     * キャンセルボタンが押下された際にコールバックされるリスナーメソッドです。<br/>
     *
     * @param evt イベントオブジェクト
     */
    private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {
        
        _worker.cancel(true);
        
        startButton.setEnabled(true);
        cancelButton.setEnabled(false);        
        
    }

    /**
     * 開始ボタンが押下された際にコールバックされるリスナーメソッドです.<br/>
     * 該当ファイルのダウンロードを行います.<br/>
     * 
     * @param evt イベントオブジェクト
     */
    private void startButtonActionPerformed(java.awt.event.ActionEvent evt) {

        URL url = null;
        try {

            url = new URL(downloadUrlLabel.getText());
            
        } catch (MalformedURLException ex) {
            JOptionPane.showMessageDialog(null, ex.getMessage(), "エラーが発生しました.", JOptionPane.ERROR_MESSAGE);
            return;
        }
        
        startButton.setEnabled(false);
        cancelButton.setEnabled(true);
        
        _worker = new DownLoadWorker(url);
        _worker.execute();
    }
    
    protected javax.swing.JButton cancelButton;
    protected javax.swing.JLabel downloadUrlLabel;
    protected javax.swing.JLabel jLabel1;
    protected javax.swing.JLabel jLabel2;
    protected javax.swing.JPanel jPanel1;
    protected javax.swing.JProgressBar progressBar;
    protected javax.swing.JButton startButton;
    protected javax.swing.JPanel statusBarPanel;
    protected javax.swing.JLabel statusLabel;

    //
    // カスタムSwingWorkerの定義(後述)
    //

    //
    // PropertyChangeListenerの定義(後述)
    //
}


次に、カスタムSwingWorkerクラスの定義と追加するPropertyChangeListenerです。

    /**
     * 指定されたURLデータのダウンロードを行うSwingWorkerクラスです.<br/>
     *
     * @author  gsf_zero1
     * @version 1.0
     */
    class DownLoadWorker extends SwingWorker<String, String>{
        
        /** URL */
        private URL               _url;
        
        /** データダウンロードに使用するコネクション */
        private HttpURLConnection _conn;
        
        /**
         * コンストラクタ.<br/>
         *
         * @param downLoadUrl ダウンロード先
         */
        public DownLoadWorker(URL downLoadUrl){
            _url = downLoadUrl;
            
            addPropertyChangeListener(new WorkerProgressPropertyChangeListener());
            addPropertyChangeListener(new WorkerStateValuePropertyChangeListener());
        }
        
        /**
         * ダウンロード処理を行います.<br/>
         *
         * @return 処理結果
         * @throws 処理中にエラーが発生した場合
         */
        protected String doInBackground() throws Exception {
            
            if(isCancelled()){
                return "処理はキャンセルされました。";
            }
            
            progressBar.setValue(0);
            
            publish("コネクションを確立中・・・・");
            
            _conn = (HttpURLConnection) _url.openConnection();
            _conn.setRequestMethod("GET");
            _conn.connect();
            
            //
            // ダウンロードファイルサイズを取得.
            //
            double totalBytes = _conn.getContentLength();

            publish("コネクションを確立しました。ダウンロード中です・・・・・    ファイルサイズ 約:" + (int) (totalBytes / 1024 / 1024) + "MB");
                        
            BufferedInputStream in = null;
            try{
                
                in = new BufferedInputStream(_conn.getInputStream());
            
                double currentReadBytes = 0.0;
                for(byte[] buf = new byte[1024]; in.read(buf) != -1; buf = new byte[1024]){
                    if(isCancelled()){
                        break;
                    }
                    
                    currentReadBytes += buf.length;
                    
                    setProgress( (int) ( (currentReadBytes / totalBytes) * 100));
                }
                
            }finally{
                if(in != null){
                    in.close();
                }
            }
            
            return "ダウンロードが完了しました。";
            
        }
        
        /**
         * doInBackgroundメソッドにてpublishされた際にコールバックされるメソッドです.<br/>
         * 処理の経過を表示します.<br/>
         *
         * @param chunks publish()で指定された引数データ
         */
        protected void process(List<String> chunks) {
            
            String message = chunks.get(0);
            
            statusLabel.setText(message);
            
        }
        
        /**
         * 処理が完了した際にコールされるメソッドです.<br/>
         * 終了メッセージの設定を行います.<br/>
         *
         */
        protected void done() {
            
            String message = null;
            try {
                
                message = get();
                
            } catch (ExecutionException ex) {
                message = "処理実行中に、エラーが発生しました。";
            } catch (InterruptedException ex) {
                message = "処理はキャンセルされました。";
            } catch (CancellationException ex){
                message = "処理はキャンセルされました。";
            }finally{
                
                statusLabel.setText(message);                
                
                if(_conn != null){
                    _conn.disconnect();
                }
                
            }
            
        }
        
    }

    /**
     * ワーカオブジェクトにて、progressバウンドプロパティの値が変更された際に<br/>
     * コールバックされるリスナーです.<br/>
     * 指定された値をフォームのJProgressBarに設定する役割を担当します.<br/>
     *
     * @author  gsf_zero1
     */
    class WorkerProgressPropertyChangeListener implements PropertyChangeListener{

        public void propertyChange(PropertyChangeEvent evt) {

            if("progress".equalsIgnoreCase(evt.getPropertyName())){
                progressBar.setValue( (Integer) evt.getNewValue() );
            }
            
        }
        
    }
    
    /**
     * ワーカーオブジェクトにて、stateバウンドプロパティ変更の際にコールバックされる<br/>
     * リスナーです.<br/>
     * 指定された値をフォームのステータスバーに表示する役割を担当します。<br/>
     *
     * @author  gsf_zero1
     */
    class WorkerStateValuePropertyChangeListener implements PropertyChangeListener{
        
        public void propertyChange(PropertyChangeEvent evt) {
            
            if("state".equalsIgnoreCase(evt.getPropertyName())){
                SwingWorker            worker = (SwingWorker)            evt.getSource();
                SwingWorker.StateValue state  = (SwingWorker.StateValue) evt.getNewValue();
                
                if(SwingWorker.StateValue.STARTED == state){
                    statusLabel.setText("処理を実行中です・・・しばらくお待ちください。");
                }else if(SwingWorker.StateValue.DONE == state){
                    if(!worker.isCancelled()){
                        statusLabel.setText("ダウンロードが完了しました。");
                    }                    
                    
                    startButton.setEnabled(true);
                    cancelButton.setEnabled(false);
                }
                
            }
            
        }
        
    }

開始ボタン押下時のイベントリスナーの部分は、事前処理を行い、Workerを作成して
すぐに処理を戻すようにしています。このようにすることで、GUIがブロックされることを
防ぎます。上記の場合のように、時間がかかる処理も含めて
全ての処理を単一のリスナ内で行うとその処理時間分GUIがブロックされるからです。
(リスナーメソッドはEvent Dispatch Threadから呼ばれます)


Workerに追加している2つのPropertyChangeListenerは、それぞれ以下のことを担当します。

  • 処理開始時と終了時の前処理と後処理。
  • 進捗状況の更新。


SwingWorkerは内部にprogressとstateという2つのバウンドプロパティを持っています。
それを利用して処理を行います。


追記:
ソースとモジュールをアップしようとしたけど、はてなって画像ファイルしかアップできないんですね・・・。

更に追記:
ファイルダウンロードの部分ですが、前はtimerを使用して適当にプログレスバーを進めていた部分を
一応ちゃんとファイルサイズみて進ませるようにしました。


================================
過去の記事については、以下のページからご参照下さい。