連載
» 2004年02月21日 00時00分 公開

Eclipse徹底活用(7):EclipseによるSWTアプリケーションの作成 (3/4)

[金子崇之, 岡本隆史,NTTデータ]

ウィジェットへのイベントリスナーの登録処理の追加

 続いて、ウィジェットへのイベントに対し、ロジックを呼び出すリスナーを登録します。

メニューへの登録

 openメソッドでメニューを作成した直後に、各メニューに対して呼び出すべき処理を登録します。

    // 開くメニュー
    mItemOpen.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {

        // ファイルダイアログを開き、ファイル名を入手
        getOpenFileName();

        if (fileName != null) {
          // CSVファイル読み込み事前処理
          loadBegin();
          // CSVファイル読み込み処理
          loadFile();
          // CSVファイル読み込み事後処理
          loadEnd();
        }
      }
    });

    //終了メニュー
    mItemExit.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        close();
      }
    });

 メニューへの処理の登録は、各メニューに対しaddSelectionListenerメソッドを使用してリスナーを追加することで実現できます。

 SelectionAdapterクラスは、メニューが持つaddSelectionListenerメソッドの引数ISelectionインターフェイスを空実装するクラスです。SelectionAdapterのwidgetSelectedメソッドを、無名クラスを用いてオーバーライドすることで、ISelectionインターフェイスのすべてのメソッドを実装しなくてもよくなります。

テーブル選択時の処理の追加

 createTableメソッド内で、テーブルに対し、メニューと同様にリスナーを登録します。

    table.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {
        // テーブル選択時に、行番号をステータスバーに表示
        setTableSelectedIndex();
      }
    });

 ここまでのソースファイルはここかから入手できます。(ファイル名、クラス名を変えています)

 以上のコーディングを終了したら、プログラムを実行してみてください。適当なCSVファイルを用意し、読み込んでみましょう。

画面 CSVファイルを読み込んだ例

 これでCSVViewerの機能としては一応完成です。ここまでのソースは、ここからダウンロードしてください。

 しかし、実はこのアプリケーションには問題があります。それは、行数の多いCSVファイルを読み込んだときに、ファイル読み込み中はアプリケーションが停止してしまうという問題です。読み込み状況が分からないとともに、ウィンドウの移動や最小化などが行えなくなってしまいます。

 次では、この問題を解決すべく、アプリケーションの修正を行います。

別スレッドからの呼び出し

なぜ停止してしまうのか?

 正確には、アプリケーションは停止しているわけではありません。loadFileメソッド実行中に、GUIへの変更を描画する処理が呼び出されなくなるため、再描画やウィンドウの状態変化(イベント)などへの処理が遅れてしまうのが原因です。

 mainメソッド内のループでDisplayオブジェクトのreadAndDispatchメソッドを呼び出していますが、実はこのメソッドを呼び出すことで描画やイベントの処理が行われます。SWTではDisplayオブジェクトを生成し、ループ内でreadAndDispatchメソッドを実行するスレッドをUIスレッドと呼びます。

 このUIスレッドが長時間かかる処理を呼び出すと、アプリケーションが停止しているように見えてしまいます。

図 現状の処理の流れ (クリックすると拡大します)

 そこでこの状況を解決するために、別スレッド上でファイルの読み込み処理を行わせるように修正します。

図 修正後の処理の流れ (クリックすると拡大します)

スレッド化する

Runnableを実装するクラスの作成

 別スレッドで読み込み処理を行うための、Runnableを実装するクラスを作成します。

 読み込み処理では、CSVViewerがインスタンス変数として持つウィジェットを多数参照する必要があります。Runnableを実装するクラスをCSVViewerとは別のソースファイルとして作成することもできますが、今回はプログラムを簡易にするために、CSVViewerのインナークラスとして作成します。

  class CSVLoader implements Runnable {
    public void run() {
    }
  }

[開く]メニューの処理の変更

 loadBegin、loadFile、loadEndを呼び出していた部分を、別スレッドを生成して実行するように変更します。

    // 開くメニュー
    mItemOpen.addSelectionListener(new SelectionAdapter() {
      public void widgetSelected(SelectionEvent e) {

        // ファイルダイアログを開き、ファイル名を入手
        getOpenFileName();

        if (fileName != null) {
          // 別スレッドで実行
          new Thread(new CSVLoader()).start();

        }
      }
    });   

メソッド、フィールドの移動

 loadBegin、loadFile、loadEndメソッドをCSVLoaderに移動します。また、これらのメソッドからのみ使用するインスタンス変数beginTimeもCSVLoaderに移動します。そして、runメソッド内でこれらのメソッドを呼び出すようにします。

 CSVLoader部分のコードは、以下のようになります。

  class CSVLoader implements Runnable {

    // 読み込み時間測定用
    private long beginTime;

    public void run() {
      // CSVファイル読み込み事前処理
      loadBegin();
      // CSVファイル読み込み処理
      loadFile();
      // CSVファイル読み込み事後処理
      loadEnd();
    }
    private void loadBegin() {
      // 開くメニューを選択不可にする
      mItemOpen.setEnabled(false);
      ...【省略】
    }

    private void loadFile() {
      ...【省略】
    }

    private void loadEnd() {
      ...【省略】
    }

  }

 以上でファイルの読み込み処理を別スレッドに委譲するコードになりました。

スレッド化の問題

 さて、この状態でプログラムを実行すると、どうなるでしょうか。実は、[開く]メニューを選択不可にするところで、以下のようなSWTExceptionが発生します。

org.eclipse.swt.SWTException: Invalid thread access
  at org.eclipse.swt.SWT.error(SWT.java:2330)
  at org.eclipse.swt.SWT.error(SWT.java:2260)
  at org.eclipse.swt.widgets.Widget.error(Widget.java:385)
  at org.eclipse.swt.widgets.Widget.checkWidget(Widget.java:315)
  at org.eclipse.swt.widgets.MenuItem.setEnabled(MenuItem.java:557)
  at csvviewer.CSVViewer$CSVLoader.loadBegin(CSVViewer.java:173)
  at csvviewer.CSVViewer$CSVLoader.run(CSVViewer.java:164)
  at java.lang.Thread.run(Thread.java:534)

 この例外は、SWTのウィジェットを操作する処理が、上記で述べたUIスレッド以外から実行されたために発生します(UIスレッドにのみ実行が許されている)。SWTではこのような制限を設けることによって、複数スレッドからのGUI操作に対してシンプルでかつ安全な動作を提供しています。詳細については、Eclipseのヘルプの[Platform プラグイン・デベロッパー・ガイド]->[プログラマーズ・ガイド]->[Standard Widget Toolkit]->[スレッド化の問題]を参照してください。

 SWTでは、このように別スレッドからSWTのウィジェットを操作するときには、DisplayクラスのsyncExec、asyncExecメソッドを使用して、UIスレッドに処理を委譲します。

別スレッドからSWTのウィジェットを操作するときの処理の流れ (クリックすると拡大します)

 仕組みが理解できたところで、コードを修正していきましょう。

UIスレッドへの委譲

asyncExecメソッドの追加

 修正対象は、loadBegin、loadFile、loadEndメソッドになりますが、まずはユーティリティメソッドを追加します。マルチスレッド環境下でdisplayが破棄されることも考慮し(例えば処理中にウィンドウが閉じられたなど)、以下のユーティリティメソッドをCVSLoaderに追加します。

    private boolean checkAsyncExec(Runnable r) {
      if (!display.isDisposed()) {
        display.asyncExec(r);
        return true;
      } else {
        return false;
      }
    }    

loadBeginメソッドの修正

 SWTのウィジェットを操作する個所を、checkAsyncExecメソッドを使ってUIスレッドに実行させるように修正します。

  private void loadBegin() {
   checkAsyncExec(new Runnable() {
    public void run() {

     //[ 開く] メニューを選択不可にする
     mItemOpen.setEnabled(false);

     // テキストボックスにファイル名を設定
     textBox.setText(fileName);

     // カラム数が変わるので、テーブルを再作成する
     Composite comp = table.getParent();
     table.dispose();
     createTable(comp);
     comp.layout();

     // ステータスバーに状態を表示
     statusBar.setText("読み込み中...");
    }
   });


   // 読み込み時間測定用
   beginTime = System.currentTimeMillis();
  } 

loadFileメソッドの修正

 loadBeginと同様に、checkAsyncExecメソッドを使用します。whileループの中のみ、以下にコードを示します。

  private void loadFile() {

   ・・・【省略】

    while ((line = reader.readLine()) != null) {

     // カンマで文字列を区切る
     final String[] datas = line.split(",");

     boolean isOK = checkAsyncExec(new Runnable() {
      public void run() {
       if (table.isDisposed()) {
        return;
       }

       // 列が少なかったら加える
       for (int i = table.getColumnCount();
        i < datas.length;
        i++) {
        TableColumn column =
         new TableColumn(table, SWT.LEFT);
        column.setText("列" + (i + 1));
        column.setWidth(50);
       }

       // 行を設定
       TableItem item = new TableItem(table, SWT.NULL);
       item.setText(datas);
      }
     });
     
     if (!isOK) {
      return;
     }

    }

   ・・・【省略】

  }

 Runnableのrunメソッド内でloadFileメソッドのローカル変数datasを参照するため、datasをfinal宣言しています。また、display、tableのどちらかが破棄されていた場合には処理を続けても無意味なので、メソッドを抜けるようにしています。

loadEndメソッドの修正

 loadBeginメソッドと同様に、checkAsyncExecメソッドを使用します。

    private void loadEnd() {
      checkAsyncExec(new Runnable() {
        public void run() {
          if (mItemOpen.isDisposed() || statusBar.isDisposed()) {
            return;
          }

          mItemOpen.setEnabled(true);
          long end = System.currentTimeMillis();
          statusBar.setText(
            "読込完了 : 処理時間"
            + (end - beginTime) + "ミリ秒");
        }
      });

    }

 メニュー、ステータスバーが破棄されていた場合には、処理を続けられないのでメソッドを抜けるようにしています。

 以上で完成です。ここまでのソースファイルはここから入手できます(ファイル名、クラス名を変えています)。

 CSVViewerを実行し、CSVファイルの読み込み中に、テーブルのスクロールバーの移動、ウィンドウの最小化や移動を行ってもアプリケーションが素早く応答することを確認してください。

 さて、ここまでのCSVViewerではアプリケーションの応答速度は向上しましたが、その半面、ファイルの読み込みにより時間がかかるようになっています。CSVViewerの最後の修正として、チューニングを行います。

チューニング

現状把握

 読み込むファイルの大きさや、ディスクやCPUなどの実行環境の性能にもよりますが、筆者の環境では1万行のレコードを読み込むのに、スレッド化していないもので7.0秒、スレッド化したもので11.7秒と、スレッド化によって約1.7倍の時間がかかっています。

 asyncExecメソッド1回の呼び出しにつき、実行オブジェクトの生成、Display内のキューへの積み上げが行われます。そしてUIスレッドによる実行オブジェクトのキューからの取得、実行が行われます。その間には、OSからのイベントの監視なども行われます。

 当然これらの処理に対するオーバーヘッドが存在します。そして呼び出し回数が多くなると、そのオーバーヘッドが無視できない大きさになってしまいます。

 通常、このような呼び出し回数に関する問題は、バッファリングにより呼び出し回数を減らすことで対応することができます。そこで、ファイルからの読み込み1行ごとに呼び出しているasyncExecメソッドを、100行ごとに呼び出すように修正します。

1行ごとに呼び出しと、100行ごとに呼び出した場合の違い (クリックすると拡大します)

loadFileメソッドの修正

 ArrayListに読み込んだデータを一時的に格納しておくことにより、バッファリングを実現します。

    private void loadFile() {
      ・・・【省略】

        // バッファサイズ
        int bufferSize = 100;


        while ((line = reader.readLine()) != null) {

          final ArrayList array = new ArrayList(bufferSize);

          // カンマで文字列を区切る
          String[] datas = line.split(",");
          array.add(datas);

          // バッファサイズまで読み込み
          for (int i = 1;
            i < bufferSize && (line = reader.readLine()) != null;
            i++) {
            datas = line.split(",");
            array.add(datas);
          }

          boolean isOK = checkAsyncExec(new Runnable() {
            public void run() {
              if (table.isDisposed()) {
                return;
              }

              for (int i = 0; i < array.size(); i++) {
                String[] datas = (String[]) array.get(i);


                // 列が少なかったら加える
                for (int j = table.getColumnCount();
                  j < datas.length;
                  j++) {
                  TableColumn column =
                    new TableColumn(table, SWT.LEFT);
                  column.setText("列" + (j + 1));
                  column.setWidth(50);
                }

                // 行を設定
                TableItem item = new TableItem(table, SWT.NULL);
                item.setText(datas);
              }
            }
          });

          if (!isOK) {
            break;
          }
        }
      ・・・【省略】
    }

 完成したソースファイルはここから入手できます(ファイル名、クラス名を変えています)。

 実行し効果を確認したところ、筆者の環境では7.9秒とスレッド化とチューニングを施していないものと比べて約1.1倍までオーバーヘッドを縮めることができました。

 このチューニングの注意点としては、asyncExecメソッドに委譲する処理の中で長時間かかる処理を実行してしまうと、前節で作成したCSVViewerと同じようにUIスレッドが占有されてしまうことです。最適なバッファサイズについては環境に依存しますし、また今回の記事の趣旨からは外れますので、CSVViewerはこれで完成とします。

まとめ

 本稿では、SWTについての解説と、Eclipseを用いたSWTアプリケーションの作成方法を紹介しました。また、SWTアプリケーションでUIスレッドとは別のスレッドを使用する方法と、SWTの重要な仕組みであるasyncExecメソッドに特化した簡単なチューニングも行いました。

 SWTを使用すれば、GUIのコンポーネントを組み合わせて、高度なアプリケーションを簡単に組み上げることができます。それは、EclipseがSWTで記述されていることからもお分かりいただけると思います。

 本稿が参考となり、快適なクライアントアプリケーションやEclipseプラグインを構築するためのきっかけとなれば幸いです。

 次回の記事では、今回のSWTの内容を踏まえ、Eclipseのプラグインを開発します。お楽しみに。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。