« 2004年3月 | トップページ | 2004年5月 »

2004.04.21

TableViewer向けLabelProviderの真実

前回「TableViewer向けLabelProviderの作成」において,TableViewerが各列に何を表示すればよいかを判断するためのLabelProviderの作り方を解説した。その中で通常はITableLabelProviderインタフェースを実装すれば良い」という何とも意味深な表現をした。今回は,なぜ「通常は」なのか?そしてその問題の奥に潜むTableViewerとLabelProviderの真実について述べてみたい。

TableViewer向けContentProviderの作成」において,LabelProviderをセットしていないにも関わらずオブジェクトのハッシュ値(toStringメソッドの結果)がTableViewerに表示されていた。ContentProviderをセットしないでTableViewerを使った場合,setInputメソッドの呼び出しの時点で「ContentProviderがないぞ」とAssertionFailedException例外が発生する。しかし,LabelProviderはセットしなくても一応TableViewerは動作する。

なぜLabelProviderをセットしなくても動作するかというと,実はTableViewerはデフォルトでLabelProviderを内部で生成して持っているからなのである。これは,

  Table table = ...;
  TableViewer viewer = new TableViewer(table);
  System.out.println(viewer.getLabelProvider().getClass().getName());

というコードを実行すると,getLabelProviderメソッドの戻り値がnullではないことからわかる事実である。つまり「TableViewer向けContentProviderの作成」では,TableViewerがデフォルトで持っているLabelProviderが使われることによってエラーにならずに表示ができていた,ということなのである。

さて,上記のコードを実行すると,標準出力に以下のように出力される。

  org.eclipse.jface.viewers.LabelProvider

このLabelProviderクラスは「TableViewer向けLabelProviderの作成」でITableLabelProviderインタフェースの実装クラスを自作した際に継承したクラスである。そのクラスがTableViewerのデフォルトLabelProviderとして使われているというのはちょっとした驚きがある。今までの知識では,いくつかの謎がでてくる。まず,LabelProviderクラスはITableLabelProviderインタフェースを実装していない。にも関わらず,TableViewerのLabelProviderとして使用できている。「話が違うじゃないか!」と思うかもしれないが,「TableViewer向けLabelProviderの作成」で使った「通常は」という表現がここで活きてくる。

実は,TableViewerで使用できるLabelProviderは,ITableLabelProviderインタフェースの実装クラスのほかに,ILabelProviderインタフェースの実装クラスも使用することができるLabelProviderクラスは,ILabelProviderインタフェースを実装しているので,TableViewerのLabelProviderとして使用することができたというわけだ。ただし,ITableLabelProviderインタフェースとILabelProviderインタフェースでは,規定されているメソッドも全く違っていて,当然TableViewerの動きが大きく違ってくる。

ITableLabelProviderインタフェースの実装クラスでは,getColumnTextメソッドやgetColumnImageメソッドを実装して,引数で渡ってくる列のインデックスに対応した表示文字列や表示イメージを返却する処理を記述した。それに対して,ILabelProviderインタフェースの場合は,

  String getText(Object element) - 指定された行オブジェクトを元に表示文字列を返す
  Image getImage(Object element) - 指定された行オブジェクトを元に表示イメージを返す

という2つのメソッドを実装する必要がある。引数に列のインデックスがないことからもわかることだが,ILabelProviderインタフェースの実装オブジェクトがsetLabelProviderメソッドによりセットされた場合,TableViewerはgetTextメソッドおよびgetImageメソッドの戻り値の文字列およびイメージを0列目(一番左の列)に表示する,という動きになる。

そして,LabelProviderクラスではgetTextメソッドは引数のelementオブジェクトのtoStringメソッドの戻り値を取得して返却する,getImageメソッドは必ずnullを返却する,という実装になっているので,TableViewerにLabelProviderを何もセットしなかったときは,「TableViewer向けContentProviderの作成」のときのように,ContentProviderが生成した行オブジェクトのtoStringメソッド呼び出しの結果が0列目に表示される,という動きになるのである。

上記の説明だと,もう1つ疑問なことが出てくる。「TableViewer向けLabelProviderの作成」で作ったEmployeeLabelProviderクラスは,LabelProviderクラスを継承し,ITableLabelProviderインタフェースを実装していた。つまり,ILabelProviderインタフェースとITableLabelProviderインタフェースを両方とも実装している,ということになる。これでは「どっちが使われるんじゃ!?」ということになってしまう。

実は,TableViewerではITableLabelProviderインタフェースが実装されているかどうかを先にチェックしている。つまり,両インタフェースを実装していた場合は,ITableLabelProviderインタフェースが優先される。すなわち,getColumnTextメソッドおよびgetColumnImageメソッドが呼び出され,getTextメソッドおよびgetImageメソッドは呼び出されない。つまりEmployeeLabelProviderクラスの例では,あくまでgetColumnTextメソッドおよびgetColumnImageメソッドしかTableViewerから呼び出されないのである。ではなぜLabelProviderクラスを継承したのか?LabelProviderクラスの継承の目的は,単にILabelProviderListener関連のメソッドの実装をパクりたかっただけ,なのである。

ただし,ほとんどの場合はTableViewerではILabelProviderインタフェースの実装では役に立たないと思われるので「通常はITableLabelProviderインタフェースを実装する」と表現したのである。TableViewerを使うということはマルチカラムにしたいのだから,ITableLabelProviderインタフェースを使うのが普通でしょ?って話である。

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

2004.04.19

TableViewer向けLabelProviderの作成

TableViewer向けContentProviderの作成」で解説したContentProviderを作った結果,とりあえず行ごとのデータを持つオブジェクトの配列をTableViewerに提供することができるようになった。今回は,その行ごとのデータを各列に分解し,表示したい情報が正しく表として表示されるようにするための機構,LabelProviderを紹介する。これは「TableViewerのデータ供給・表示の仕組み」で示した3つの機構の最後である。

TableViewerの表のヘッダ列に関して,「TableViewerのヘッダ列の作成」で作り方を示した。ContentProviderで生成された行のオブジェクトを元に,各列にどのような文字列あるいはイメージを表示するかを決める機構がLabelProviderである。TableViewerでは,LabelProviderは通常ITableLabelProviderインタフェースの実装クラスとして作成する(「通常」というところがミソなのだが,これは後日紹介する)。

  public class EmployeeLabelProvider
        extends LabelProvider implements ITableLabelProvider {
    public String getColumnText(Object element, int columnIndex) {
      Employee employee = (Employee)element;
      switch(columnIndex) {
        case 0:
          return employee.getName();
        case 1:
          return String.valueOf(employee.getAge());
      }
      return null;
    }
    public Image getColumnImage(Object element, int columnIndex) {
      return null;
    }
  }

上記のコードは,「TableViewerのデータ供給・表示の仕組み」で取り上げた社員一覧の表示に対するLabelProviderの実装例である。ITableLabelProviderインタフェースを実装し,さらにLabelProviderクラスを継承している。そして,メソッドとしてgetColumnTextメソッドとgetColumnImageメソッドを実装している。

ITableLabelProviderインタフェースは,各列のデータを返すgetColumn~メソッドのほかに,継承しているIBaseLabelProviderインタフェースで規定されたいくつかのメソッド(ILabelProviderListener登録関連のメソッドや後処理を担当するdisposeメソッドなど)を持っている。これらのメソッドを個別に実装しても良いのだが,実はそれらに関しては既にEclipseプラットフォームが提供しているLabelProviderクラスが実装してくれているので,今回はそれを拝借するためにLabelProviderクラスを継承している。通常はLabelProviderクラスを継承してしまってよいだろう。

さて,今回の本質のメソッドだが,getColumnTextメソッドで各列に表示する文字列を,getColumnImageメソッドで各列に表示するイメージをそれぞれ返却する。この際,第1引数にContentProviderで生成したオブジェクトの配列の各要素が,第2引数に列のインデックスが渡されてくる。一般的には,第1引数のオブジェクトをダウンキャストして目的の型にし,第2引数の列のインデックスに対してオブジェクトから適切にデータを取り出して表示したいデータとして返却する。上記のコードでは,まずEmployeeクラスでキャストし,0列目であればgetNameメソッドを呼び出して名前を返却し,1列目であればgetAgeメソッドを呼び出して年齢を取得し,それを文字列として返却している。

getColumnImageメソッドに関しても,基本的にやることはgetColumnTextメソッドと同じ。違う点は,文字列ではなくてイメージを返すことである。ただ,イメージの場合はいろいろと考えなければならないことがあるので,後日紹介することとして,今回はnullを返却することにする(「nullを返却する」=「イメージを表示しない」)。

LabelProviderができたところで,早速TableViewerにセットし,以下のようにして実行してみる。

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

「TableViewer Test View」を表示されたときのスナップショットを以下に示す。ちゃんと社員一覧が表示された。

tableviewer-test-view-labelprovider.gif

ちなみに,getColumn~メソッドは,「TableViewerのヘッダ列の作成」で作成した列数分呼び出される。つまり,引数のcolumnIndexは,0~(作成した列数-1)である。

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

2004.04.18

TableViewer向けContentProviderの作成

TableViewerを使うにあたって必要となる3つの機構を「TableViewerのデータ供給・表示の仕組み」で解説した。今回はその中の1つ,ContentProviderを取り上げる。ContentProviderを準備すれば,とりあえずTableViewerが動くようになる。

TableViewerでは,ContentProviderはIStructuredContentProviderインタフェースの実装クラスとして作成する。ここでは,「TableViewerのデータ供給・表示の仕組み」で例として紹介したEmployeeオブジェクトのListコレクションをドメインオブジェクトとした際にContentProviderをどのように作成するかを示す。

  public class EmployeeContentProvider
      implements IStructuredContentProvider {
    public Object[] getElements(Object inputElement) {
      return ((List)inputElement).toArray();
    }
    public void dispose() {
    }
    public void inputChanged(Viewer viewer,
        Object oldInput, Object newInput) {
    }
  }

TableViewerクラスのsetInputメソッドで渡されたドメインオブジェクトは,IStructureContentProviderインタフェースの実装クラスで規定されたgetElementsメソッドに渡される。getElementsメソッドでは,引数で渡されたドメインオブジェクトを元に,1行分の情報を持つオブジェクトの配列を生成して返却する。上記では,Employeeオブジェクトが格納されたListオブジェクトが引数に渡されてくるという前提のもとに,引数をList型にキャストし,そのtoArrayメソッドを呼び出してコレクション内のオブジェクトを配列に変換し,それを返却している。つまり,Employeeオブジェクトの配列を返却している。

disposeメソッドは,TableViewerが破棄された時に呼び出されるメソッドで,ContentProvider内で何らかの後始末的処理を記述する場所である。上記の例では後始末は必要ないので,何も処理を記述していない。

inputChangedメソッドは,TableViewerオブジェクトのsetInputメソッドでドメインオブジェクトが渡されたときに呼び出されるメソッドである。既にsetInputメソッドでドメインオブジェクトがセットされていた場合,再度setInputメソッドで新しいドメインオブジェクトがセットされると,inputChangedメソッドの呼び出し時に引数としてoldInputに既にセットされていたドメインオブジェクト,newInputに新しいドメインオブジェクトが渡されてくる。第1引数のTableViewerオブジェクトをContentProviderインスタンス内に保持しておく使用方法なども考えられるが,getElementsメソッドさえしっかり実装しておけば通常は問題ない。

上記のようにIStructuredContentProviderインタフェースの実装クラスができあがったところで,早速それをTableViewerオブジェクトにセットする。手順は以下のような感じ。setContentProviderメソッドの引数の型はIContentProviderインタフェースだが,TableViewerクラス(の親クラスのStructuredViewerクラス)ではIStructuredViewerオブジェクトかどうかがチェックされている。

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

ここまでのコードで実行したときのスナップショットを以下に示しておく。

tableviewer-test-view-contentprovider.gif

ご期待通り(?),名前と年齢は表示されず,Employeeオブジェクトのハッシュコードが最初(名前)の列に表示されてしまった。正しく表示するためには,もう一つの機構,LabelProviderが必要である。

ちなみに,実は上記の例では,IStructuredContentProviderインタフェースの実装クラスを自作する必要はない。ドメインオブジェクトが「オブジェクトの配列」または「コレクション(java.util.Collectionオブジェクト)」の場合,Eclipseプラットフォームが提供してくれているArrayContentProviderクラスを利用することができる。ArrayContentProviderクラスでは,ドメインオブジェクト,すなわちgetElementsメソッドの引数としてオブジェクトの配列が渡された場合はそれをそのまま返却し,コレクションが渡された場合はtoArrayメソッドを使ってオブジェクトの配列に変換してそれを返却してくれる。ArrayContentProviderクラスで事足りる場合は,積極的に使ってよい。

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

2004.04.17

TableViewerのデータ供給・表示の仕組み

TableViewerを使った表コンポーネントの利用」により,表コンポーネントをビュー上に出せるようになった。また,「TableViewerのヘッダ列の作成」により,表コンポーネントにヘッダ列(タイトル行)を作れるようになった。しかし,肝心のデータを表に表示することができていない。ここでは,TableViewerでのデータの供給や表示の仕組みについて紹介する。

まず,TableViewerに表示させたいデータを持つオブジェクトのことをドメインオブジェクトと呼ぶ。アプリケーション独自にドメインオブジェクトは設計されるが,基本的にドメインオブジェクトのクラスに対してTableViewer向けに手を加える必要はない。例えば,社員1人を表す以下のようなクラスがあって,何人分かの社員オブジェクトを持つコレクションがあったとする。

  public class Employee {
    private String name;
    private int age;
    public Employee(String name, int age) {
      this.name = name;
      this.age = age;
    }
    public String getName() {
      return name;
    }
    public int getAge() {
      return age;
    }
  }

TableViewerには,そのドメインオブジェクトを何とそのまま突っ込んでOKである。TableViewerにドメインオブジェクトを渡すためにsetInputメソッドを使用するのだが,その引数の型はObjectである。最初社員0人でいいのであれば,viewer.setInput(new ArrayList()) でもOK。ここではListコレクションを渡しているが,Listでなければならないということはなく,自作のクラスだってかまわない。

  List employeeList = new ArrayList();
  employeeList.add(new Employee("よういちろう", 29));
  employeeList.add(new Employee("ようこ", 25));

  TableViewer viewer = ...;
  viewer.setInput(employeeList);

さて,TableViewerが受け取ったドメインオブジェクトをどのように扱うかだが,もちろん独自のクラスのオブジェクトを渡されたって,TableViewerは困ってしまう。そこでTableViewerは,ドメインオブジェクトをTableViewerが認識できる形(具体的にはObjectの配列)に変換しようとする。そして,ドメインオブジェクトをTableViewerが認識できる形に変換する機構がContentProviderと呼ばれるもので,TableViewerの場合はIStructuredContentProviderインタフェースにその機構が規定されている。IStructuredContentProviderオブジェクトをsetContentProviderメソッドに渡してTableViewerにドメインオブジェクト変換機構をセットすれば,setInputメソッドで渡したドメインオブジェクトがTableViewerに認識される。

  IStructuredContentProvider contentProvider = ...;
  viewer.setContentProvider(contentProvider);

IStructuredContentProviderオブジェクトによって変換されたドメインオブジェクトは,Objectの配列,つまり行の情報を持つ任意の型のオブジェクトの配列としてTableViewerにより扱われるようになる。

さて,IStructuredContentProviderオブジェクトによって変換されたとしても,相変わらず行のデータはObject型という汎用的な型である。TableViewerは行のデータを持つオブジェクトの使い方なんて知る由もない。どのメソッドを呼んでデータを取得すればいいのか?取得したデータは何列目に表示すればいいのか?といったことがわからなければ,TableViewerはデータを表示できない。

TableViewerは,行のオブジェクトを元に何列目にどのような文字列を表示するかを決定しようとする行の情報から各列の情報を取り出す機構がLabelProviderと呼ばれるもので,TableViewerの場合は通常ITableLabelProviderインタフェースに規定された機構が使用される。ITableLabelProviderオブジェクトを作成し,setLabelProviderメソッドにそれを渡すことで,TableViewerは各行の各列に何を表示すればいいのかを取得できるようになる。

  ITableLabelProvider labelProvider = ...;
  viewer.setLabelProvider(labelProvider);

以上のように,TableViewerは3つの部品を使ってデータを表示する。
  (1) 表示したいデータを持つドメインオブジェクト
  (2) ドメインオブジェクトを行単位の配列に変換するContentProvider
  (3) 行単位のオブジェクトから列毎のデータを提供するLabelProvider

複雑に思うかもしれないが,やってみると実はSwingのJTableを使うときよりも単純だったりする。

上記の説明だけでは,なんのこっちゃさっぱりかもしれない。ContentProviderについては「TableViewer向けContentProviderの作成」を参照されたし。LabelProviderの作り方については後日紹介する予定である。

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

2004.04.13

TableViewerのヘッダ列の作成

TableViewerを使った表コンポーネントの利用」の続きということで,表コンポーネントには欠かすことができないヘッダ列の作成を今回は紹介する。

ヘッダ列の作成は,TableViewerの話ではなく,SWTのTableコンポーネントの話になる。この辺もTableViewerがSWTのTableを隠蔽しないという点が現れている部分である。

ヘッダ列は,TableColumnクラスを使用して作成する。ちなみに,Tableオブジェクトの作り方は「TableViewerを使った表コンポーネントの利用」を参照されたし。

  Table table = ...;
  TableColumn column = new TableColumn(table, SWT.LEFT, 0);
  column.setText("名前");
  column.setWidth(200);
  column = new TableColumn(table, SWT.RIGHT, 1);
  column.setText("年齢");
  column.setWidth(50);

作成したいヘッダの列の個数分,TableColumnクラスのインスタンスを作成する。その際,TableColumnクラスのコンストラクタに,対象となるTableオブジェクトを渡すことで,TableColumnオブジェクトをTableオブジェクトに関連付ける。さらにコンストラクタの引数として,ヘッダ列のスタイル値(文字列の表示位置:LEFT,RIGHT,CENTERのどれか)と,列のインデックスを渡す。

文字列の表示位置だが,なぜか一番左の列(インデックスが0)に関しては,CENTERやRIGHTを指定しても,それが無視されてLEFT(つまり左寄せ)になってしまう。Tableコンポーネントの仕様だと思われるが,真相は謎。

TableColumnオブジェクトができたら,setTextメソッドでヘッダ列に表示する文字列をセットし,さらにsetWidthメソッドで列の幅をセットする。もしヘッダ列の表示文字列で自動的に幅を設定したいときは,packメソッドを使うことで勝手に幅を設定してくれる。

ここでもう1つ謎。setWidthメソッドまたはpackメソッドを呼び出してあげないと,表に列が表示されなかった。幅の初期値が0で,幅を与えてあげないと0のままで表示されないのかな?と推測できるが,これも未調査なので不明である。

上記のコードを「TableViewerを使った表コンポーネントの利用」で取り上げた例に追記してビューを表示したときのスナップショットを紹介しておく。

tableviewer-test-view-header.gif

ちなみに,TableViewerオブジェクトを作る前でも作った後でも,上記のヘッダ列の作成は可能である。

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

TableViewerを使った表コンポーネントの利用

Eclipseのビューでは,よく表形式の表示が使われている。タスクリストやブックマークのリスト,プロパティリストやエラーログもみんな表形式である。Eclipseでは,このような表形式で情報を表示したいときのために,TableViewerを提供している

task-list-view.gif

TableViewerはJFaceというツールキットの中のコンポーネントの1つで,SWTで提供されているTableコンポーネントを「より便利に」「より簡単に」使用できるように,という目的のものである。SWTのTableを完全に隠蔽しているわけではなく,TableViewerで提供されている機能を使えばSWTのTableに対してさまざまな細かい操作を肩代わりしてくれるという便利クン,という位置づけである。SWTのTableを隠しもしないし,TableViewerで提供されていない機能は,当然SWTのTableを直接操作する必要がある。

さて,TableViewerを使用して画面上に表を作成するには,最初にSWTのTableクラスのインスタンスを作成する

  Composite parent = ...;
  Table table = new Table(parent,
    SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.FULL_SELECTION);
  table.setHeaderVisible(true);
  table.setLineVisible(true);

Tableクラスのインスタンスを生成するためには,Tableコンポーネントを貼り付ける親のオブジェクト(上記ではparent)と,どのような表にするかを決めるスタイル値をコンストラクタに与える必要がある。親のオブジェクトは,例えばビュー全体に表を貼り付ける場合は,ViewPartクラスのcreatePartControlメソッドの引数で渡されるCompositeオブジェクトを渡すことになる。第2引数の表のスタイルを表す値は,SWTクラスに用意されている各種定数値の論理和(OR)をとった値を渡す。上記であれば「単一の選択で,縦横両方のスクロールバーを持ち,行全体を選択状態にする表」となる。

Tableオブジェクトが生成できたら,その他表に調整を加えたいものがあれば,ここでやっておく。上記では「表のヘッダ行を表示する」「セルを分割する罫線を表示する」という設定を行っている。

ホントは表の列定義などを行ったりするのだが,ここでは割愛する。とりあえずTableオブジェクトが出来上がったので,それを元にTableViewerオブジェクトを生成する

  TableViewer viewer = new TableViewer(table);

これにより,先ほど作成したTableオブジェクトをTableViewerクラスが持つ各種機能を使って「高級的に」操作することができるようになる。

上記のコードだけで,とりあえず空の表を表示することができる(ビューの作成方法は「新規ビューの作成」を参照)。TableViewer Test Viewという名前のビューに上記のコードをcreatePartControlメソッドに埋め込んだときのスナップショットを以下に示す。

tableviewer-test-view-empty.gif

列を何も作ってないのに,2つ列があるのが謎だ。

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

2004.04.09

マルチ・ステータス

Eclipseのプラットフォームや各種プラグイン内で発生したエラーは,エラーの内容をIStatusオブジェクトに格納し,それを持つCoreException例外のスローによって表現する。「CoreExceptionとIStatus」では,あるひとつのエラーに対してのIStatusオブジェクトの作成とCoreException例外のスローについて解説したが,実際には複数のエラーが同時に生じる場合もあり,CoreException例外は複数のエラーの情報を持つことができなければならない。

例えば,プロジェクトが2つ以上のビルダーを持っていた場合,ビルダーの実行中に複数のビルダーで処理に失敗する可能性がある。ビルダーの実行はエラーが生じたかどうかに関わらず全てのビルダーが実行されるため,当然エラーの個数も複数となる。つまり,エラーの内容を持つIStatusオブジェクトが複数生成されるということである。

複数のエラーが生じたことを表現するために,IStatusインタフェースは複数の子IStatusオブジェクトを扱えるように規定されている。複数のステータスのことをマルチ・ステータスと呼ぶ。IStatusインタフェースによるマルチ・ステータスに関する処理の規定は以下のものがある。

  public boolean isMultiStatus() - マルチ・ステータスかどうか
  public IStatus[] getChildren() - 子のステータスの配列

CoreException例外オブジェクトのgetStatusメソッドで取得したIStatusオブジェクトに対して,isMultiStatusメソッドを使用することにより,マルチ・ステータスかどうかを判断することができる。もしisMultiStatusメソッドの結果がtrueだった場合は,getChildrenメソッドを用いて子ステータスの配列を取得し,それぞれに対して処理を行う(個別にログに出力するなど)。

  try {
    ...
  } catch(CoreException e) {
    Plugin plugin = ...;
    IStatus status = e.getStatus();
    plugin.getLog().log(status);
    if (status.isMultiStatus()) {
      IStatus[] children = status.getChildren();
      for (int i = 0; i < children.length; i++) {
        plugin.getLog().log(children[i]);
      }
    }

isMultiStatusメソッドは,子のステータスが1つ以上あるかどうか,というメソッドではないことに注意。あくまでIStatusオブジェクトがマルチ・ステータスなのかどうかを判断するために使用する。これは,単独のステータスの実装クラスがStatusクラスなのに対して,マルチ・ステータスの場合の実装クラスがMultiStatusクラスであることに起因する。isMultiStatusメソッドの戻り値はStatusクラスであれば常にfalse,MultiStatusクラスであれば常にtrueである。

ビルダーの起動において,ビルド処理中にエラーが発生した場合,CoreException例外が捕捉される。この場合はIStatusオブジェクトはマルチ・ステータスなので,getChildrenメソッドを使って発生したエラーの数だけIStatusオブジェクトを取得することができる。

自分でマルチ・ステータスなIStatusオブジェクトを生成する際には,以下のような感じで行えばよい。

  String pluginId = ...;
  int code = ...;
  String message = ...;
  Throwable cause = ...;
  IStatus status1 = new Status(...);
  IStatus status2 = new Status(...);
  IStatus multiStatus = new MultiStatus(
    pluginId, code,
    new IStatus[]{status1, status2},
    message, cause);
  throw new CoreException(multiStatus);

ある処理が別の複数の処理を呼び出す場合などで,複数のエラーを同時に呼び出し元に通知したい場合などは,マルチ・ステータスの使用は非常に有効である。

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

2004.04.08

ビルド処理の進捗状況表示

ワークスペース全体やプロジェクトに対してビルド処理を呼び出す際に,もちろんビルド処理は長い時間がかかる可能性があるので,ユーザに対して進捗情報を表示すべきである。「ビルダーの起動」で紹介した方法では,buildメソッドの第2引数(IProgressMonitorオブジェクト)にnullを渡しているため,ビルド処理中であってもユーザに対して何も表示されないというとても不親切な例だった。

実は,ビルド処理(buildメソッド)の呼び出し時に,IProgressMonitorオブジェクトを渡すことができれば,buildメソッド内の処理で適切に進捗情報を表示してくれるようになる。つまり,「進捗状況のダイアログ表示」で紹介したProgressMonitorDialogクラスを使用すれば,ビルド処理に関する進捗情報をダイアログに表示することができるようになるのだ。

  Shell shell = ...;
  ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
  try {
    dialog.run(true, true, new IRunnableWithProgress() {
      public void run(IProgressMonitor monitor)
          throws InvocationTargetException, InterruptedException {
        try {
          // ワークスペースに対するビルド処理の場合
          ResourcesPlugin.getWorkspace().build(
            IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
          // プロジェクトに対するビルド処理の場合
          IProject project = ...;
          project.build(
            IncrementalProjectBuilder.INCREMENTAL_BUILD, monitor);
        } catch(CoreException e) {
          // ビルド失敗
          throw new InvocationTargetException(e);
        }
      }
    } );
  } catch(InvocationTargetException e) {
    CoreException cause = (CoreException)e.getCause();
    // ビルド失敗に対する処理
  } catch(InterruptedException e) {
    // キャンセルされた場合の処理
  }

進捗状況を表示するために,ProgressMonitorDialogクラスを利用している。ProgressMonitorDialogオブジェクトのrunメソッドに渡すIRunnableWithProgressインタフェースの実装クラス内に,ビルド処理を呼び出すコードを記述している。IRunnableWithProgressインタフェースのrunメソッドに渡ってくるIProgressMonitorオブジェクトをビルド処理(buildメソッド)にそのまま渡してあげることで,ProgressMonitorDialogクラスで作成される進捗表示用のダイアログがビルド処理内で使用され,ユーザに進捗状況が表示されるようになる

何らかのビルダーでビルド処理に失敗した際は,CoreException例外が発生する。その際には,一旦InvocationTargetException例外にCoreException例外オブジェクトを渡してスローする。そしてProgressMonitorDialogオブジェクトのrunメソッド呼び出しに対する例外処理でInvocationTargetException例外をキャッチし,CoreException例外を取り出してビルド失敗時の処理を行う。

EclipseプラットフォームのAPIをいくつも利用していると,結構頻繁にIProgressMonitorオブジェクトを引数に持つメソッドにお目にかかる。その場合は上記のようにProgressMonitorDialogクラスを使って進捗状況をユーザに表示するようにしてあげると,より良いプラグインになるだろう

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

進捗状況のダイアログ表示

処理の進捗状況を表示するためによく使用されるプログレスバー。ビルド作業やリファクタリングなど,比較的長い時間かかる処理を実行する際には,あとどれくらいで処理が終わるのか,または少なくとも長い時間がかかる処理をやってるよ,という表示をユーザに対してするべきである。ここでは,処理の進捗状況をダイアログを用いて表示するための手順を紹介する。

Eclipseでは,進捗状況を簡単に表示するためのクラスを提供してくれている。進捗状況をダイアログで簡単に表示したときは,ProgressMonitorDialogクラスを使用する

  Shell parent = ...;
  ProgressMonitorDialog dialog = new ProgressMonitorDialog(shell);
  try {
    dialog.run(true, true, new IRunnableWithProgress() {
      public void run(IProgressMonitor monitor)
          throws InvocationTargetException, InterruptedException {
        // 進捗を監視する処理
      }
    } );
  } catch(InvocationTargetException e) {
    // 処理中に何らかの例外が発生したときの処理
  } catch(InterruptedException e) {
    // キャンセルされたときの処理
  }

まず,ダイアログを表示するときの親となるShellオブジェクトを取得する(「Shellオブジェクトの取得」「Shellオブジェクトの取得 Part2」参照)。そしてProgressMonitorDialogオブジェクトをShellオブジェクトを元に生成する。

そして,ProgressMonitorDialogオブジェクトのrunメソッドを呼び出して,処理を開始する。runメソッドの引数は3つある。

  第1引数 - 別スレッドとして(3)の処理を実行するかどうか
  第2引数 - 処理の取り消しを有効とするかどうか
  第3引数 - 実行したい処理をカプセル化したオブジェクト

第1引数は通常はtrueでよい。もしfalseにした場合は,上記のコードを実行しているスレッドにより処理が行われてしまうため,[Cancel]ボタンが効かなくなったり,ダイアログの再描画が行われなくなるなどの好ましくない現象が生じてしまう。第2引数をfalseにすると,ダイアログ上にある[Cancel]ボタンがdisableになり,処理のキャンセルができなくなる。

第3引数には,進捗監視対象となる処理を持つオブジェクトを指定する。進捗監視対象の処理は,IRunnableWithProgressインタフェースの実装クラスとして作成するIRunnableWithProgressインタフェースの実装クラスにおいてrunメソッドを実装する。runメソッドは引数としてIProgressMonitorオブジェクトを持っている。runメソッド内において,処理の開始,経過および終了の各タイミングで,IProgressMonitorインタフェースに規定されたメソッドを呼び出すことにより,進捗状況がダイアログに表示される。タスク名,サブタスク名は,ダイアログに表示される。

  処理(タスク)開始時 - monitor.beginTask("タスク名", 合計仕事量)
  副処理(サブタスク)開始時 - monitor.subTask("サブタスク名")
  単位仕事完了時 - monitor.worked(完了仕事量)
  処理完了時 - monitor.done()

さらに,任意のタイミング(例えばサブタスクの開始直前など)でIProgressMonitorインタフェースのisCanceledメソッドを呼び出して,ユーザによって[Cancel]ボタンが押されたかどうかを検査し,処理をキャンセルすべきかどうかを見る必要がある。もしisCacneledメソッド呼び出しの結果がtrueだった場合は,InterruptedException例外をスローして処理がキャンセルされたことを通知する。この例外はProgressMonitorDialogクラスのrunメソッドからスローされてくるので,それをキャッチしてキャンセル処理を行う。

例えば,ある仕事(ここでは1秒寝るだけ)を10回繰り返す処理は,以下のような感じのコードになる。同時に,実行中のスナップショットも紹介しておこう。

  dialog.run(true, true, new IRunnableWithProgress() {
    public void run(IProgressMonitor monitor)
        throws InvocationTargetException, InterruptedException {
      monitor.beginTask("1秒寝る処理を10回繰り返します。", 10);
      for (int i = 0; i < 10; i++) {
        if (monitor.isCanceled()) {
          throw new InterruptedException("Cancel has been requested.");
        } else {
          monitor.subTask((i + 1) + "回目");
          Thread.sleep(1000);
          monitor.worked(1);
        }
      }
      monitor.done();
    }
  } );

ProgressMonitorDialog.gif

処理中に何らかの例外が発生した場合は,InvocationTargetException例外に発生した例外オブジェクトを持たせてスローする。そして,ProgressMonitorDialogクラスの使用元でInvocationTargetException例外をキャッチして適切な処理を行う。発生した例外は,getCauseメソッドで取得することができる

  try {
    dialog.run(true, true, new IRunnableWithProgress() {
      public void run(IProgressMonitor monitor)
          throws InvocationTargetException, InterruptedException {
        try {
          // 何らかの処理
        } catch(FileNotFoundException e) {
          throw new InvocationTargetException(e);
        }
      }
    } );
  } catch(InvocationTargetException e) {
    Throwable cause = e.getCause();
    if (cause instanceof FileNotFoundException) {
      // ファイルがなかったときの処理
    }
  }

このように,進捗状況の表示はProgressMonitorDialogクラスで簡単に実現できる。ユーザフレンドリーなGUIの構築には,このProgressMonitorDialogクラスは欠かすことのできないものである。

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

2004.04.06

ビルダーの起動

あるワークスペースに存在する全プロジェクトに対して,またはあるプロジェクトに対して,それが持つビルダーをプラグインから起動することができる。今回は,ビルダーをプラグインのプログラム内から実行する方法を紹介する。

まずはあるワークスペースに所属する全プロジェクトに対してビルダーを実行する方法から取り上げよう。ワークスペースに対してビルダーの起動を指示するには,IWorkspaceインタフェースのbuildメソッドを使用する

  try {
    IWorkspace workspace = ResourcesPlugin.getWorkspace();
    workspace.build(
      IncrementalProjectBuilder.INCREMENTAL_BUILD, null);
  } catch(CoreException e) {
    // ビルド失敗処理
  }

まず,ResourcesPluginクラスのgetWorkspaceメソッドで,ワークスペースのオブジェクト(IWorkspaceオブジェクト)を取得する。その後,IWorkspaceオブジェクトのbuildメソッドを呼び出すことで,ワークスペースが持つ各プロジェクトに対してビルダーが実行される

buildメソッドの引数は2つ。第1引数はビルドの種類を表す定数値(INCREMENTAL_BUILDまたはFULL_BUILDのどちらか)を指定する。第2引数はビルドの進捗状況を表示するためのIProgressMonitorオブジェクトを指定する。上記のように第2引数にnullを指定した場合は,進捗状況を表示するためのダイアログは表示されず,ビルド作業のキャンセルもできない

次に,ある1つのプロジェクトに対しての話に移る。ある1つのプロジェクトに対してビルド作業を指示するには,IProjectインタフェースのbuildメソッドを使用する。

  try {
    IProject project = ...;
    project.build(
      IncrementalProjectBuilder.INCREMENTAL_BUILD, null);
  } catch(CoreException e) {
    // ビルド失敗処理
  }

最初に,何らかの手順でビルド対象のプロジェクトのオブジェクト(IProjectオブジェクト)を取得する。その後,IProjectオブジェクトのbuildメソッドを呼び出すことで,そのプロジェクトが持つビルダーの処理が実行される。buildメソッドの引数は,IWorkspaceインタフェースのbuildメソッドの時と同じである。

上記2つのbuildメソッドは,どちらも(ビルド作業が失敗するしないに関わらず)すべてのビルダーが実行される。そしてすべてのビルダーのビルド処理が終了したときに,ひとつでもビルド作業に失敗が生じていた場合はCoreException例外がスローされる。この場合のCoreException例外オブジェクトには1つ以上の複数のビルド失敗に関する情報が格納されている。これについては「マルチ・ステータス」を参照されたし。

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

2004.04.02

ビルドの種類

新規ビルダーの定義を行った際,ビルダーのクラスの中でbuildメソッドが登場した。そのメソッドのシグネチャは,

  protected IProject[] build(
    int kind, Map args, IProgressMonitor monitor)
      throws CoreException;

となるが,第1引数のint kindって一体何なのだろうか。

突然だが,Javaプログラムの開発をJDTを用いて行っている場合,プログラミング作業に不可欠なコンパイル作業を明示的に行う必要がない。JDTで提供されるJavaビルダーが自動的にコンパイル作業を行ってくれている。

この際,Javaプロジェクト内のすべてのソースコードに対してコンパイルを行っているかというと,ほとんどの場合そうではない。ソースコードというリソースについて,変更が生じたリソースのみをコンパイルするようにJavaビルダーは動作している。完全にコンパイル作業を開発者に意識させないようにするためには,最低限のリソースに対してビルドを行い,できる限りビルド時間を短縮しなければならない。このように,変更が加えられたリソースに対してのみ次々とビルドを行っていくビルドのことを,インクリメンタルビルドと呼ぶ新規ビルダーの定義で作成したビルダーのクラスが継承した親クラスの名前「IncrementalProjectBuilder」の由来も理解できるだろう。

もちろん,すべてのリソースに対してビルドを行わなければならないときもある。その場合は,Eclipseの「Rebuild All」or「Rebuild Project」メニューを選ぶことで,例えばJavaビルダーはすべてのソースコードに対してコンパイルを行ってくれる。すべてのリソースに対してビルドを行うことを,完全ビルドと呼ぶ

上記のビルドの種類は,IncrementalProjectBuilderクラスの定数として定義されている。

  ・IncrementalProjectBuilder.INCREMENTAL_BUILD - インクリメンタルビルド
  ・IncrementalProjectBuilder.FULL_BUILD - 完全ビルド
  ・IncrementalProjectBuilder.AUTO_BUILD - 自動ビルド

AUTO_BUILDに関しては,内容を変更したリソースに対して保存を行ったときなどに,Eclipseが自動的にビルドを実行したことを表す定数である

さて,冒頭で紹介したbuildメソッドの第1引数には,上記の定数のうちのいづれかの値がEclipseによって渡される。kindにFULL_BUILD値が渡されたときには,全リソースに対してビルド作業を行い,それ以外の場合はリソース変更デルタ(どのリソースが変更されたのか,という情報を持つ。そのうち解説する予定)を見て,インクリメンタルビルドを行うか完全ビルドを行うかを判断する。kind引数によるビルド処理の分岐の例を以下に示す(これは「Platform プラグイン・デベロッパー・ガイド」に記載されているコードである)。

  protected IProject[] build(
      int kind, Map args, IProgressMonitor monitor)
        throws CoreException {
    if (kind == FULL_BUILD) {
      // 完全ビルド
    } else {
      IResourceDelta delta = getDelta(getProject());
      if (delta != null) {
        // インクリメンタルビルド
      } else {
        // 完全ビルド
      }
    }
  }

リソース変更デルタオブジェクトは,getDeltaメソッドを呼び出すことで取得できる。これがnullの場合は,リソースの変更が行われていないと判断し,完全ビルドを行っている。もしリソース変更デルタオブジェクトがnullではない場合は,それが持つ情報を元にインクリメンタルビルドを行っている。もちろんkind引数がFULL_BUILDだった場合は,完全ビルドを行っている。

このbuildメソッド内の処理如何によって,Eclipseの使用感に雲泥の差がでてくるので,プラグイン開発者の腕の見せ所であると言えるだろう。

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

2004.04.01

Shellオブジェクトの取得 Part2

以前「Shellオブジェクトの取得」でShellオブジェクトの取得方法を紹介したが,よりEclipseの作法に則っていると思われる方法があるので,ここで紹介する。

Shellオブジェクトは,ワークベンチオブジェクト(IWorkbenchオブジェクト)から辿って取得することができる

  IWorkbench workbench = PlatformUI.getWorkbench();
  IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
  Shell shell = window.getShell();

まず,PlatformUIクラスのgetWorkbenchメソッドを呼び出して,IWorkbenchオブジェクトを取得する。その後,getActiveWorkbenchWindowメソッドにより,現在アクティブなワークベンチウィンドウのオブジェクトIWorkbenchWindowオブジェクト)を取得する。Shellオブジェクトはトップレベルウィンドウを表すオブジェクトなので,IWorkbenchWindowオブジェクトはドンぴしゃりのオブジェクトであり,IWorkbenchWindowインタフェースに規定されたgetShellメソッドによって,Shellオブジェクトを取得することができる

ここで注意することは,上記のコードの実行のタイミングによっては,ワークベンチウィンドウがまだ存在していない可能性があるということである。Eclipseの起動直後とか,マルチスレッドでEclipseがフォーカスを得ていない状況だったとかがその例である。その場合,getActiveWorkbenchWindowメソッドの戻り値はnullになってしまうので,getShellメソッドを呼び出す前に,getActiveWorkbenchWindowメソッドの戻り値がnullかどうかを判断する処理をしておくことが多くの場合必要だろう。

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

すべてのエディタの内容を保存する

Javaのソースコードに対して自動的にコンパイルしたいときなど,エディタの内容をプラグイン内で保存したいときがある。Eclipseでは,ワークベンチ内で開かれているすべてのエディタに対して,その内容を保存することを簡単に実行するためのメソッドが存在する

  IWorkbench workbench = PlatformUI.getWorkbench();
  boolean saved = workbench.saveAllEditors(true);

まず,ワークベンチのオブジェクト(IWorkbenchオブジェクト)を取得する。PlatformUIクラスのクラスメソッドとして提供されているgetWorkbenchメソッドを呼び出すことで,IWorkbenchオブジェクトを取得できる。そして,IWorkbenchインタフェースで規定されたsaveAllEditorsメソッドを呼び出すことで,開かれているすべてのエディタに対して,内容を保存するよう指示をだすことができる

saveAllEditorsメソッドは,boolean型の引数を持つ。上記のように引数にtrueを渡した場合,ユーザに保存するエディタを選択させるためのダイアログが表示される

save-resources.gif

ダイアログで[OK]ボタンが押された場合は,saveAllEditorsメソッドの戻り値がtrueに,[Cancel]ボタンが押された場合は,saveAllEditorsメソッドの戻り値はfalseになる。もちろん[Cancel]ボタンが押されたときは,保存処理は行われない。

saveAllEditorsメソッドの引数にfalseを指定した場合は,ダイアログは表示されずに,強制的に保存処理が実行される。つまり,暗黙のうちにすべてのエディタの内容が保存される。何らかのプラグインが勝手にすべてのエディタの内容を保存していいのかどうかは賛否両論あると思うが,まぁユーザビリティをどこまで考えるか,ということになると思う。

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

« 2004年3月 | トップページ | 2004年5月 »