« 2004年4月 | トップページ | 2004年6月 »

2004.05.26

アクション起動時の要素選択でのIAdaptableの利用

Package Explorerビューの各要素について,コンテキストメニューに追加したアクションの起動を,ファイルに対してのみ行いたいと考えたとする。例えば,

package-explorer.gif

という感じだった場合に,「class-icon.gifEmployee.java」「,「class-icon.gifEmployeeTest.java」「class-icon.giftest.exclusion」という3つのファイルに対してアクションを実行したいというシチュエーションを想像して欲しい。

コンテキストメニューへのアクションの追加」で取り上げた方法を参考にして,Eclipse内でファイルリソースはIFileオブジェクトで表現されていることを踏まえると,以下のようなアクションの定義が考えられる。

  <extension point="org.eclipse.ui.popupMenus">
    <objectContribution
        id="yoichiro.myPlugin"
        objectClass="org.eclipse.core.resources.IFile">
      <action
          id="yoichiro.myPlugin.MyAction"
          name="My action"
          class="yoichiro.myPlugin.MyAction">
      </action>
    </objectContribution>
  </extension>

実際に上記をプラグイン・マニフェストに記載して実行してみると,「class-icon.giftest.exclusion」ファイルしかアクションの対象にならない。ビューの見かけ上はファイルなのに,なんで!?ってことになってしまう。

実はclass-icon.gifEmployee.java」と「class-icon.gifEmployeeTest.java」という要素は,ICompilationUnitオブジェクトであって,IFileオブジェクトではない。同時に,2つのファイルが持つクラスのノード(class-icon.gifアイコンの要素)に関しても,「コンテキストメニューへのアクションの追加」で紹介したように,ITypeオブジェクトであってIFileオブジェクトではない。よって,上記の記述ではこれらの要素はIFileオブジェクトではないと判断され,アクションが有効にならないのである。「Package Explorerに表示されるあらゆる要素の種別を予測して,個別に定義していかなくちゃいけないの?」となると,とっても面倒だし柔軟性という点でとても良いとはいえない。

Eclipseでは,このような問題に対して,IAdaptableインタフェースを使うことで解決している。上記のプラグイン・マニフェストで記述されているobjectContribution要素のobjectClass属性で指定された型について,仮にICompilationUnitオブジェクトやITypeオブジェクトであっても,「おまえホントはIFileオブジェクトちゃうんか!?」という問い合わせを各要素に対して行わせるための指定をすることができる

  <extension point="org.eclipse.ui.popupMenus">
    <objectContribution
        id="yoichiro.myPlugin"
        objectClass="org.eclipse.core.resources.IFile"
        adaptable="true">
      <action
          id="yoichiro.myPlugin.MyAction"
          name="My action"
          class="yoichiro.myPlugin.MyAction">
      </action>
    </objectContribution>
  </extension>

Package Explorer上に表示されている要素は,IAdaptableインタフェースを実装したオブジェクトである。上記のようにadaptable属性にtrueを渡すことにより,Eclipseプラットフォームは各要素に対して,getAdapterメソッドにIResourceクラスオブジェクトを渡して呼び出し,その戻り値がobjectClass属性で指定した型かどうかをチェックする。そして,もしgetAdapterメソッドの戻り値のオブジェクトがobjectClass属性で指定したクラスの型であれば,その要素はアクションを有効にして良いと判断され,アクションがコンテキストメニューに表示されるようになる。

ITypeインタフェースの実装クラスでは,getAdapterメソッドにIResourceクラスオブジェクトが渡されると,そのクラスが定義されたファイルを表すIFileオブジェクトを返却するようになっている。もちろんICompilationUnitオブジェクトやIMethodオブジェクトなども同様である。つまり上記の例ではclass-icon.gifEmployee」の要素はITypeオブジェクトだけど,getAdapterメソッドが利用されることによりEmployee.javaファイルのIFileオブジェクトが取り出され,結果としてアクションが有効になる,という動きになる。

上記のアクションを処理するIObjectActionDelegateインタフェースの実装クラスの中では,例えば,

  public void selectionChanged(IAction action, ISelection selection) {
    IStructuredSelection structured = (IStructuredSelection)selection;
    IAdaptable adaptable = (IAdaptable)structured.getFirstElement();
    IFile file = (IFile)adaptable.getAdapter(IResource.class);
  }

というようにして,IFileオブジェクトを取得することができる。

| | コメント (0) | トラックバック (0)

2004.05.25

エディタを閉じる

今回は「エディタを開く」の逆,エディタを閉じる方法について紹介する。エディタを開く機能があれば,もちろんエディタを閉じる機能も存在する。

エディタを開く」で取り上げたopenEditorメソッドがIWorkbenchPageインタフェースに規定されていたように,エディタを閉じる機能もIWorkbenchPageインタフェースに規定されている。IWorkbenchPageインタフェースには,ある特定のエディタを閉じるためのcloseEditorメソッドすべてのエディタを閉じるためのcloseAllEditorsメソッドが規定されている。

ある特定のエディタを閉じるためには,まず閉じたいエディタを表すIEditorPartオブジェクトを取得する必要がある。これは例えば「アクションの登録先と使用インタフェース」でちょろっと紹介したIEditorActionDelegateインタフェースを使ったアクションなどで取得できる。IEditorPageオブジェクトが取得できたら,あとはそれをcloseEditorメソッドに渡せばよい。IWorkbenchPageオブジェクトの取得方法は「エディタを開く」で取り上げた方法を参考されたし。

  IEditorPage editor = ...;

  IWorkbenchPage page = ...; // 「エディタを開く」参照
  page.closeEditor(editor, true);

closeEditorメソッドの第2引数にtrueを渡すと,もし対象のエディタの内容が保存されていないときに,ユーザに内容を保存するかどうかを問い合わせるダイアログが表示されるようになる(「すべてのエディタの内容を保存する」の図を参照)。逆に,falseを指定した場合は,エディタの内容は保存されずに破棄されてエディタが閉じるという,ちょっと恐ろしい動作になる。

開かれているすべてのエディタを一気に閉じるには,単にcloseAllEditorsメソッドを呼び出せばよい

  IWorkbenchPage page = ...; // 「エディタを開く」参照
  page.closeAllEditors(true);

この場合も,引数にtrueを渡せば,保存しなくていいかを確認するためのダイアログが表示される。逆に,falseを渡すと,全エディタの変更内容を保存することなく問答無用で破棄してエディタを閉じてしまうという,かなり凶悪な動作になる。

ユーザビリティを考えると,falseで呼び出すことは避けたほうがいいだろう。。。

| | コメント (0) | トラックバック (0)

2004.05.24

エディタを開く

【追記(2004.09.03)】 Eclipse3.0からは「エディタを開く Part2」のやり方を参考にしてください。

通常エディタは,Package Explorer上などでユーザがダブルクリックなどの操作を行うことで開かれる。しかし時には,プラグインが自動的にエディタを開いてどうのこうの・・・,という動作をしたいこともあるだろう。ここでは,プログラム上で何らかのファイルをエディタで開くための方法を紹介する。

最初に,エディタで開きたいファイルをIFileオブジェクトの形で取得する。例えば「コンテキストメニューへのアクションの追加」のやり方でobjectClass属性にIFileを指定し,アクションクラス内で選択されたIFileオブジェクトを取り出す,などが代表例。これについてはさまざまな方法が考えられるので,取得方法は割愛する。

エディタで開きたいIFileオブジェクトが取得できたら,ワークベンチからアクティブなページを取得して,エディタを開く処理を呼び出す

  IFile file = ...;

  IWorkbench workbench = PlatformUI.getWorkbench();
  IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
  IWorkbenchPage page = window.getActivePage();

  IEditorPart editorPart = page.openEditor(file);

アクティブなページのIWorkbenchPageオブジェクトは,「ワークベンチウィンドウ→ページ→ビュー」で紹介した方法で取得する。そしてIWorkbenchPageオブジェクトのopenEditorメソッドに先ほど取得したIFileオブジェクトを渡すことで,エディタが開かれる

IFileオブジェクトを引数にとるopenEditorメソッドでは,以下の手順で開くエディタが決定される。

  (1) ワークベンチに登録されているエディタと拡張子の関連付けから対応するエディタを検索し,関連付けの情報が存在すれば,その情報を元にエディタを開く。

  (2) OSの関連付け情報を元に対応するアプリケーション(Notepadとか)を検索し,関連付けの情報が存在すれば,そのアプリケーションを起動する。

  (3) (1)と(2)のどちらにも関連付け情報がなかったときは,Eclipseデフォルトのテキストエディタを開く。

もし固定的にあるエディタで開きたい場合は,IFileオブジェクトとエディタのIDの2つの引数をとるopenEditorメソッドを用いると良い。

  IFile file = ...;
  String editorId = ...; // エディタのID
  page.openEditor(file, editorId);

OSの関連付け情報を参照するあたり,いかにもEclipseだなぁと思ってしまう。便利に越したことはない。

| | コメント (2) | トラックバック (1)

2004.05.23

自作エディタの定義

最近本職が忙しくてまったく更新していなかったEclipseプラグイン開発Blog。久々の更新第1弾は,エディタの自作について紹介しようと思う。やはりEclipseの中心は「エディタ」である。パースペクティブも,ビューも,ツールバーに乗っかっているアイコンも,すべてはエディタを補佐するためのものであり,エディタの自作ができるようになれば「Eclipseを制覇した」と言うことができるだろう。

自作エディタを新規に作成するには,もちろんプラグイン・マニフェストに新規エディタの定義を記述することから始める。新規エディタの定義は,org.eclipse.ui.editors拡張ポイントを使って定義する

  <extension point="org.eclipse.ui.editors">
    <editor
      id="yoichiro.myPlugin.MyEditor"
      name="My Editor"
      icon="icons/MyEditor.gif"
      extensions="props"
      default="true">
      class="yoichiro.myPlugin.MyEditor"
      contributorClass="yoichiro.myPlugin.MyEditorContributor"
    </editor>
  </extension>

org.eclipse.ui.editors拡張ポイントでは,editor要素の属性を使って新規エディタの各種設定を記述する。id属性はエディタの識別子を,name属性には人間が読むためのエディタの名前を記述する。icon属性は,エディタを開いたときに表示されるアイコンのパスを記述する。

Windowsでは,ファイルの拡張子毎にアプリケーションを関連付けることができるが,Eclipseにおいてもファイルの拡張子にエディタを関連付けることができる。extensions属性に拡張子を記述することにより,その拡張子のファイルが新規エディタに関連付けられる。extensions属性は複数の拡張子を空白区切りで記述することができる。default属性にtrueが指定されることにより,extensions属性で指定された関連付けがEclipse内でデフォルトに採用される

エディタの実装クラスは,IEditorPartインタフェースを実装して作成する。そして,プラグイン・マニフェストでは,class属性にIEditorPartインタフェースの実装クラスのクラス名を記述する。

Eclipseでは,コンテキストメニューやツールバーへのアクションの追加などを,エディタごとに行うことができる。エディタが持つコンテキストメニューやツールバーなどの追加処理は,IEditorActionBarContributorインタフェースを実装したクラスに記述する。そして,プラグイン・マニフェストにおいて,contributorClass属性にIEditorActionBarContributorインタフェースの実装クラスのクラス名を記述して指定する。

プラグイン・マニフェストへの新規エディタの定義については,さほど難しいものではない。しかし,自作エディタを作成するための各種クラスの作成については,ちょっと敷居の高いものとなる。IEditorPartインタフェースやIEditorActionBarContributorインタフェースの実装クラスの作成方法は後日紹介する予定としよう。

| | コメント (0) | トラックバック (0)

2004.05.04

TableViewerのイベント処理

Taskビューでは,ビュー内の項目を選択すると,該当するエディタの場所にジャンプする。また,Error Logビューでは,項目をダブルクリックすることで,そのエラーの詳細がダイアログで表示される。これらのビューは,TableViewerを使用してつくられているので,これらの動作はTableViewerのイベント処理として実装される。ここでは,TableViewerについて代表的なイベントの処理方法を紹介する。

まずは表の操作で代表的な選択のイベントから取り上げよう。マウスによるクリック,あるいはキーボードのカーソルキーによる選択位置の変更を行った際には,選択変更イベントが発生する。選択変更イベントをハンドリングするには,ISelectionChangedListenerインタフェースの実装クラスを作成し,そのオブジェクトをTableViewerオブジェクトのaddSelectionChangedListenerメソッドを使って登録する

  TableViewer viewer = ...;
  viewer.addSelectionChangedListener(
      new ISelectionChangedListener() {
    public void selectionChanged(
        SelectionChangedEvent event) {
      // イベント処理
      System.out.println("addSelectionChangedListener");
    }
  });

addSelectionChangedListenerメソッドでISelectionChangedListenerオブジェクトを登録した場合,選択位置が変更される度にselectionChangedメソッドが呼び出される。つまり,キーボードのカーソルキーを押しっぱなしにしてオートリピートさせたときは,一行一行イベントが発生してしまう。もちろん悪いことではなく,基本的にはこれを使用して問題ない。

しかし,イベント処理がある程度重い処理だった場合,いちいち各行でイベントが発生してしまっては問題がある場面もある。オートリピートが終了したとき,つまりユーザの操作が完了したときに最終的に選択されている行に対してイベント処理を行いたい場合は,addPostSelectionChangedListenerメソッドを使用することにより実現できる。

  TableViewer viewer = ...;
  viewer.addPostSelectionChangedListener(
      new ISelectionChangedListener() {
    public void selectionChanged(
        SelectionChangedEvent event) {
      // イベント処理
      System.out.println("addPostSelectionChangedListener");
    }
  });

addPostSelectionChangedListenerメソッドを使ってリスナーを登録した場合は,連続して発生する選択イベントの最後に発生したもののみがリスナーに渡される。addSelectionChangedListenerメソッドでのリスナー登録と併用することもできる。上記の実行結果はこんな感じになる。

  addSelectionChangedListener
  addSelectionChangedListener
  addSelectionChangedListener
  addPostSelectionChangedListener ← ちょっと遅れて表示

最後に,項目のダブルクリックしたときのイベントのハンドリングは,addDoubleClickListenerメソッドにIDoubleClickListenerオブジェクトを登録することにより実現できる。

  TableViewer viewer = ...;
  viewer.addDoubleClickListener(new IDoubleClickListener() {
    public void doubleClick(DoubleClickEvent event) {
      // イベント処理
    }
  });

IDoubleClickListener実装クラスのdoubleClickメソッド内に,イベント処理を記述する。ちなみに,キーボードの[Enter]キーを押したときにも,このダブルクリックイベントが発生する

注意すべき点として,特に選択イベントに関しては,あくまでユーザの操作によってイベントが発火する,ということがある。TableViewerに渡しているSWTのTableオブジェクトに対して直接selectメソッドで選択させても,選択イベントは発火しない。Swingとは大きく異なる動作なので,把握しておく必要ありである。

| | コメント (0) | トラックバック (0)

2004.05.02

「Eclipseプラグイン開発」記事一覧

分野別に記事の題名のリストを作ってみました。ブログのようでブログでないこのブログ(?)を参考に,Eclipseプラグイン開発に興味を持ってもらえると嬉しい限りです。

  【公開プラグイン
    翻訳ビュープラグイン (2004.11.03)
    翻訳ビュープラグインをバージョンアップしました (2005.03.20)
    IPMessengerプラグイン作ってます!(2005.09.05)
    IPMessengerプラグイン公開開始しました(2005.09.18)
    早速バグ発見(IPMessengerプラグイン:コア)(2005.09.19)
    sourceforge.jpはじめました(ipmsg4e)(2005.10.01)

  【リッチクライアント
    最初のリッチクライアント(1) - プラグイン・マニフェストの作成 (2004.07.01)
    最初のリッチクライアント(2) - クラスの作成 (2004.07.01)
    最初のリッチクライアント(3) - デバッガで実行 (2004.07.02)
    最初のリッチクライアント(4) - 単独で実行 (2004.07.03)
    ワークベンチ・ウィンドウの表示・消去時の処理 (2004.07.03)

  【TableViewer
    TableViewerを使った表コンポーネントの利用 (2004.04.13)
    TableViewerのヘッダ列の作成 (2004.04.13)
    TableViewerのデータ供給・表示の仕組み (2004.04.17)
    TableViewer向けContentProviderの作成 (2004.04.18)
    TableViewer向けLabelProviderの作成 (2004.04.19)
    TableViewer向けLabelProviderの真実 (2004.04.21)
    TableViewerのホントの利用方法 (2004.05.01)
    TableViewerの更新はスレッドに注意せよ (2004.05.02)
    TableViewerのイベント処理 (2004.05.04)

  【ダイアログ
    ダイアログの自作 (2004.07.13)
    ダイアログボタンの変更 (2004.07.13)

  【ステータス・例外処理・ロギング
    CoreExceptionとIStatus (2004.03.25)
    エラーダイアログの表示 (2004.03.26)
    エラーログの出力 (2004.03.27)
    マルチ・ステータス (2004.04.09)

  【進捗状況表示
    進捗状況のダイアログ表示 (2004.04.08)
    ビルド処理の進捗状況表示 (2004.04.08)

  【リソース
    ファイルの作成 (2004.06.05)
    イメージの扱い方 (2004.06.10)
    イメージレジストリ (2004.06.11)

  【ビルダー・ネーチャー
    プロジェクト・ネーチャーの定義方法 (2004.03.15)
    プロジェクト・ネーチャーの適用方法 (2004.03.16)
    プロジェクト・ネーチャーの解除方法 (2004.03.17)
    新規ビルダーの定義 (2004.03.21)
    ビルダーの登録方法 (2004.03.21)
    ビルダーの解除方法 (2004.03.21)
    ビルダー登録の担当者は? (2004.03.21)
    ビルドの種類 (2004.04.02)
    ビルダーの起動 (2004.04.06)

  【ワークベンチ
    Shellオブジェクトの取得 (2004.01.12)
    MessageDialogで簡単ダイアログ (2004.01.12)
    ワークベンチウィンドウ→ページ→ビュー (2004.02.04)
    Shellオブジェクトの取得 Part2 (2004.04.01)

  【エディタ
    すべてのエディタの内容を保存する (2004.04.01)
    自作エディタの定義 (2004.05.23)
    エディタを開く (2004.05.24)
    エディタを閉じる (2004.05.25)
    あるファイルがエディタで開かれているかを取得する方法 (2004.07.30)
    エディタを開く Part2 (2004.09.03)
    アクティブなエディタの取得方法 (2004.10.04)

  【テキストエディタ
    ~LineRuleって怪しいぞ (2004.01.07)
    「行の先頭から~」Rule (2004.01.07)
    NonRuleBasedDamagerRepairerって (2004.01.08)
    キーワードRuleの作成方法 (2004.01.08)
    行の先頭ルールとunreadについて (2004.01.11)
    IDocumentListenerインタフェース (2004.01.12)
    ドキュメント区画の変更通知を受け取るには (2004.01.12)
    IDocumentPartitionerに文書を接続するのを忘れずに・・・ (2004.01.12)
    ドキュメント区画の取得 (2004.01.12)
    RuleBasedPartitionScannerはIRuleじゃだめ! (2004.01.14)
    ドキュメントを区画分けするスキャナの作成 (2004.01.14)
    テキストエディタからのIEditorInput,IDocumentオブジェクトの取得 (2004.01.17)
    テキストエディタのワードラップをONにするには? (2004.01.18)
    テキストエディタ上のカーソル位置の取得方法 (2004.08.27)
    テキストエディタで指定位置へジャンプ (2004.09.08)
    テキストエディタのコンテキストメニューID (2004.09.12)

  【テキスト操作
    ドキュメントの操作を簡単にするテキスト編集クラス (2004.08.05)
    文字列の移動,コピーを行うテキスト編集クラス (2004.08.06)
    テキストのフォーマット(1.戦略の作成) (2004.09.18)
    テキストのフォーマット(2.戦略の登録) (2004.09.18)
    テキストのフォーマット(3.戦略の呼び出し) (2004.09.18)

  【テキストバッファ
    テキストバッファを使ったIDocumentオブジェクトの取得 (2004.07.30)
    テキストバッファとエディタ (2004.08.02)

  【JDT(Java Development Tool)
    型の発見方法 (2004.03.10)
    その型は誰の持ち物? (2004.03.10)
    型の仲間達(サブクラス,スーパークラス)の取得方法 (2004.03.11)
    Javaエディタのコンテキストメニュー (2004.07.17)
    エディタからICompilationUnitオブジェクトを取得する方法 (2004.08.24)

  【プロパティページ
    プロパティ・ページの作成方法 (2004.03.24)

  【マーカー
    リソースが持つマーカーの取得 (2004.02.19)
    リソースへのマーキング (2004.02.23)
    マーカーの削除 (2004.02.23)
    標準マーカー (2004.02.24)
    標準マーカーの属性 (2004.02.25)
    新規マーカーの作成 (2004.03.01)
    マーカーのアイコン (2004.03.02)
    マーカーレゾリューション (2004.03.04)
    MarkerUtilitiesクラス (2004.09.09)

  【アウトラインビュー
    空のアウトラインページの作成 (2004.01.15)
    アウトラインページの表示更新タイミング (2004.01.15)

  【ビュー
    新規ビューの作成方法 (2004.02.05)
    ページへのビューの表示・非表示 (2004.02.06)
    ビュー上のコンテキストメニュー(1) (2004.02.11)
    ビュー上のコンテキストメニュー(2) (2004.02.11)
    コンテキストメニューの公開 (2004.02.12)
    特定ビューへのコンテキストメニュー項目の追加 (2004.02.14)

  【ステータスバー
    ステータスバーへのアクセス (2005.11.22)
    ステータスバーへのコンポーネント登録と削除 (2005.11.24)

  【拡張ポイント
    拡張ポイントの自作(1) (2004.01.29)
    拡張ポイントの自作(2) (2004.01.29)
    なぜcreateExecutableExtensionを用いるべきか? (2004.01.29)
    拡張ポイント呼び出し時の例外対処 (2004.01.30)

  【アクション
    ツールバーへのアクションの追加 (2004.01.23)
    ツールバーへのトグルボタンの追加 (2004.01.23)
    コンテキストメニューへのアクションの追加 (2004.01.24)
    アクションの対象オブジェクト(Element)の取得方法 (2004.01.25)
    アクションの登録先と使用インタフェース (2004.02.15)
    アクション起動時の要素選択でのIAdaptableの利用 (2004.05.26)

  【プラットフォーム
    IAdaptableとは?(前編) (2004.01.09)
    IAdaptableとは?(後編) (2004.01.09)

  【SWT
    かっこいいタブの作り方(CTabFolder) (2004.10.21)
    UIスレッドでのタイマー実行 (2005.11.26)

  【プラグイン
    Pluginクラスの作成 (2004.06.09)
    Eclipse起動時のプラグイン活性化 (2004.04.09)

  【その他
    サンプルプラグインのインストール方法 (2004.01.14)
    Plugin Developer Guideだけの日本語化 (2004.01.15)
    Eclipseを覗き込むSpiderプラグイン (2004.01.23)
    PDE JUnit Pluginのインストール (2004.01.31)
    APIリファレンスの抜き出し方法 (2004.03.05)
    デバッグ用コード切り替えスイッチ (2004.03.29)
    Contributing to Eclipse本のサンプルコード (2005.10.14)

ちなみに,分類は僕の独断と偏見ですので,お気になさらずに・・・。

| | コメント (0) | トラックバック (2)

TableViewerの更新はスレッドに注意せよ

現在のEclipseの最新リリースバージョンは2.1.3であり,僕もそのバージョンを使っている。今までの記事はすべて2.1.3を対象にしたものである。しかし,もしかしたら「TableViewerのホントの利用方法」で取り上げた例について,古いバージョンのEclipseを使用していた場合に「動かないじゃん!」ってなった人がいるかもしれない。実は,「TableViewerのホントの利用方法」でのコーディング例の中で,非常に危険な部分が存在している。ContentProvider内でTableViewerにドメインモデルを反映している部分(employeeAddedメソッド内のTableViewerに対するaddメソッド呼び出し)で,例外が発生してしまう可能性があるのだ。

では,実際にわざと危険な状況を作ってみる。「TableViewerのホントの利用方法」の中で取り上げたDivisionContentProviderクラスのemployeeAddedメソッドを,以下のように書き換える。

  public void employeeAdded(final Employee employee) {
    if (viewer != null) {
      Thread thread = new Thread(new Runnable() {
        public void run() {
          viewer.add(employee);
        }
      });
      thread.start();
    }
  }

Threadクラスを使用して,TableViewerへのEmployeeオブジェクトの追加処理を,employeeAddedメソッドを実行しているスレッドとは別のスレッドで実行している。この変更により,setInputメソッドでTableViewerにセットされているDivisionオブジェクトのaddEmployeeメソッドを使ってEmployeeオブジェクトを追加した場合,以下のような例外が発生してしまう。

  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.Table.getItemCount(Table.java:817)
   at org.eclipse.jface.viewers.TableViewer.indexForElement(TableViewer.java:330)
   at org.eclipse.jface.viewers.TableViewer.add(TableViewer.java:114)
   at org.eclipse.jface.viewers.TableViewer.add(TableViewer.java:133)
   at yoichiro.myPlugin.DivisionContentProvider$1.run(DivisionContentProvider.java:62)
   at java.lang.Thread.run(Thread.java:534)

上記の例では,明示的に新しいスレッドを起動しているが,実際にはDivisionオブジェクトがどのスレッドにより操作されるかはプラグインの作り方次第である。TableViewerへの操作はすなわちSWTのTableコンポーネントへの操作になるのだが,当然マルチスレッド化での同一リソース(この場合はTableコンポーネント)に対する複数スレッドからの操作について,何らかの安全策がなければならない

実はSWTにおいて,
  ・GUIのイベントディスパッチを行っているスレッド(Eclipse2.1.3ではメインスレッド)からしか操作ができない
というお約束事がある。この制限により,SWTコンポーネントに対する複数の処理の順序が保たれている。上記の例では,独自に生成したスレッド内からSWTコンポーネントに対して操作が行われたために,SWTコンポーネント内部のチェックに引っかかって例外が発生した,というわけなのである。

ではどうしたらいいかというと,SwingでもinvokeLaterメソッドが存在するように,SWTにおけるGUIのイベントディスパッチスレッドに対して,SWTコンポーネントへの処理の実行をお願いすることになる。GUIのイベントディスパッチスレッドは,SWTではDisplayオブジェクトがそれに該当するので,操作したいコンポーネントからDisplayオブジェクトを取り出して,それに対して処理の実行を依頼する。

  public void employeeAdded(final Employee employee) {
    if (viewer != null) {
      Control control = viewer.getControl();
      if ((control != null) && !(control.isDispose()) {
        control.getDisplay().syncExec(new Runnable() {
          public void run() {
            viewer.add(employee);
          }
        });
      }
    }
  }

まず,TableViewerオブジェクトからSWTコンポーネント(この場合Tableコンポーネント)のオブジェクトをgetControlメソッドを使って取得する。そして,その結果がnullではなく,しかもSWTコンポーネントが破棄されていないことを確認する(Eclipseの終了時やその他突発的なSWTコンポーネントの破棄に対処するため)。

そしてSWTコンポーネントからgetDisplayメソッドを使ってDisplayオブジェクトを取得し,それが持つsyncExecメソッドに(Javaのスレッド関連でおなじみの)Runnableインタフェースのオブジェクトを渡して,GUIのイベントディスパッチスレッドに処理を依頼している。上記の例では,TableViewerオブジェクトに対してEmployeeオブジェクトをaddメソッドにより追加する処理をrunメソッド内に実装している。これで,先のSWTコンポーネントのルールに則った操作が行われるようになった。

syncExecメソッドが呼び出されると,Displayオブジェクトは適当なタイミングで渡された処理を実行する(すぐに実行されるとは限らない)。syncExecメソッドの場合,渡した処理が完了するまでメソッドは復帰しない。それとは対照的に,処理の実行を依頼するだけで,渡した処理の実行完了を待たなくてもいい場合は,asyncExecメソッドを使用することもできる。

syncExecメソッドとasyncExecメソッド,どっちを使うかは開発者の自由。というか,行わせたい処理の内容に従って決定されるべきだろう。ちなみに,syncExecメソッドはプログラム次第でデッドロックの危険性が存在するので注意が必要である。

| | コメント (0) | トラックバック (0)

2004.05.01

TableViewerのホントの利用方法

TableViewerの使い方について,数回に分けて紹介してきた。

  ・TableViewerを使った表コンポーネントの利用
  ・TableViewerのヘッダ列の作成
  ・TableViewerのデータ供給・表示の仕組み
  ・TableViewer向けContentProviderの作成
  ・TableViewer向けLabelProviderの作成
  ・TableViewer向けLabelProviderの真実

実はこれだけでは十分ではなく,「各クラスの役割とかはわかったけど,それで実際にはどう使えばいいの?」というのが正直なところだろう。「ドメインモデルの情報を如何にしてTableViewerに表示させるか」といった話をもう少し付け加えないと,TableViewerの使い方を本当に理解したとは言えない。ドメインモデルの情報をどのようにTableViewerに伝えるか,つまりポイントは「ドメインモデルの情報の変更を如何にTableViewerに反映するか」である。

最初にTableViewerクラスのJavadocを覗いてみる。TableViewerには,行を追加するためのaddメソッドや,行を任意のインデックスに挿入するためのinsertメソッド,行を削除するためのremoveメソッドが備わっていることがわかる。これらのメソッドのJavadocで書かれている記載の中で注目すべき点は2つ。

  「引数で渡したオブジェクトはTableViewerにしか反映されない
  「各メソッドはContentProviderから呼び出されるべきである

まず前者についてだが,TableViewerオブジェクトのaddメソッドに行のオブジェクトを渡すと,とりあえずTableViewerに行が追加されて表示は行われる。しかし,これはあくまで表示だけの話であり,その後refreshメソッドを呼び出してしまうとContentProviderからドメインモデルが再読み込みされるので,追加された行は消えてしまう。

そして後者は,ドメインモデルをTableViewerに対応させる橋渡し的な役割を持つContentProviderからのみしか,TableViewerに対してaddメソッドなどの各種メソッドを呼び出してはいけないということを表している。言い換えると,TableViewerへの表示データの供給は,ContentProviderからのみしか行ってはならないということである。

つまり,表示情報の追加や削除,挿入が「TableViewer → ドメインモデル」という流れではないことが判明する。ではどうするのが正解かというと,「ドメインモデル → ContentProvider → TableViewer」というようにデータが流れるようにしなければならない。ドメインモデルに対して行った追加や削除,挿入がContentProviderに通知され,通知を受け取ったContentProviderはそれをTableViewerに反映させる,という流れにするのである。

例として,ある部署に所属している社員のリストを表示したいとしよう。まず社員のクラスについては「TableViewerのデータ供給・表示の仕組み」で取り上げたEmployeeクラスをそのまま使用する。そして,部署を表すクラスとして,Divisionクラスを新たに作成する。Divisionクラスは,所属する社員のEmployeeオブジェクトを複数内部のコレクションに保持できるようにする。このDivisionクラスがドメインモデルとなり,DivisionオブジェクトがTableViewerオブジェクトにsetInputメソッドでセットされる。さらに,社員が追加された際にその通知を受けることができるように,DivisionListenerインタフェースを準備する。

  public interface DivisionListener {
    public void employeeAdded(Employee employee);
  }

  public class Division {
    private List employeeList = new ArrayList();
    private ListenerList listenerList = new ListenerList();
    public List getEmployeeList() {
      return employeeList;
    }
    public void addEmployee(Employee employee) {
      employeeList.add(employee);
      Object[] listeners = listenerList.getListeners();
      for (int i = 0; i < listeners.length; i++) {
        DivisionListener listener = (DivisionListener)listeners[i];
        listener.employeeAdded(employee);
      }

    }
    public void addDivisionListener(DivisionListener listener) {
      listenerList.add(listener);
    }
    public void removeDivisionListener(DivisionListener listener) {
      listenerList.remove(listener);
    }
  }

DivisionListenerインタフェースでは,社員が追加されたときに呼び出されるemployeeAddedメソッドを定義し,引数として追加された社員を表すEmployeeオブジェクトが渡されるようにしておく。

Divisionクラスでは,社員を表すEmployeeオブジェクトを格納しておくためのemployeeListコレクションを準備し,ContentProviderが社員の一覧を取得できるようにgetEmployeeListメソッドでemployeeListコレクションを返すようにしている。DivisionListenerオブジェクトの格納には,Eclipseプラットフォームがリスナーの管理用に提供してくれているListenerListクラスを使用する。ここでは,addDivisionListenerメソッドおよびremoveDivisionListenerメソッドを準備して,リスナーの登録と削除ができるようにしている。

そしてメインの社員を追加するためのaddEmployeeメソッドの登場である。ここでは,まず引数で受け取ったEmployeeオブジェクトをemployeeListコレクションに追加している。社員のコレクションへの追加後,listenerListオブジェクトから登録されているリスナーオブジェクトを順次取り出し,それぞれのemployeeAddedメソッドを呼び出すことで観測者に社員が追加されたことを通知する。これにより,ドメインモデルの内容の変化を外部に通知できるようになった。

そして,DivisionオブジェクトをドメインモデルとするDivisionContentProviderクラスの作成だが,ドメインモデルの内容変化通知を受け取ってTableViewerにそれを反映するために,DivisionListenerインタフェースを実装する

  public class DivisionContentProvider
      implements IStructuredContentProvider, DivisionListener {
    private TableViewer viewer;
    public Object[] getElements(Object inputElement) {
      return ((Division)inputElement).getEmployeeList().toArray();
    }
    public void dispose() {
    }
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
      this.viewer = (TableViewer)viewer;
      if (oldInput != null)
        ((Division)oldInput).removeDivisionListener(this);
      if (newInput != null)
        ((Division)newInput).addDivisionListener(this);
    }
    public void employeeAdded(Employee employee) {
      if (viewer != null)
        viewer.add(employee);
    }
  }

まず,ドメインモデルから行のオブジェクトを返却するgetElementsメソッドは,引数で渡されるDivisionオブジェクトのgetEmployeeListメソッドを呼び出してEmployeeオブジェクトが格納されているコレクションを取得し,そのtoArrayメソッドを呼び出して配列に変換し,その結果を返却している。ここは「TableViewer向けContentProviderの作成」のときとほぼ同じなので問題ないだろう。disposeメソッドに記載する処理は,今回も特になし。

そして「TableViewer向けContentProviderの作成」のときは何も処理を記述しなかったinputChangedメソッドが,ここでは非常に重要である。まず,引数で受け取ったTableViewerオブジェクトをフィールドに保持するようにしている。フィールドに保持されたTableViewerオブジェクトは,ドメインモデルに変更があった際にその反映先となる。

ちょっと説明の順番が前後するが,第3引数で渡されてくるnewInputオブジェクトをDivision型にキャストし,addDivisionListenerメソッドに自分自身を渡してドメインモデルの内容変化の観測者として登録している。これにより,このDivisionContentProviderオブジェクトはsetInputメソッドでセットされたドメインモデルの変更通知を受け取ることができるようになる。これとは反対に,setInputメソッドで新たなドメインモデルがセットされるまで使用されていた第2引数で渡されるoldInputオブジェクト,すなわち古いドメインモデルに対しては,removeDivisionListenerメソッドを呼び出して自分自身を観測者から解除する。

あとはDivisionListenerインタフェースに規定されているemployeeAddedメソッドを実装する。引数で渡されてくる追加された社員のEmployeeオブジェクトを,予めフィールドで保持しておいたTableViewerオブジェクトのaddメソッドに渡すことによって,TableViewer上に反映している。

以上のコーディングにより,Divisionオブジェクト,すなわちドメインモデルに対する社員の追加という行為が,シームレスにTableViewerに反映されるようになった。上記は社員の追加だけだが,社員の削除などに関しても同様の実装方法となる。使い方的には,TableViewerを使用するビューのクラス内で,

  List employeeList = ...; // ドメインオブジェクト作成
  TableViewer viewer = ...; // TableViewerオブジェクト作成
  // ヘッダ列の作成
  viewer.setContentProvider(
    new DivisionContentProvider());
  viewer.setLabelProvider(new EmployeeLabelProvider());

  Division division = ...;
  viewer.setInput(division);

としておいて,任意の場所で,

  Division division = ...;
  Employee employee = new Employee("よういちろう", 29);
  division.addEmployee(employee);

とすることにより,自動的にTableViewerに追加される,というわけである。まとめると,

  ・ContentProviderはドメインモデルの内容変化の観測者となり,内容変化をTableViewerに反映する。
  ・そのためには,ドメインモデルがその内容変化を外部に通知する機構を持つ必要がある。

ということである。

・・・うーん,一番長い記事になっちゃったかも。

| | コメント (0) | トラックバック (0)

« 2004年4月 | トップページ | 2004年6月 »