2015年8月31日月曜日

SwingWorker サンプル

目次へ



  • イベントディスパッチスレッドとは
  • SwingWorkerとは
  • SwingWorkerの2つの引数
  • SwingWorkerサンプル


■■■■イベントディスパッチスレッドとは

Swingの描画に関連する処理と、イベントに対する応答処理(たとえばJButtonが押された時に呼び出されるactionPerformed等は イベントディスパッチスレッドと呼ばれる一つのスレッドで実行されます。
そのため、たとえば、ボタンを押すと画像を表示しているJLabelが300msecおきに5ピクセルづつ10回動くようにしたくて(ゆっくり進めたい)、 下のようなプログラムを作ったとすると、うまくいきません。
このプログラムを実行し、ボタンを押すと、300×10msecの間、画像は何も動かず、3000msec後にいきなり、5×10ピクセル移動してしまうことになります。

これはactionPerformedと、label.setBoundsを実行することによる描画処理が1つのスレッドで行われるためです。


なぜこうなるのでしょう
ボタンを押すと、①のactionPerformedがイベントディスパッチスレッド上で実行開始します。
1回目のループで②が実行されると、描画処理はイベントディスパッチスレッド上で行われなければいけないのですが、 イベントディスパッチスレッド上ではactionPerformedを実行中のため、これが終了したら実行できるように、 イベントキューにこの処理をならべます。
イベントキューとは、複数のイベントが発生した時に同時にその処理を実行できないため、それらのイベントを並べておく待ち行列です。
イベントディスパッチスレッドは、イベントキューに並んでいる処理を順番に実行していくことになります。

actionPerformedはイベントキューに描画処理を並べた後、300msecのスリープに入ります。
actionPerformedが終了するまでイベントキューに並んでいる処理は実行できませんから、まだ、描画処理は実行されず、待ち続けます。
2回目のループでも同じで、②の位置まで来ると、ひとつ前の②の処理がまだ実行されずにイベントキューで待っていますが、 その後ろに、②の処理を並べ、スリープします。
こうして、10回のループがやっと終了すると、イベントキューに並んでいる10個の処理が順番に実行されます。
10個の処理はスリープもなく、すぐに実行できますから、人間の目には1ぺんに、最初の位置から、最後の位置まで動いたように見えます。


うまくいかないプログラム
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class SwingworkerNasiTest extends JFrame {
 private static int WIDTH = 29; //画像幅
 private static int HEIGHT = 41; //画像高さ

 JButton btnWalk = new JButton("歩く");
 JLabel  label   = new JLabel(new ImageIcon("./penguin.gif"));
 JPanel  panel = new JPanel();
 JPanel  contentPane = (JPanel)getContentPane();

 int labelX = 0;   //ラベルの位置x
 int  labelY = 0;   //ラベルの位置y

 public SwingworkerNasiTest() {
  //--------------------------- パネルとボタンを置く
  contentPane.add(panel,BorderLayout.CENTER);
  contentPane.add(btnWalk,BorderLayout.SOUTH);

  //---------------------------画面の設定
  this.setSize(500,300);
  this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  this.setVisible(true);

  //----------------- パネルの右下にペンギンの乗ったラベルを置く
  panel.setLayout(null);
  labelX = panel.getWidth()-WIDTH;
  labelY = panel.getHeight()-HEIGHT;
  label.setBounds(labelX, labelY, WIDTH, HEIGHT);
  panel.add(label);

  //--------------------------- ボタンの処理
  btnWalk.addActionListener(new ActionListener(){
   public void actionPerformed(ActionEvent e) {   //①
    for(int i=0; i<10; i++) {
     labelX -= 5;
     label.setBounds(labelY, labelY, WIDTH, HEIGHT); //②
     try {
      Thread.sleep(300);
     } catch (InterruptedException e1) {}
    }
   }
  });
 }
 public static void main(String[] args) {
  new SwingworkerNasiTest();
 }
}



■■■■SwingWorkerとは

SwingWorkerクラスはイベントディスパッチスレッドと別スレッドで処理をしたいときに使います。
たとえば、イベントディスパッチスレッド上で実行されるactionPerformedの中で長~い処理を書いた場合、 その間、描画処理などができなくなってしまいます。
その長い処理は別スレッドで行えればよいわけです。
別スレッドで行う処理はSwingWorkerを継承したクラスのdoInBackgroundに記述し、actionPerformedの中でこのクラスのインスタンスを作成し、 実行を開始します。

これだけなら、普通のスレッドと同じ気もしますが、SwingWorkerの場合、別スレッドで行いたい処理と、 イベントディスパッチスレッドで行いたい処理 の両方を書くことができます。
別スレッドで行いたい長~い処理は、上述のとおり、doInBackgroundに記述し、 イベントディスパッチスレッド上で行いたい処理は、done()あるいはprocess()に記述します。
doneはdoInBackgroundが終了してから実行したい場合
processはdoInBackground実行中に実行したい場合に使います。

doneメソッドの中に書いた処理は、doInBackgourndメソッドが終了すると自動で呼び出され、イベントディスパッチスレッド上で実行されます。
processメソッドの中に書いた処理は、doInBackgroundの中で、publish()を呼び出すと、やはり、イベントディスパッチスレッド上で実行されます。

また、イベントディスパッチスレッド上で実行しなければいけないような処理が無い場合にはdoInBackgroundのみ作成すればよいのです。


■■■■SwingWorkerの2つの引数

SwingWorkerには
SwingWorker<Object,Object>
のように2つの引数があります。
この2つの型はdone、あるいは、processに渡すためのデータの型で、必要な型を書けばよいのです。

まず、1つ目の型はdoInBackgroundの戻り値の型で、これは、doneメソッドの中でget()を呼び出すことで使えます。

2つ目の型はdoInBackgroundの中でpublishを呼び出すときに、引数として使います。
この引数はprocessメソッドに渡されます。
また、doInBackgroundと、processは別スレッドで実行されるため、 1回目のporocessがまだ実行される前に2回目のprocessが呼び出される可能性もあります。
そのような場合、効率のために、processは1回の呼び出しにまとめられ、publish2回分の引数がprocessに渡されます。

たとえば、SwingWorker<T,V>とした場合、
void publish(V... chunks)
void process(List<V> chunks)
となり、publishで渡せるのは、V型の引数をいくつでも渡せるし、processでは、たまたま、publishを2回分実行するような場合には、 2回渡された引数を2個のListとして受け取ることができるようになります。
また、publishは可変個引数ですから、processに引数で渡すものが無い場合には、引数なしで呼び出せばよいことになります。


■■■■SwingWorkerサンプル

さて、上でうまくいかなかったプログラムをSwingWorkerを使って変更してみます。
まずSwingWorkerを継承したクラスを作成します。②
別スレッドで行うのは、300msec待つという長い処理です。これをdoInBackgroundに書きます。③
label.setBoundsの部分はイベントディスパッチスレッド上で実行しなければいけないので、processに書きます。⑤
ループの途中でprocessを呼び出したいので、publishを呼び出し、間接的にprocessを呼び出します。④

10回のループが終わった後、イベントディスパッチスレッド上で実行しなければいけないことはないので、 doneメソッドは、本当は作る必要はないです。削除しても大丈夫です。⑥

doInBackgroundからprocessに渡すデータはないので、publishの引数はなしです。④
引数の型は、使わないので、<Object,Object>としています。②

これで、SwingWorkerを継承したクラスができたので、actionPerformedの中で、このインスタンスを作り、executeを呼び出します。①
executeを呼び出すことにより、別スレッドで、doInBackgroundの実行が開始されます。


SwingWorkerを使ったクラス
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingWorker;

public class SwingworkerTest extends JFrame{
 private static int WIDTH = 29;
 private static int HEIGHT = 41;

 JButton btnWalk = new JButton("歩く");
 JLabel  label   = new JLabel(new ImageIcon("./penguin.gif"));
 JPanel  panel = new JPanel();
 JPanel  contentPane = (JPanel)getContentPane();

 int  labelX = 0;
 int  labelY = 0;

 public SwingworkerTest() {
  //--------------------------- パネルとボタンを置く
  contentPane.add(panel,BorderLayout.CENTER);
  contentPane.add(btnWalk,BorderLayout.SOUTH);

  //---------------------------画面の設定
  this.setSize(500,200);
  this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  this.setVisible(true);

  //----------------- パネルの右下にペンギンの乗ったラベルを置く
  panel.setLayout(null);
  labelX = panel.getWidth()-WIDTH;
  labelY = panel.getHeight()-HEIGHT;
  label.setBounds(labelX, labelY, WIDTH, HEIGHT);
  panel.add(label);

  //--------------------------- ボタンの処理
  btnWalk.addActionListener(new ActionListener(){
   public void actionPerformed(ActionEvent e) {
    MySwingWorker worker = new MySwingWorker();
    worker.execute(); //①
   }
  });
 }
 class MySwingWorker extends SwingWorker <Object,Object> { //②
  protected Object doInBackground() throws Exception { //③
   for(int i=0; i<10; i++) {
    publish(); //process()を呼び出す④
    try {
     Thread.sleep(300);
    } catch (InterruptedException e1) {}
   }
   return null;
  }
  protected void process(List <Object>  chunks) {  //⑤
   labelX -= 5;
   label.setBounds(labelX, labelY, WIDTH, HEIGHT);
   System.out.println("process");
  }
  protected void done() {     //⑥
   System.out.println("doInBackgroundの処理が終了しました");
  }
 }
 public static void main(String[] args) {
  new SwingworkerTest();
 }
}



にほんブログ村 IT技術ブログ IT技術メモへ
にほんブログ村