2005.11.26

UIスレッドでのタイマー実行

ユーザインタフェースを操作する処理を一定時間ごとに繰り返し行いたい場合,スレッドを使うのが一般的である。しかし,普通に自前で生成したスレッドからはSWTのUIコンポーネントを操作することができないために,DisplayクラスのsyncExec()やasyncExec()を使用する必要がある。ある間隔を持って処理を繰り返し行う場合は,Thread.sleep(500)というようにしてスレッドの実行を停止させることが思いつく方法だが,SWTではOSのタイマーイベントを使用する方法が提供されている。

OSが持つタイマーイベントを利用するには,DisplayクラスのtimerExecメソッドを使用する。

  Runnable runnable = new Runnable() {
    public void run() {
      // 繰り返し行いたい処理
      Display display = ...;
      if (!display.isDispose())
        display.timerExec(500, this);
    }
  };
  Display display = ..;
  if (!display.isDispose())
    display.timerExec(500, runnable);

繰り返し行いたい処理は,Runnableインタフェースの実装として作成する。上記では,匿名クラスとしてRunnableインタフェースの実装オブジェクトを生成している。そして,Displayオブジェクトが破棄されているかどうかをチェックし,破棄されていなければ,DisplayオブジェクトのtimerExecメソッドに時間とRunnableオブジェクトを渡す。第1引数は,Runnableオブジェクトのrunメソッドを呼び出すまでの待ち時間を指定する。上記では,0.5秒後にrunメソッドを呼び出すように指定している。

timerExecメソッドによって,OSにタイマーの登録が行われ,指定時間後にタイマーイベントが発生し,それを契機としてrunメソッドが実行される。ただし,実行されるのは1回だけ。繰り返し処理を行いたい場合は,runメソッド内で再度timerExecメソッドを呼び出して再びrunメソッドが実行されるように登録すればよい。その際に,Displayメソッドが破棄されているかどうかをチェックする必要がある。

このrunメソッドの呼び出しはUIスレッドが行うので,SWTのUIをrunメソッド内で操作することが可能だ。

あるUIコンポーネントを定期的に操作する方法としては,原則として上記のtimerExecメソッドを使用するべきだ。例えば,下記のように自前でスレッドを生成し,その中で繰り返しsyncExecメソッドなどの呼び出しを行ってUIコンポーネントを操作した場合は,他のコンポーネントに影響が発生してしまう。

  Runnable runnable = new Runnable() {
    public void run() {
      while(true) {
        Display display = ..;
        if (!display.isDispose()) {
          display.syncExec(new Runnable() {
            public void run() {
              // 繰り返し処理
            }
          });
        }
        Thread.sleep(500);
      }
    }
  };
  (new Thread(runnable)).start();

上記のようなコードでも,繰り返し処理はちゃんと実行されるだろう。しかし,DirectoryDialogクラスを利用して実現されるディレクトリ選択ダイアログの動作が,正しく行われなくなってしまう。具体的には,サブディレクトリやアイコンの読み込みがマルチスレッド化されているのだが,そのスレッドが動作しなくなってしまい,ディレクトリの選択ができないといった現象になる。

気軽にスレッドを作ることはできるが,その結果思わぬ不具合を引き起こしてしまう可能性があるので,気をつけて欲しい。

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

2005.11.24

ステータスバーへのコンポーネント登録と削除

ステータスバーへのアイコン登録

今回は,Eclipseのワークベンチウィンドウに配置されているステータスバーへアイコン表示コンポーネントを登録する方法を紹介する。アイコンはSWTのLabelコンポーネントを使うので,実質上はSWTコンポーネントのステータスバーへの登録方法となる。

まず,普通にエディタがアクティブなときのステータスバーを見てみると,以下のような感じになっている。

status0

編集モードやキャレットの位置などが,セパレータで区切られて表示されている。もちろん,自分のプラグインの領域を確保したいので,セパレータの登録も是非やっておきたい一つだろう。

まずはワークベンチウィンドウから,ステータスバーマネージャを取得する。この方法は,「ステータスバーへのアクセス」で紹介した手順で行えばよい。基本的には,得られたIStatusLineManagerオブジェクトのaddメソッドを使って,コンポーネントやアクションをステータスバーに登録する。例えば,アクションを登録するには以下のような感じになる。

  IStatusLineManager manager = ...; // ステータスバーオブジェクトの取得
  manager.add(new Action("アクション") {});

status1

SWTのコンポーネントをステータスバーに登録するためには,IContributionItemインタフェースを実装したクラスのオブジェクトが必要となるが,ControlContributionクラスを使用することが一般的だろう。

  manager.add(new ControlContribution("my_id") {
    protected Control createControl(Composite parent) {
      new Label(parent, SWT.SEPARATOR); // セパレータの登録
      Label l = new Label(parent, SWT.NONE); // アイコンの登録
      ImageDescriptor desc = ...;
      l.setImage(desc.createImage());
      return parent;
    }
  });

登録するIContributionItemオブジェクトには,IDが必要となる。このIDは,ステータスバーからオブジェクトを取得したり削除する際に使用できる。上記では,ControlContributionクラスのコンストラクタにIDを渡している。

セパレータは,Labelクラスのコンストラクタの第2引数にSWT.SEPARATORを渡すことで登録できる。上記の例では,その後にイメージを持つLabelオブジェクトを登録している。createControlメソッドに渡されるCompositeオブジェクトは,レイアウトとしてStatusLine$StatusLineLayoutクラスが適用されていて,コンポーネントの生成順に左から配置してくれるようになっている。

これを実行すると,以下のような感じになる。アイコンは,IP Messengerのものを使用してみた。

status2

登録したIContributionItemオブジェクトを削除するためには,以下のようにすればよい。

  IContributionItem item = manager.find("my_id");
  manager.remove(item);
  manager.update(true);

findメソッドにIDを渡してIContributionItemオブジェクトを取得し,それをremoveメソッドに渡して削除する。その後,updateメソッドを呼び出して,削除したことをステータスバーの表示に反映させる。

上記のIContributionItem系の扱いは,ツールバーやメニューバーに関しても同じなので,覚えておくと応用が利くだろう。

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

2005.11.22

ステータスバーへのアクセス

Eclipseの最下部には,いろんな情報を通知してくれるステータスバーが位置している。自作プラグインから,このステータスバーを利用したいと思うことも多いだろう。今回は,Eclipseのステータスバーへアクセスするための方法を紹介する。

statusbar

Eclipseの下部に配置されているステータスバーは,状況依存のステータスバーである。つまり,状況によって表示される内容が異なるということだ。ここで言う状況とは,ワークベンチ内で何が選択されているか,という状況である。例えばテキストエディタがアクティブになれば,編集属性やキャレットの位置が表示されるし,アウトラインビューがアクティブになれば,アウトラインに関する情報に表示が切り替わる。表示できる領域は限られているので,今どこに着目しているかによって表示が限定されるということだ。

ステータスバーを管理しているオブジェクトは,IStatusLineManagerインタフェースで表される。「ワークベンチウィンドウに張り付いているんだから,IWorkbenchWindowインタフェースに取得メソッドがあるんじゃないの?」と思ってしまうが,そんなに簡単ではない。なぜなら,状況依存という特徴があるからだ。

まずは,特定のViewに依存するステータスバーオブジェクトの取得方法から紹介しよう。ViewPartクラスのサブクラス内で,以下のようにすればステータスバーオブジェクトを得ることができる。

  IViewSite viewSite = getViewSite();
  IActionBars actionBars = viewSite.getActionBars();
  IStatusLineManager manager = actionBars.getStatusLineManager();

このmanagerオブジェクトに対してコンポーネントやアクションを登録することで,そのViewに特化したステータスバーに登録することができる。もちろん状況依存なので,managerに登録されたコンポーネントやアクションは,そのViewがアクティブになったときにしか表示されない。

次は,特定のEditorに依存するステータスバーオブジェクトの取得方法を紹介する。EditorPartクラスのサブクラス内で,以下のようにすればステータスバーオブジェクトを得ることができる。

  IEditorSite editorSite = getEditorSite();
  IActionBars actionBars = editorSite.getActionBars();
  IStatusLineManager manager = actionBars.getStatusLineManager();

このmanagerオブジェクトに対してコンポーネントやアクションを登録することで,そのEditorに特化したステータスバーに登録することができる。もちろん状況依存なので,managerに登録されたコンポーネントやアクションは,そのEditorがアクティブになったときにしか表示されない。

さて,上記の方法では,特定のViewやEditorがアクティブにならなければ,せっかく登録したコンポーネントやアクションが表示されない。時には状況に依存せずに常にコンポーネントやアクションをステータスバーに表示させておきたいこともあるだろう。状況非依存のステータスバーオブジェクトを得る方法があるのだが,Eclipse的には反則気味なやり方になる。internalパッケージのクラスを利用しなければならないからだ。

  WorkbenchWindow workbenchWindow = (WorkbenchWindow)getSite().getWorkbenchWindow();
  IActionBars actionBars = workbenchWindow.getActionBars();
  IStatusLineManager manager = actionBars.getStatusLineManager();

WorkbenchPartクラス(ViewPartやEditorPartの親)のサブクラス内で上記の処理を行うことによって,状況非依存のステータスバーのオブジェクトを得ることができる。このmanagerオブジェクトに対してコンポーネントやアクションを登録することで,ViewやEditorの選択状態に関わらず,常にコンポーネントやアクションが表示されるようになる。

ただでさえ狭い領域なので,ステータスバーにはむやみに常に表示されてしまうようなコンポーネントやアクションを登録するべきではない。internalパッケージにしか取得メソッドが存在しないのは,推奨できない方法なんだというメッセージが含まれているのだろう。よほど特殊な場合でなければ状況依存の方法を採用し,どうしてもという時は,16x16のアイコンの登録くらいに押さえておくようにすべきだろう。

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

2005.10.14

Contributing to Eclipse本のサンプルコード

「Eclipseプラグイン開発を行うなら,まずはこれを読め!」という濃い内容の「Contributing to Eclipse」本,これに掲載されているソースコードのダウンロード元をいつも忘れてしまうので,メモしておく。

Eclipseプラグイン開発(Contributing to eclipse)
http://www.asahi-net.or.jp/~yf8k-kbys/eclipse.html

上記は訳本の紹介サイトだけど,ソースコードは基本そのまんまなので有効である。

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

2005.10.01

sourceforge.jpはじめました(ipmsg4e)

IP Messenger for Eclipseの開発を進めるにあたって,やはり「オープン」であることが大事かなと思い,sourceforge.jpにてソースコードやトラッキングなどの情報を,一般に公開することにした。

[IP Messenger for Eclipse(ipmsg4e) : sourceforge.jp]
https://sourceforge.jp/projects/ipmsg4e/

もちろんこのブログ内でもエントリは続けていくが,このブログはあくまで「プラグイン開発のための情報」をエントリしていきたいので,ipmsg4eについてのことは上記の場所で行っていくことにする。

もし要望などのご意見があれば,上記のフォーラムなどでどんどん連絡してほしい。

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

2005.09.19

早速バグ発見(IPMessengerプラグイン:コア)

j2sdk1.4.2(Windows,Linux)でEclipseを動作させているときに,何らかのメッセージを受け取ると文字列解析に失敗(NoSuchMethodError)する。コアのバージョン0.0.1を公開したので,j2sdk1.4.2を使用している方はアップデートして欲しい。

外見の現象としては,ユニキャストを受信できないというものだった。開発開始当時,DatagramSocketの使い方を間違っていて,ブロードキャストされたものしか受信できないという全く同じ現象がでたので,「あれ?JDK1.5とJDK1.4.2でDatagramSocketの実装が違うの?VMのバージョン見てDatagramSocketの使い分けしないといけない?」とか疑ってしまった。

なんてことはない。JDK1.5から搭載されたAPIを使ってしまっていただけのことだった。早合点してしまった自分に反省。orz

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

2005.09.18

IPMessengerプラグイン公開開始しました

予告していたIP Messenger for Eclipseだが,アップデートサイトに配置し,公開を開始した。

update-site: http://www.eisbahn.jp/update-site/

ipmsg

今回の公開は,とりあえず「ちゃんと作ってますよ」というメッセージをこめて,バージョン0.0.0で公開することとした。つまり,使い勝手はもちろん,ニックネームの設定もできず,機能も不足していることを完全に自覚した「お試しバージョン」として捉えて欲しい。

なので,Eclipse3.1+JDK1.5.0_04(Windows, Linux)の組み合わせでしか動作確認をしていない。JDK1.4.2シリーズでもうまくいくとは思うけど,もしうまく動作しなかったらコメントを寄せて欲しい。

IP Messengerビューとして作成してあるので,[Windows]-[Show View]メニューで「Other」カテゴリからビューを表示して使用する。使い方については,前回のエントリを参考にして欲しい。

このバージョン0.0.0では,超基本的な機能として,ネットワークへの参加・脱退,メッセージの送信・受信,開封確認という機能のみの実装である。不在通知や暗号化,ファイル転送については,たぶん後回しにする。それよりも,Eclipseならではの機能の実装を優先するつもりでいる。何故かは,そのうち発表しようと思う。

IP Messengerプラグインは,コアとUIの2つのプラグインに分けて開発した。コアはIP Messengerの基本的な機能を提供するプラグインだ。ネットワークへの参加・脱退,メッセージの送受信,ネットワーク上にいるユーザの管理などが,コアにより行われる。UIのプラグインは,コアの機能を使用して,ユーザインタフェースを提供する。

コアのプラグインについては,拡張ポイントも準備している。ソース公開を後日予定しているので,興味のある方についてはもう少しお待ちくださいということで。

「動かない!」「いい感じ!」「こんな機能が欲しい!」「こんな操作感がいいのでは?」などなど,どしどし意見を寄せて欲しい。あまり否定的な意見だとモチベが下がっちゃうので,できれば前向きな意見を下さると嬉しいかも。

では,お試しくださいっ!!

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

2005.09.05

IPMessengerプラグイン作ってます!

翻訳プラグインの超マイナーバージョンアップのお知らせを久々にエントリしたこのブログ。気がつけば,10万件アクセスを突破した。着実にEclipseプラグイン開発が広がりを見せている証拠だと思う。非常に嬉しい限りだ。

さて,最近の自分といえば,Eclipseプラグインから離れていた・・・わけではなく,新プラグインの開発を虎視眈々ともくろんでいた。そしてついさっき,基本的な機能が動き出したので,ここで紹介しようと思う。

今僕は,IPMessengerのEclipseプラグイン版を開発している。その画面イメージは以下のような感じだ。

ipmsg1

左側にメンバーの一覧とメッセージの入力エリア,右側に送受信履歴とメッセージの表示エリアを配置している。[送信]ボタンを配置すると場所をとってしまうので,入力エリアで「Ctrl+Space」キーを押すことで送信できるようにしている。送信したメッセージが開封されたときは,送受信履歴一覧の未読の「○」が消えるようにしている。

送信処理のリトライ送信やメッセージの暗号化,ログファイルの出力など,本来のIPMessengerが持つ基本機能をまだ実装していないので,一般公開はもうちょっとかかりそう。まぁ,IPMessenger開発研究室からC++やJavaの参考にできるコードを入手できるので,そう時間はかからないはず。

それよりも,Eclipseプラグインだからこその機能をいろいろと盛り込んでみたい。今思っているのは,
  (1) 「プロジェクト/ファイル/位置」をメッセージに付加して,相手側のエディタで表示させる機能。
  (2) あるコードを変更したことを,他ユーザに通知する機能
って感じだ。共同作業の助けとなる機能が中心になるだろう。もちろん,相手が普通のIPMessengerでも,読んで意味のわかる記述にする必要がある。

とにかく,元のIPMessengerがダイアログベースなものなため,ユーザインタフェースについては,まだまだかなり改善しないといけない。エディタとの連携というか,エディタ上にいろいろ噴出し的な表示をするようにしても面白いかも。

もし「こうして欲しい」みたいな意見などがあれば,是非コメントを寄せて欲しい。もちろん,基本機能を最優先に搭載して,さっさと一般公開するようにするつもりだが,さくっと組み込めるものであれば一緒に組み込むことも可能だと思うので,どしどし意見をくれると嬉しい。

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

2005.07.31

翻訳ビュープラグインVer. 1.1.1公開!

翻訳ビュープラグインを久しぶりにバージョンアップした。といっても,だいぶ前に修正したものなんだけど。。。

修正点は,Linuxでもちゃんと動くようにした,という点だけ。文字化けしてしまうという報告を受けていたので,その対処を施した。

それと,アップデートサイトの場所を変更した。今まではso-netの自分の領域だったが,自ドメインを取得したので,以下の場所に変更した。

update-site: http://www.eisbahn.jp/update-site/

Windows以外でEclipseをお使いの方は,是非試してみて欲しい。

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

2005.04.09

Eclipse起動時のプラグイン活性化

Eclipseは,その起動にかかる時間をできる限り一定にするために,いろいろな工夫が施されている。プラグインの集合がEclipseを形成しているのだが,プラグインの数は半端じゃないし,起動時に活性化しなくても良いプラグインも多く存在する。すべてのプラグインを活性化していてはEclipseの起動時間は大変なことになるので,最低限起動に必要なプラグインのみが活性化されるようになっている。

さて,プラグインによっては,Eclipse起動時に何かを行わなければならない処理もあるだろう。例えば,ワークスペースに対する何らかの変更を監視するリスナーを追加したり,Eclipseを何らかのドメインに特化した開発環境にする,などが考えられる。そもそもEclipseの起動時にのみ何かを行いたいだけのプラグインもあるかもしれない(思いつかないけど)。

久々の今回は,Eclipseの起動時にプラグインを活性化させるための方法について紹介する。起動時に活性化するプラグインは,org.eclipse.ui.startup拡張ポイントを使用する

  <plugin id="..." ...>
    ...
    <extension point="org.eclipse.ui.startup"/>
    ...
  </plugin>

さらに,そのプラグインのPluginクラスに対して,IStartupインタフェースを実装しておく。

  public class MyPlugin extends AbstractUIPlugin implements IStartup {
    public void start(BundleContext context) throws Exception {
      ...
    }
    public void earlyStartup() {
      ...
    }
  }

これにより,Eclipse起動時にプラグインが活性化され,上記のMyPluginクラスのインスタンスが生成される。それと同時に,startメソッドとearlyStartupメソッドが順に呼び出される

上記のような場合は,startメソッドとearlyStartupメソッドを呼び出すスレッドは同一のようなので,どちらに処理を記述しても問題ないだろう。まぁ気分的には,startメソッドに書くのが自然だと思う。なお,活性化時点で他のIStartup実装オブジェクトを実行したい場合は,org.eclipse.ui.startup拡張ポイントのclass属性にIStartup実装クラスを記述しておくことで実現できる。

もちろん,IStartup実装クラス内には,いつまでたってもEclipseが起動してこないのはよろしくないので,比較的短時間で終わる処理を記述することが好ましい。

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

2005.03.20

翻訳ビュープラグインをバージョンアップしました

昨年の11月3日に公開した翻訳ビュープラグイン,3ヶ月余りの沈黙を破り,バージョンアップを行った。

基本的な機能は特に修正をしていないが,使い勝手の向上と,翻訳サイトとのインターフェイスの設定を行えるようにした。具体的には以下の修正を施してある。

  ・Eclipseのシャットダウン中に翻訳処理が実行された場合,例外が発生していた不具合を修正。
  ・クリップボードを扱うための右クリックメニューを追加。
  ・翻訳中にビューのタイトルイメージを変更するように修正。
  ・Ctrl+Spaceキーで翻訳を実行する機能を追加。
  ・Javaエディタおよびテキストエディタに翻訳実行のためのメニューを追加。
  ・翻訳サイトとのインターフェイスを設定するための設定画面を追加。

これらのいくつかは,ここにコメントして頂いた要望である。

tv

目玉としては,やっぱり翻訳サイトと通信する際のインターフェイスを変更するための設定ができるようになったことだろう。少なくとも,Exciteの翻訳サイトのインターフェイスが変わってしまっても,対応することができるようになった。

preference

インストール方法は,前のバージョンと同じように,更新サイトから行ってほしい。更新サイトは,

  Name: Yoichiro's update site
  URL: http://www.eisbahn.jp/update-site/

である。これにアクセスすると,Version1.1.0があるのがわかると思う。

ここで注意。良く確認していないのだが,もし既にVersion1.0.0をインストールしているなら,それをまず削除してから,Version1.1.0をインストールしてほしい。2つのバージョンを混在させた状態での不具合を連絡されても,無視するのでご注意を。

では,素敵なEclipseライフをお送りくださいませ。

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

2004.11.04

ごめんなさいっ!

@ITの「連載:Eclipse徹底活用(10) 辞書検索プラグインを作る」をお書きになった方から,さっそくトラックバックを頂いた。

ごめんなさい!悪気はなかったんです!

たぶん上記サイトで解説されているプラグインが入手できれば,翻訳ビュープラグインは作らずにそれを使っていただろう。ただ,残念ながらSourceForgeにモノがなかったので,「自分で作っちゃえ」と思って作ってしまった。インタフェースなどの仕様は,使いやすさとEclipseのAPI的なしがらみを考えると,@ITの記事のものと似てしまった。もちろん失礼がないよう,機能面では+αして恥ずかしくないものにしたつもり。

あと,決定的に「辞書引き」と「翻訳」では機能が違う。辞書引きじゃないとダメなことも多々あるので,僕は辞書検索プラグインが提供されれば,たぶん翻訳ビュープラグインは使わなくなる。文章まるごと翻訳よりも単語の意味を知りたいことのほうが多いので。それに,僕が作った翻訳ビュープラグインはExciteの翻訳サイト必須なので,ネットにつながってないと機能しないし。

というわけで,辞書検索プラグイン,期待してます!何かしらのPDIC形式の辞書を買って,プラグインが公開されるのを心待ちにしています。

ちなみに,このブログ,10ヶ月以上やってるのにトラバついたのは何と2回目。めちゃくちゃ嬉しかったことは言うまでもない。

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

2004.11.03

翻訳ビュープラグイン

Eclipseを使って開発をしていると,英語のコメントなどを翻訳したくなったりする。特にオープンソースのJavaのソースコードなどを見ているときに,手軽に翻訳ができると便利である。Webブラウザや翻訳ソフトを使えば翻訳はできるが,Eclispeといったりきたりするのはとても面倒。Eclipseの中で済ませたくなる。

そこで,日英,英日の翻訳をしてくれるプラグインを作ってみた。このプラグインは,翻訳をしてくれるビューをEclipseに提供してくれる。動作確認はEclipse3.0.1で行ったが,たぶん3.0以上であればOK。スクリーンショットはこんな感じ。

translationview1.gif

インストールはSoftware Updateを使って自動インストールを使って行う。[Help]-[Software Updates]-[Find and Install]メニューを選択して,「Install/Update」ダイアログを表示させる。[Search for new features to install]項目を選択させて[Next]ボタンを押下,次の画面の[New Remote Site]ボタンを押して,表示される「New Update Site」ダイアログの各項目に以下のように入力し[OK]ボタンを押す。

  Name: Yoichiro's update site
  URL: http://www.eisbahn.jp/update-site/

すると,[Sites to include in search:]項目内にサイトが追加されるので,それにチェックを入れて[Next]ボタンを押す。次の画面で「TranslationView Plug-in Feature」が表示されるので,それにもチェックを入れて[Next]ボタンを押す。「フリーウェアです。再配布してもいいけど,改変はしないでね」という超簡単なライセンスが表示されるので,同意できる人は[I accept ...]項目にチェックを入れて[Next]ボタンを押し,さらに次の画面で[Finish]ボタンを押す。「電子署名がないぞ」と警告ができるが,[Install]ボタンを押すことでインストールが行われる。インストール完了後,(たぶんやらないでもOKだけど)ワークベンチを再起動して準備完了となる。

翻訳ビューを呼び出すには,[Window]-[Show View]-[Other]メニューを選択し,[Other]カテゴリにある「Translation」を選択し[OK]ボタンを押すことで,翻訳ビューがパースペクティブに追加される。

使い方はとっても簡単。Beforeフィールドに翻訳したい文章(英語か日本語)を入れて,そのフィールドをフォーカスアウトさせる(マウスでAfterフィールドをクリックする,[Ctrl]+[Tab]キーを押す,など)ことにより,Afterフィールドに翻訳結果が表示される。基本的にはこれだけ。シンプルである。

ただし,エディタとの連携は考えてある。テキストエディタやJavaエディタの中で翻訳したい文字列を選択状態にしておき,翻訳ビューをアクティブにするだけで,自動的に選択文字列を翻訳し結果を表示してくれる。ちなみに,全部半角文字だった場合は英日翻訳,一文字でも全角文字が含まれていた場合は日英翻訳を行う。

翻訳ビューのツールバーとメニューには,いくつかのアクションが準備されている。左から順に解説すると...

translationview2.gif」ボタンを押すと,翻訳結果の文章をアクティブになっているエディタに挿入する。
translationview3.gif」ボタンを押すと,翻訳結果の文章をクリップボードにコピーする。
translationview4.gif」ボタンを押すと,BeforeフィールドおよびAfterフィールドをクリアする。
translationview5.gif」ボタンをOFFにすると,テキストエディタから読み取った選択文字列の[/*][*][*/][//]およびタブ文字を空白に変換する。
translationview6.gif」ボタンをOFFにすると,テキストエディタから読み取った選択文字列のタグ文字(正規表現でいうと)を空白に変換する。

という機能である。translationview5.gif」ボタンと「translationview6.gif」ボタンを両方ともOFFにしておくと,Javaソースコード中のJavadocコメントを翻訳する際に言い感じになるだろう。

このプラグイン,Exciteの翻訳サイトを使って翻訳を行っている。つまり,翻訳結果は内部でHTMLを解析して抜き出しているのである。よって現時点では,Exciteの翻訳サイトのHTTPリクエストの書式や結果のHTMLの内容が変わってしまうと,翻訳はできなくなるのでご了承いただきたい。もちろん,今後うまくカスタマイズできるように機能拡張するつもりではいる。

「こんな機能付けて!」とか「ソース公開してよ」などのご要望や不具合報告は,コメントなりトラックバックなり掲示板なりでご連絡ください。

| | コメント (33) | トラックバック (3)

2004.10.21

かっこいいタブの作り方(CTabFolder)

Eclipse2.1からEclipse3.0になって最も目立つ変更点,それはIDEの概観である。2.1までは角ばっていたイメージが,3.0からは曲線による丸いイメージに変化している。特に複数のビューやエディタを束ねるためのタブについては,とてもかっこいい仕上がりになっている。例えば「タブブラウザを自作したい」といった場合,このタブを使わない手はない。もちろん,自作プラグインやリッチクライアントにて上記のタブを利用できるようになっている。今回は,このかっこいいタブを使うための方法について紹介する。

CTabFolder1.gif

SWTでは従来からTabFolderクラスにてタブペインを実現するためのコンポーネントが提供されてきたが,Eclipse3.0からはSWTのカスタムコンポーネントとしてCTabFolderクラスが提供されている。このCTabFolderクラスが,かっこいいタブを実現してくれているコンポーネントである。以下に使い方の例を示す。

  CTabFolder tabFolder = new CTabFolder(
        composite, SWT.BORDER | SWT.CLOSE);
  CTabItem taroTab = new CTabItem(tabFolder, SWT.NONE);
  taroTab.setText("タロ(Mr. Taro)");
  CTabItem jiroTab = new CTabItem(tabFolder, SWT.NONE);
  jiroTab.setText("ジロ(Mr. Jiro)");

CTabFolder2.gif

まず,貼り付ける親のコンポーネントとSWTクラスで定義された定数を使って,CTabFolderオブジェクトを生成する。上記では,SWT.BORDER定数を指定することによりタブペイン全体の枠線を描画させ,さらにSWT.CLOSE定数を指定することにより各タブに閉じる(×)ボタンを付与させている。その後CTabItemオブジェクトを2つ生成してタブを追加し,それぞれにタブの名称をセットしている。

これではEclipseのIDEとちょっと見た目が違っているが,もちろん各種setterを使うことによって概観を変更することができる。

まずはEclipseのIDEのように,タブの高さを少し高くして,タブに曲線が使われるようにしてみる。タブの高さの調整はsetTabHeightメソッドを使用し,タブの描画に曲線が使われるようにするためにsetSimpleメソッドを使用する。

  tabFolder.setTabHeight(25);
  tabFolder.setSimple(false);

CTabFolder3.gif

更に,選択されているタブの文字色と背景色を付けてみる。選択されているタブの文字色の変更にはsetSelectionForegroundメソッドを,選択されているタブの背景色の変更にはsetSelectionBackgroundメソッドを使用する。特にsetSelectionBackgroundメソッドでは,複数の白を指定してグラデーションさせることできるようになっている。

  Display display = Display.getCurrent();
  Color titleForeColor =
    display.getSystemColor(SWT.COLOR_TITLE_FOREGROUND);
  Color titleBackColor1 =
    display.getSystemColor(SWT.COLOR_TITLE_BACKGROUND);
  Color titleBackColor2 =
    display.getSystemColor(SWT.COLOR_TITLE_BACKGROUND_GRADIENT);

  tabFolder.setSelectionForeground(titleForeColor);
  tabFolder.setSelectionBackground(
    new Color[] {titleBackColor1, titleBackColor2},
    new int[] {100},
    true
  );

CTabFolder4.gif

DisplayオブジェクトのgetSystemColorメソッドを使って,OSで割り当てられている色(上記ではタイトルで使用される目的で用意されているもの)のColorオブジェクトを取得している。そして,setSelectionForegroundメソッドを使って文字色を,setSelectionBackgroundメソッドで背景色をセットしている。setSelectionBackgroundメソッドの引数には,グラデーションさせたい色のColorオブジェクトの配列と,色の占める割合の数値(%),グラデーションの方向(縦にしたければtrue)を渡す。グラデーションではなく単色でいいのであれば,setSelectionBackground(Color) メソッドを使えばよい。

各タブへのコンポーネントの追加は,CTabItemオブジェクトのsetControlメソッドを使用する。

  Label hello = new Label(tabFolder, SWT.NONE);
  hello.setText("こんにちは!");
  taroTab.setControl(hello);

CTabFolder5.gif

CTabItemオブジェクトのsetImageメソッドでアイコンも指定すれば,よりかっこいいタブペインになるだろう。

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

2004.10.04

アクティブなエディタの取得方法

ご要望があったので,今回はアクティブなエディタのオブジェクトの取得方法について紹介する。

エディタ,すなわちIEditorPartオブジェクトは,ワークベンチページ(IWorkbenchPageオブジェクト)が持っている。そして,ワークベンチページは,ワークベンチウィンドウ(IWorkbenchWindowオブジェクト)が持っている。これを踏まえて各インタフェースを見ていくと,ずばりアクティブなエディタを取得するメソッドが提供されているのがわかると思う。

  IWorkbench workbench = PlatformUI.getWorkbench();
  IWorkbenchWindow workbenchWindow = workbench.getActiveWorkbenchWindow();
  IWorkbenchPage workbenchPage = workbenchWindow.getActivePage();
  IEditorPart editorPart = workbenchPage.getActiveEditor();

エディタやビューなどはPlatformUI.getWorkbench()から,ファイルやフォルダなどのリソースはResourcesPlugin.getWorkspace()から辿っていくと探し当てることができるだろう。

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

2004.09.18

テキストのフォーマット(3.戦略の呼び出し)

前々回の「テキストのフォーマット(1.戦略の作成)」と前回の「テキストのフォーマット(2.戦略の登録)」で,フォーマット機能の追加は完了した。しかし,このままではフォーマット処理を呼び出すことができず,宝の持ち腐れになってしまう。つまり,ユーザがフォーマット機能を呼び出すことができるように,UIを準備しなければならない。ただし,テキストエディタに対してフォーマット機能の実行を呼び出すためには,ちょっと工夫が必要である。エディタクラスのformatメソッドを呼ぶだけ,なんて簡単なものではない。

テキストのコピーやペースト,アンドゥや文字列の選択などのテキストエディタに対する操作は,ITextOperationTargetインタフェースを通して行う。つまり,テキストエディタに対する指令は,ITextOperationTargetインタフェースに規定されているdoOperationメソッドを使用するように統一されているのだ。フォーマット機能の実行についてもITextOperationTargetインタフェースを通じて行う。ITextOperationTargetオブジェクトの取得については,テキストエディタを規定しているITextEditorインタフェースのgetAdapterメソッドを使うことによって取得することができる

  IEditorPart editorPart = ...;

  ITextEditor textEditor = (ITextEditor)editorPart;
  ITextOperationTarget target =
    (ITextOperationTarget)textEditor.getAdapter(ITextOperationTarget.class);
  target.doOperation(ISourceViewer.FORMAT);

フォーマット機能を実行するための定数として,ISourceViewerインタフェースにFORMAT定数が定義されている。つまり,ITextOperationTargetオブジェクトのdoOperationメソッドにISourceViewer.FORMATを渡すことにより,フォーマット機能が実行される。結果として,テキストエディタの内容は以下の図のように変更される。

format2.gif

上記のフォーマット機能の呼び出し処理は,エディタのコンテキストメニューのアクションクラスなどに記述して使用すると良いだろう。

以上がフォーマット機能の実装方法である。実際の整形処理については開発者自身が記述することなので,腕の見せ所といったところだろうか。

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

テキストのフォーマット(2.戦略の登録)

さて,前回の「テキストのフォーマット(1.戦略の作成)」では,各区画用にフォーマット処理クラスを作成した。しかし,テキストエディタが直接これらのオブジェクトを利用するわけではない。ではどうなっているかというと,JFaceが提供しているテキストエディタフレームワークは,フォーマット処理をIContentFormatterオブジェクトに依頼するIContentFormatterインタフェースには,指定されたコンテントタイプに対応するIFormattingStrategyオブジェクトを返すgetFormattingStrategyメソッドが規定されている。つまり,フレームワークからフォーマット処理を依頼されたIContentFormatterオブジェクトは,それに対して登録されたIFormattingStrategyオブジェクトをコンテントタイプに応じて利用することによりフォーマット処理を遂行していくのである。

IFormattingStrategyインタフェースの実装クラスは前回自作したが,IContentFormatterインタフェースに関してはEclipseがデフォルト実装クラスとしてContentFormatterクラスを提供してくれている。通常はContentFormatterクラスを使用すればよい。

IContentFormatterオブジェクトをエディタに提供する処理は,SourceViewerConfigurationクラスのサブクラスで行う。SourceViewerConfigurationクラスのサブクラスにてgetContentFormatterメソッドをオーバーライドし,その中でContentFormatterオブジェクトを生成して返却するようにする。

  public class MySourceViewerConfiguration extends SourceViewerConfiguration {
    ...
    public IContentFormatter getContentFormatter(ISourceViewer sourceViewer) {
      ContentFormatter formatter = new ContentFormatter();
      formatter.setFormattingStrategy(
        new MetaPartFormattingStrategy(), "META_PART");
      formatter.setFormattingStrategy(
        new ContentPartFormattingStrategy(), "CONTENT_PART");
      return formatter;
    }
  }

ContentFormatterオブジェクトを生成し,setFormattingStrategyメソッドを使ってIFormattingStrategyオブジェクトを登録する。setFormattingStrategyメソッドには,第1引数にIFormattingStrategyオブジェクトを,第2引数に対象とする区画の識別子を指定する。そしてIFormattingStrategyオブジェクトが登録されたContentFormatterオブジェクトをgetContentFormatterメソッドの結果として返却する。もちろん上記のSourceViewerConfigurationクラスのサブクラスのインスタンスをAbstractTextEditorクラスのsetSouceViewerConfigurationメソッドを使って登録することも忘れずに。

以上でフォーマット機能の追加は完了である。しかし,このままではフォーマット処理を呼び出すことができず,宝の持ち腐れになってしまう。次回「テキストのフォーマット(3.戦略の呼び出し)」はフォーマット機能を呼び出すための処理について紹介する。

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

テキストのフォーマット(1.戦略の作成)

Javaエディタには,インデントや空白の調整を行うフォーマット機能が備わっていて,誰が書いたコードに関しても一定の規律に従ってソースコードを整えることができる。Eclipseでは,さまざまな言語に対応したエディタを自作するための強力なフレームワークが提供されているが,このフォーマット機能もその一つである。今回は自作したエディタに対して,入力された内容を整形するフォーマット機能の作成方法を紹介する。

JFaceが提供しているテキストエディタフレームワークでは,入力されたテキストについて,あるルールに従って複数の区画(パーティション)に分割するのが普通である。例えばJavaエディタでは,Javadocコメントの部分と実際のコードの部分に区画が分けられている。特にプログラム言語を扱う場合,一つのソースコード中でもいくつかの分野(コメント文やマクロ,関数など)が混在しているので,インデントや空白,改行位置などのテキストの整形を行いたい場合,その分野ごとに異なる整形を施したくなる。そこで,テキストエディタフレームワークでは,テキストの区画ごとに異なる整形戦略(Strategy)を割り当てられるようになっている

このフォーマット機能は,具体例を示しながらの方がわかりやすいだろう。ここでは,META_PARTおよびCONTENT_PARTという2つの区画が存在するエディタを題材とする。META_PART区画は{$BEGIN_META}から{$END_META}で囲まれた部分,CONTENT_PART区画は{$BEGIN_CONTENT}から{$END_CONTENT}で囲まれた部分とし,それぞれ異なる背景色を定義してある。入力した状況を下図に示す。

format1.gif

META_PART区画については英小文字をすべて英大文字に変更,CONTENT_PART区画については句読点「。」で改行するようにフォーマット機能を仕上げる。

では,フォーマット処理を行うクラスの作成方法から紹介する。フォーマット処理はIFormattingStrategyインタフェースに規定されている。基本的には,IFormattingStrategyインタフェースの実装クラスでformatメソッドに整形処理を実装すればよい

  public class MetaPartFormattingStrategy implements IFormattingStrategy {
    public void formatterStarts(String initialIndentation) {}
    public String format(String content, boolean isLineStart, String indentation, int[] positions) {
      return content.toUpperCase();
    }
    public void formatterStops() {}
  }

フォーマット処理の直前にformatterStartsメソッドが,フォーマット処理の直後にformatterStopsメソッドがそれぞれ呼び出される。この際,formatterStartsメソッドの引数にフォーマット処理対象の文字列の中で最初の行に使われているインデント文字列(タブあるいは1つ以上の空白)が渡される。

そして実際のフォーマット処理を記述するのがformatメソッドである。基本的に,引数のcontent変数にフォーマット対象の文字列が渡されてくるので,それに対してフォーマット処理を行い,その結果の文字列を返却すればよい。フォーマット処理の呼び出し時点で,もし区画内で文字列が選択されていない場合は区画全体の文字列が,もし区画内で文字列が選択されていた場合は選択文字列がcontent引数に渡されてくる。上記の例では,渡された文字列に対してtoUpperCaseメソッドを使用して英小文字を英大文字に変換し,その結果の文字列をフォーマット処理後の文字列として返却している。

その他の引数として,isLineStart引数は処理対象の文字列の最初の行が0桁目から開始されているかどうかが,indentation引数は処理対象の文字列の最初の行に使用されているインデント文字列が渡される。最後のpositions引数は,Javadocには更新される文字の位置と記述されているが,よくわからず(誰か教えてください)。

上記のコードはMETA_CONTENT区画に対してのフォーマット処理クラスである。CONTENT_PART区画に対してのフォーマット処理クラスについても紹介しておこう。

  public class ContentPartFormattingStrategy implements IFormattingStrategy {
    public void formatterStarts(String initialIndentation) {}
    public String format(String content, boolean isLineStart, String indentation, int[] positions) {
      String temp = "", result = "";
      StringTokenizer st = new StringTokenizer(content, "\r\n");
      while(st.hasMoreTokens())
        temp += st.nextToken();
      BreakIterator iterator = BreakIterator.getSentenceInstance();
      iterator.setText(temp);
      int start = iterator.first();
      for (int end = iterator.next(); end != BreakIterator.DONE; start = end, end = iterator.next())
        result += (start == 0) ? temp.substring(start, end) : "\r\n" + temp.substring(start, end);
      if (result.startsWith("{$BEGIN_CONTENT}"))
        result = result.substring(0, 16) + "\r\n" + result.substring(16);
      return result;
    }
    public void formatterStops() {}
  }

BreakIteratorクラスを使って句読点を区切りとして文字列を分割し,その間に改行を付与することによって整形処理を行っている。

Eclipseでは,大抵のインタフェースに対応して提供されているデフォルト実装クラスを使えば,プラグイン開発者は自分で実装クラスを作成しなくても済んでしまう場面が多い。しかし,IFormattingStrategyインタフェースについては,デフォルト実装クラスは特に用意されていない(JDTにはもちろん実装が存在する)。汎用的なフォーマット処理なんぞ存在しない,ということだろう。しかし,ブロックの開始文字と終了文字を与えることによってインデント整形をやってくれる汎用実装クラスくらいは用意されてても良い気がする。。。

さて,これで各区画に対して行うフォーマット処理を実装することができた。これを使用可能にするためには,あといくつかのステップを踏む必要がある。次回「テキストのフォーマット(2.戦略の登録)」は,IFormattingStrategyオブジェクトをフレームワークに登録する方法を紹介する。

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

2004.09.12

テキストエディタのコンテキストメニューID

ビューやエディタのコンテキストメニュー(右クリックで表示されるメニュー)にアクションを追加するときに,そのコンテキストメニューのIDを知る必要がある。ビューに関しては「特定ビューへのコンテキストメニュー項目の追加」で既に取り上げている。ではエディタの場合はどうなのか,ということで今回の話題としてみよう。

Eclipseの多くのテキストエディタはAbstractTextEditorクラスを継承しているが,テキストエディタのコンテキストメニューのIDはAbstractTextEditorクラスで決定される(もちろんAbstractTextEditorクラスを継承していないエディタは対象外)。特に何も指定がなければ,テキストエディタのコンテキストメニューのIDは,

  エディタID.EditorContext

となる。例えば「yoichiro.eclipse.editor.HogeEditor」というIDのエディタがあったとしたら,そのエディタのコンテキストメニューのIDは「yoichiro.eclipse.editor.HogeEditor.EditorContext」となる。また,Eclipseに標準で搭載されるテキストエディタとJavaエディタに関しては,

  ・デフォルトテキストエディタ(TextEditorクラス)
    org.eclipse.ui.DefaultTextEditor.EditorContext

  ・Javaエディタ(CompilationUnitEditorクラス)
    org.eclipse.jdt.ui.CompilationUnitEditor.EditorContext

となる。何かのテキストエディタのコンテキストメニューに対してアクションを追加したければ,対象のエディタのIDを知ることができれば実現できる。

さて,上記とは別に,Javaエディタのコンテキストメニューにアクションを追加する際,コンテキストメニューのIDに#CompilationUnitEditorContextという文字列を指定している記述をよく見受けると思う。本ブログでも「Javaエディタのコンテキストメニュー」にて#CompilationUnitEditorContextについて紹介している。Javaエディタの場合は,上記の「org.eclipse.jdt.ui.CompilationUnitEditor.EditorContext」でもいいし,「#CompilationUnitEditorContext」でも良い。さて,#CompilationUnitEditorContextはどこから来たのだろうか?

AbstractTextEditorクラスでは,コンテキストメニューのIDをサブクラスにて自由に決めることができる。AbstractTextEditorクラスのsetEditorContextMenuIdメソッドにコンテキストメニューとしたい文字列を指定することで,「エディタID.EditorContext」とは別にコンテキストメニューが設定される

上記で紹介したデフォルトテキストエディタとJavaエディタでは,setEditorContextMenuIdメソッドを使って独自にコンテキストメニューのIDを設定している

  ・デフォルトテキストエディタ(TextEditorクラス)
    #TextEditorContext
      (TextEditorクラスのinitializeEditorメソッド内で設定)

  ・Javaエディタ(CompilationUnitEditorクラス)
    #CompilationUnitEditorContext
      (CompilationUnitEditorクラスのコンストラクタ内)

setEditorContextMenuIdメソッドを使って設定されたコンテキストメニューのIDは,Javadocに記載されるなりしないと,ソースを見ることなしに知ることはできない。TextEditorクラスのAPIリファレンスにはちゃんと記載されているが,Javaエディタに関してはCompilationUnitEditorクラスがinternalなこともあり,特にドキュメントに記載がない。

AbstractTextEditorクラスを継承してテキストエディタを自作して,setEditorContextMenuIdメソッドを使って独自にコンテキストメニューのIDを設定したときは,ちゃんとAPIリファレンスなどにコンテキストメニューのIDを記載すると,開発者に優しいエディタになるだろう。

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

2004.09.09

MarkerUtilitiesクラス

リソースへのマーキング」で,マーカーの作成方法を紹介した。これに関して,例えば,

  IMarker marker = ...;
  marker.setAttribute(IMarker.CHAR_START, new Integer(20));
  marker.setAttribute(IMarker.CHAR_END, new Integer(25));

としてしまうと,setAttributeメソッドの呼び出しのたびにリソースの変更を監視しているリスナーに通知が行われてしまう(上記では2回連続して呼び出されてしまう)ので,あまりよろしくない。その代わりに,

  Map map = new HashMap();
  map.put(IMarker.CHAR_START, new Integer(20));
  map.put(IMarker.CHAR_END, new Integer(25));
  marker.setAttributes(map);

というようにして,属性の設定時には通知が一回で済むようにしましょう,ということを「リソースへのマーキング」で取り上げた。

しかし,マーカーの作成は,マーカー自身の作成と属性の設定が同時に行われることがほとんどである。マーカーを作成した時点で,リソースの変更を監視しているリスナーに通知が行われてしまうために,

  IResource resource = ...;
  IMarker marker = resource.createMarker(IMarker.TASK);
  Map map = ...;
  marker.setAttributes(map);

としてしまうと,リスナーへの通知が2度行われてしまう。たかがリスナーへの通知が2回行われるだけだと簡単に思うなかれ,処理の内容的に問題がなくても速度的な問題が発生してしまう可能性は十分にある。

では,それを避けるためにはどうすればいいかというと,Eclipseでは便利なMarkerUtilitiesクラスを提供してくれている。MarkerUtilitiesクラスのcreateMarkerメソッドを使用することで,マーカーの作成および属性のセットを一まとめにすることができ,リスナーへの通知も1回のみになる

  IResource resource = ...;
  Map map = new HashMap();
  MarkerUtilities.setCharStart(map, 20);
  MarkerUtilities.setCharEnd(map, 25);
  MarkerUtiltites.createMarker(resource, map, IMarker.TEXT);

属性を格納するコレクションは自分で確保するのだが,コレクションへの属性値の追加は「標準マーカーの属性」で紹介したものについては便利メソッドが提供されている(上記コード中のsetCharStartメソッドなど)。リソースオブジェクト,属性が格納されたコレクション,そしてマーカーの種別を表すID文字列をMarkerUtilitiesクラスのcreateMarkerメソッドに渡すことでマーカーが作成され,その結果のリソースの変更通知は1回で抑えられる。

ここで注意すべき点は,MarkerUtilitiesクラスのcreateMarkerメソッドは,作成したIMarkerオブジェクトを返却してくれないということだ。例えば「テキストエディタで指定位置へジャンプ」をしようとした際,マーカーをMarkerUtilitiesクラスを使って作成してもIMarkerオブジェクトを得ることができないために,gotoMarkerメソッドが使えない。その場合は複数回のリスナーへの通知を抑えることはあきらめるか,MarkerUtilities#createMarker()の内容を自分で記述するしかない。ちょっと残念である。

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

2004.09.08

テキストエディタで指定位置へジャンプ

テキストエディタ上で「Ctrl+L」キーを押すと出てくる指定行ジャンプ機能。これをプラグインのプログラムから行う方法を紹介する。

テキストエディタ上のある地点(位置または行)に移動させるには,まず移動したい場所にマーカーをつける。そして,そのマーカーめがけてジャンプする。以下が,そのコードの例である。

  IEditorPart editorPart = ...;

  IEditorPart editorPart = editorPart.getEditorInput();
  IResource resource = (IResource)editorPart.getAdapter(IResource.class);

  Map attributes = new HashMap();
  attributes.put(IMarker.CHAR_START, new Integer(50));
  attributes.put(IMarker.CHAR_END, new Integer(50));
  IMarker marker = resource.createMarker(IMarker.TEXT));
  marker.setAttributes(attributes);

  IDE.gotoMarker(editorPart, marker);

  marker.delete();

処理対象のIEditorPartオブジェクトからIEditorInputオブジェクトを取得し,IEditorInputオブジェクトから実際のリソース(IResource)オブジェクトをgetAdapterメソッドを使って取得する。通常はIFileオブジェクトが返却されるだろう。

IResourceオブジェクトが取得できたら,「リソースへのマーキング」で紹介したcreateMarkerメソッドを使って,マーカーを作成する。この際,マーカーの種類として「標準マーカー」で紹介したIMarker.TEXTを指定する。そしてマーカーの属性として,IMarker.CHAR_STARTおよびIMarker.CHAR_ENDにエディタ上でジャンプしたい位置を指定する。各属性値に同じ値を指定することで,エディタ内の文字列の先頭文字からの指定位置にカーソルが移動される。異なる値を指定したときは,その範囲の文字列が選択されて,カーソルが選択文字列の直後に位置される。

マーカーが作成できたら,IDEクラスのgotoMarkerメソッドにIEditorPartオブジェクトとIMarkerオブジェクトを渡すことで,指定された場所にジャンプされる。gotoMarkerメソッドの実行後は,マーカーは必要ないものになるので,deleteメソッドを使用してマーカーを削除しておく

上記は指定位置へのジャンプだったが,指定行へのジャンプの場合は,マーカーの属性としてIMarker.LINE_NUMBERを使用する

  Map attributes = new HashMap();
  attributes.put(IMarker.LINE_NUMBER, new Integer(5));

これにより,5行目の文字列が選択され,6行目の先頭にカーソルが移動されるようになる。

実は上記のジャンプ処理は,AbstractDecoratedTextEditorクラスを継承しているテキストエディタに関して適用される動きであり,エディタであれば何でもそうなるわけではない。EclipseのデフォルトテキストエディタであるTextEditorクラスや,Javaソースコード編集用のエディタであるCompilationUnitEditorクラスは,このAbstractDecoratedTextEditorクラスを継承しているので,上記の方法が使用できる。

マーカーへのジャンプ処理は,IGotoMarkerインタフェースによって規定されている。IDEクラスのgotoMarkerメソッドは,指定されたIEditorPartオブジェクトがIGotoMarkerインタフェースを実装しているかをチェックし,実装していればIEditorPartオブジェクトのgotoMarkerメソッドを呼び出して,ジャンプ処理を行わせている。つまり,エディタがIGotoMarkerインタフェースのgotoMarkerメソッドをどのように実装しているかによって,ジャンプ処理の動きが変わってくるということになる。

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

2004.09.03

エディタを開く Part2

もう3ヶ月近く前になるが,2004年5月24日に「エディタを開く」という記事を書いた。あるファイルをプラグインのプログラム内からエディタを使って開くための方法を紹介したのだが,当時はEclipseのバージョン2.1.3を対象にしていた。6月に入ってEclipseのバージョン3.0が公開されたわけだが,実はEclipse3.0になってからいくつかのAPIが廃止あるいは非互換になっている。どんなAPIが対象化というと,Eclipseのヘルプの中の,

  「Platform Plug-in Developer Guide」→「3.0 Plug-in Migration Guide」→「Incompatibilities」

を見れば非互換になったものを把握することができる。これを見てみると,「エディタを開く」で紹介したIWorkbenchPage#openEditor(IFile)メソッドが,見事に廃止の対象になっていた。残念ながら,完全に「エディタを開く」はEclipse3.0以降では参考にならない記事になってしまったので,ここでエディタを開くための新しい方法を紹介する。

Eclipse3.0からは,IWorkbenchPageインタフェースのopenEditorメソッドについて,引数にIFileオブジェクトを取るものが廃止され,IEditorInputオブジェクトおよびエディタIDの文字列を引数に取るメソッドのみに限定された。その理由は「IWorkbenchPartインタフェースは,一般的なワークベンチの中での定義なのにも関わらず,IFileなど特定のリソースに依存してしまっているので,それに関連するメソッドを削除した」ということらしい。ごもっともである。

さて,ではIFileオブジェクトを使って直接エディタを開くことはできなくなってしまったかというと,そうではない。Eclipse3.0からIDEクラスが追加され,IDEクラスにIFileオブジェクトを使ってエディタを開くためのメソッドが提供されている。

  IFile file = ...;

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

  IEditorPart editorPart = IDE.openEditor(page, file);

アクティブなIWorkbenchPartオブジェクトを取得するのは「エディタを開く」と同じ。IWorkbenchPartオブジェクトが取得できれば,あとはIDEクラスのクラスメソッドであるopenEditorメソッドに,IWorkbenchPartオブジェクトとIFileオブジェクトを渡せば,エディタが開かれ,その結果のIEditorPartオブジェクトが返却される

このIDEクラスのopenEditorメソッド,結局はIWorkbenchPageインタフェースのopenEditorメソッドが利用されている。つまり,IFileオブジェクトを持つIFileEditorInputオブジェクトを生成し,さらにIFileオブジェクトで表されるファイルの名前から開くべきエディタの種類(エディタID)を決定し,それらをIWorkbenchPageインタフェースのopenEditorメソッドに渡してエディタを開く,という処理である。どのエディタで開くか(エディタIDの取得)は,IDEクラスのgetEditorDescriptorメソッドに判断ロジックが記述されている。その判断ロジックは,「エディタを開く」で紹介したものとほぼ同じである。

このようなインタフェース間やクラス間の依存性の排除などの見直しがされているあたりに,Eclipseの設計者のこだわりというか理想の高さを感じる。

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

2004.08.27

テキストエディタ上のカーソル位置の取得方法

テキストエディタ上である機能が呼び出された時点でのカーソル(キャレット)位置の取得を行いたいときがある。カーソル位置から現在行を取得したり,カーソル位置がメソッド内かどうかを調べたり,などが例として考えられる。カーソル位置の取得なんて簡単にできそうな気がするが,実はそこには高い壁が存在している

AbstractTextEditorクラスを基底クラスとするテキストエディタは,ViewのコンポーネントとしてSWTのStyledTextクラスを利用している。このStyledTextクラスのgetCaretOffsetメソッドを呼び出すことで,カーソル位置を取得することができる。「なんだ,それならStyleTextオブジェクトを取得できればいいだけだから簡単じゃん」と思ってしまうが,ここで落とし穴が待っている。

StyledTextオブジェクトを取得するためには,ISourceViewerインタフェース(実際の定義は親のITextViewerインタフェース)のgetTextWidgetメソッドを使用すれば良いのだが,ISourceViewerオブジェクトを取得するためのAbstractTextEditorクラスのgetSourceViewerメソッドのアクセス修飾子がprotectedなのである。つまり,テキストエディタが持つStyledTextオブジェクトの取得は,AbstractTextEditorクラスのサブクラスからのみに限定されていることになる。これでは,Eclipse既存のテキストエディタ(TextEditorやJDTのCompilationUnitEditorなど)の外部(アクション代理オブジェクトなど)からはStyledTextオブジェクトを得ることができない。結果として,AbstractTextEditorクラスのサブクラスを作ってテキストエディタを自作しない限り,カーソル位置を取得することができないことになる。

もちろんAbstractTextEditorクラスのサブクラスを自作すれば,カーソル位置を取得することが可能になる。その方法は以下のようなコードになる。

  ISourceViewer sourceViewer = getSourceViewer();
  StyledText styledText = sourceViewer.getTextWidget();
  int widgetOffset = styledText.getCaretOffset();
  int modelOffset = widgetOffset2ModelOffset(sourceViewer, widgetOffset);

getSourceViewerメソッドを使ってISourceViewerオブジェクトを取得,その後ISourceViewerオブジェクトのgetTextWidgetメソッドを呼び出してStyledTextオブジェクトを取得する。そしてStyledTextオブジェクトのgetCaretOffsetメソッドを使って,カーソル位置を取得する。ただし,getCaretOffsetメソッドで取得したカーソル位置は,あくまで画面上の見かけの位置であり,テキスト全体から見た位置ではない。Eclipseのバージョン3から搭載されたFolding機能によってテキストのある部分が非表示になることがあるため,StyledTextオブジェクトのgetCaretOffsetメソッドの結果はFolding機能によって非表示になったテキストがすっ飛ばされた位置なのである。IDocumentインタフェースで表されるテキストのモデル(つまりテキスト全体)からみた現在のカーソル位置を得るために,widgetOffset2ModelOffsetメソッドが用意されている。widgetOffset2ModelOffsetメソッドにStyledTextオブジェクトから得たカーソル位置を渡すことにより,テキスト全体から見た位置を計算して返却してくれるので,その結果をカーソル位置として使用すればよい。

さて,上記の方法はもちろんAbstractTextEditorクラスのサブクラスからしか利用できない。では方法がないのかというとそんなことはなくて,テキストの選択情報を取得するための機構を流用することで(なんちゃってではあるが)カーソル位置の取得を実現できる。テキストが選択されていない状態で選択状況の取得を行うと,選択開始位置から長さ0の選択,という情報が得られるので,その選択開始位置をカーソル位置とすることができる。もちろんこの位置はテキスト全体から見たモデル上の位置である。

  IEditorPart editorPart = ...;

  ITextEditor textEditor = (ITextEditor)editorPart;
  ISelectionProvider selectionProvider = textEditor.getSelectionProvider();
  ISelection selection = selectionProvider.getSelection();
  ITextSelection textSelection = (ITextSelection)selection;
  int offset = textSelection.getOffset();

まず,エディタを表すIEditorPartオブジェクトをITextEditor型にキャストする。そしてITextEditorオブジェクトのgetSelectionProviderメソッドを使ってISelectionProviderオブジェクトを取得する。さらに,ISelectionProviderオブジェクトのgetSelectionメソッドを使用してISelectionオブジェクトを取得し,それをITextSelection型にキャストする。ITextSelectionオブジェクトには選択の開始位置および選択文字数が格納されているので,getOffsetメソッドを使って選択開始位置を取得する。結果として,この選択開始位置をカーソル位置とする。

ただし,この方法では完全にはカーソル位置を取得することができない。エディタ上で文字列の選択を[Shift]+[→]キーで行った場合,選択後のカーソルの位置は選択文字列の最後に位置する。つまり,カーソル位置は本当は「textSelection.getOffset() + textSelection.getLength()」なのだが,選択行為を[→]キーで行ったのか[←]キーで行ったのかを得る手段がないために,選択開始位置にカーソルがあるのか選択終了位置にカーソルがあるのか区別がつかない。よって,上記のように選択開始位置をカーソル位置とする,という固定的な方法にするしかないのである。

ワープロじゃなくてテキストエディタなんだから,カーソルの現在位置の取得メソッドくらい,publicで用意されていてもいい気がするけど,何か理由があるのだろうか。。。

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

2004.08.24

エディタからICompilationUnitオブジェクトを取得する方法

JDTに付属されているJavaのソースコードを編集するためのエディタに対して機能拡張を行う際に,そのエディタで編集されているソースコードのJavaモデル表現であるICompilationUnitオブジェクトを取得したいことが多々ある。今回は,エディタからこのICompilationUnitオブジェクトを取得するための方法を紹介する。

エディタからICompilationUnitオブジェクトを得る方法は2つ存在する。

(1) ファイルリソースからICompilationUnitオブジェクトを生成する
(2) エディタ入力に実装されているIAdaptableインタフェースを利用する

まずは(1)から紹介する。これは,JDTで提供されているJavaCoreクラスのcreateCompilationUnitFromメソッドを利用する方法である。

  IEditorPart editorPart = ...;

  IFileEditorInput fileEditorInput = (IFileEditorInput)editorPart.getEditorInput();
  IFile file = fileEditorInput.getFile();
  ICompilationUnit iCompilationUnit = JavaCore.createCompilationUnitFrom(file);

最初に,エディタを表すIEditorPartオブジェクトのgetEditorInputメソッドを呼び出して,IEditorInputオブジェクトを取得する。そして,取得したIEditorInputオブジェクトをIFileEditorInput型でキャストする。もちろん本来はキャストできるかどうかチェックする必要があるが,上記では省略している。そしてIFileEditorInputオブジェクトのgetFileメソッドを呼び出して,IFileオブジェクトを取得する。あとは,JavaCoreクラスのクラスメソッドであるcreateCompilationUnitFromメソッドにIFileオブジェクトを渡すことで,ICompilationUnitオブジェクトを得ることができる。

次の(2)の方法は,知っていないと思いもつかない方法である。

  IEditorPart editorPart = ...;

  IEditorInput editorInput = editorPart.getEditorInput();
  ICompilationUnit iCompilationUnit =
    (ICompilationUnit)editorInput.getAdapter(IJavaElement.class);

まず,IEditorPartオブジェクトからIEditorInputオブジェクトを取得する。そして,IEditorInputオブジェクトのgetAdapterメソッドにJavaモデルの要素であることを示すIJavaElementインタフェースのクラスオブジェクトを渡すことで,エディタで開いているソースコードのJavaモデル表現であるICompilationUnitオブジェクトが得られる。

実際にソースコードを追ったわけではないのだが,たぶんJavaエディタでソースコードのファイルが開かれる際に,そのIEditorInputオブジェクトとICompilationUnitオブジェクトをAdapterManagerオブジェクトに登録しているのではないかと思われる。

どちらの方法を使っても得られる結果は同じなので,自由に選択してかまわない。ただ,(1)の方法だとIFileEditorInput型にダウンキャストしている点が,(2)の方法に比べてちょっと問題ありかな,という気がする。

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

2004.08.06

文字列の移動,コピーを行うテキスト編集クラス

前回の「ドキュメントの操作を簡単にするテキスト編集クラス」では,ドキュメントのテキストに対して挿入,削除,置換処理を手軽に行うための方法を紹介した。今回は,挿入などよりもちょっと高級な,文字列の移動,コピーをやってくれるテキスト編集クラスを紹介する。

まずは移動から。文字列の移動は,2つのテキスト編集クラスを組み合わせて行う。つまり,文字列の移動を噛み砕くと「○文字目から○文字分を,○文字目に移動する」っていう表現になるが,この前半の「○文字目から○文字分を」を担当するのがMoveSourceEditクラス,後半の「○文字目に移動する」を担当するのがMoveTargetEditクラスである。

早速使い方を見てみよう。例として,「1234567890」という文字列の最初の「123」を5文字目の後に移動する(結果として「4512367890」とする)処理を考えてみる。

  IDocument document = ...; // "1234567890"

  MoveSourceEdit sourceEdit = new MoveSourceEdit(0, 3);
  MoveTargetEdit targetEdit = new MoveTargetEdit(5);
  sourceEdit.setTargetEdit(targetEdit);

  MultiTextEdit multiTextEdit = new MultiTextEdit();
  multiEdit.addChild(sourceEdit);
  multiEdit.addChild(targetEdit);
  multiTextEdit.apply(document); // "4512367890"

まず,移動元の文字列を特定するためのMoveSourceEditオブジェクトを生成する。この際,第1引数に何文字目の文字列なのか,第2引数には何文字分なのかを指定する。上記では「0文字目から3文字分("123")」を指定している。次に,移動先を特定するためのMoveTargetEditオブジェクトを生成する。引数には,何文字目に挿入するかを指定する。移動先の位置は,MoveSourceEditオブジェクトで特定される文字列の長さなどを気にする必要はなく,移動前の文字列における位置でかまわない。上記では5文字目を指定している。

そして,MoveSourceEditオブジェクトとMoveTargetEditオブジェクトを関連付ける。上記では,MoveSourceEditオブジェクトのsetTargetEditメソッドにMoveTargetEditオブジェクトを渡すことで関連付けを行っている。これの代わりに「targetEdit.setSourceEdit(sourceEdit);」というようにしても良い。

あとは前回の「ドキュメントの操作を簡単にするテキスト編集クラス」の時のように,MultiTextEditオブジェクトにMoveSourceEditオブジェクトとMoveTargetEditオブジェクトをaddCihldメソッドを使って登録し,applyメソッドを使ってIDocumentオブジェクトにテキスト編集を適用する。この際注意することは,setTargetEditメソッドまたはsetSourceEditメソッドを使って関連付けをちゃんとしておくことと(これを怠ると例外が発生する),MultiTextEditオブジェクトにMoveSourceEditオブジェクトとMoveTargetEditオブジェクトの両方をちゃんと登録しておくことである(片方だけだと移動処理が中途半端に終わってしまう)。

上記で移動の話は終わり。次にコピーだが,やり方は基本的に移動のときと同じであるが,使用するクラスが違ってくる。コピーの場合は,コピー元の文字列を特定するためにCopySourceEditクラスを,コピー先の位置を特定するためにCopyTargetEditクラスを使用する。

例として「1234567890」の最初の「123」を5文字目の後にコピーして「1234512367890」とする処理を考えると,以下のようなコードで実現できる。

  IDocument document = ...; // "1234567890"

  CopySourceEdit sourceEdit = new CopySourceEdit(0, 3);
  CopyTargetEdit targetEdit = new CopyTargetEdit(5);
  sourceEdit.setTargetEdit(targetEdit);

  MultiTextEdit multiTextEdit = new MultiTextEdit();
  multiEdit.addChild(sourceEdit);
  multiEdit.addChild(targetEdit);
  multiTextEdit.apply(document); // "1234512367890"

良く見ると,移動のときのサンプルとほぼ同じで,使用しているクラスを変えただけである。setSourceEditメソッドとsetTargetEditメソッドの話や,使用時の注意点などもMove~Editクラスのときと同じである。

あるテキストに対して,次々と編集を一気に加えていく処理を行うときは,「ドキュメントの操作を簡単にするテキスト編集クラス」で紹介したInsertEdit,DeleteEdit,ReplaceEditクラスや,今回取り上げたMove~Edit,Copy~Editクラスを組み合わせて実現することで,パニックを起こさずに処理を記述していけることだろう。

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

2004.08.05

ドキュメントの操作を簡単にするテキスト編集クラス

一般的にエディタに対して機能を拡張するプラグインでは,IDocumentインタフェースで表されるドキュメント(テキスト)を操作することが求められる。すなわち,エディタ内のテキストに対して,文字列の挿入や削除,置換などを行わなければならない。もちろん,IDocumentインタフェースのreplaceメソッドを使えば,テキスト操作を全て行うことができるのだが,ちょっと考えただけでも恐ろしくなるほどreplaceメソッドだけではテキスト操作は難しい

例えば,「09012345678」という電話番号があったとして,これに「-」を2つ加えて「090-1234-5678」としたいとすると,単純に考えると「3文字目の後と,7文字目の後に『-』を挿入する」となる。そこで,

  IDocument document = ...; // 09012345678
  document.replace(3, 0, "-");
  document.replace(7, 0, "-");

と実行してみると,結果は「090-123-45678」となってしまう。1回目のreplaceメソッドの実行によって1文字挿入されてしまったために,挿入すべき位置がずれてしまうからである。もちろん挿入した文字数を考慮して2回目の挿入位置を考慮すればいい話だが,もっと複雑なテキスト編集を行う場合は,はっきりいって誰でも頭の中はパニックになるだろう。

そんな大変なテキスト編集を簡単にするために,Eclipseでは便利なTextEditクラス(とそのサブクラス群)を用意してくれているTextEditクラスの機構を使うことで,上記のような操作位置の計算を自動的に行ってくれる

TextEditクラスは抽象クラスであり,開発者が使うのはTextEditクラスのサブクラスである。以下が代表的なTextEditクラスのサブクラスである。

  ・InsertEdit - 指定位置への文字列の挿入
    ex) new InsertEdit(10, "hoge"); // 9文字目に"hoge"を挿入

  ・DeleteEdit - 指定位置から指定長さ分だけ文字列の削除
    ex) new DeleteEdit(10, 5); // 9文字目から5文字分を削除

  ・ReplaceEdit - 指定位置から指定長さ分の文字列の置換
    ex) new RplaceEdit(10, 5, "hoge"); // 9文字目から5文字分を"hoge"に置換

これらのクラスのインスタンスを複数組み合わせることにより,ドキュメントを操作できる。例えば,先の電話番号の例では,InsertEditオブジェクトを2つ生成すればよい。

  InsertEdit edit1 = new InsertEdit(3, "-");
  InsertEdit edit2 = new InsertEdit(7, "-");

2つ目の「-」を挿入するInsertEditオブジェクトの生成では,1つ目の挿入を気にすることなく,7文字目を指定している。

さて,この2つのテキスト編集オブジェクトをドキュメントに対して一気に適用させるために,MultiTextEditクラスを使用する

  MultiTextEdit multiTextEdit = new MultiTextEdit();
  multiTextEdit.addChild(edit1);
  multiTextEdit.addChild(edit2);

MultiTextEditオブジェクトを生成し,addChildメソッドを使って先ほど生成したInsertEditオブジェクトを登録する。もちろんaddChildメソッドにRemoveEditオブジェクトやReplaceオブジェクトを渡すことも可能である。

最後に,applyメソッドにIDocumentオブジェクトを渡すことで,ドキュメントにテキスト編集を適用させる

  multiTextEdit.apply(document);

これにより,各テキスト編集の位置を内部で自動計算しながら,ドキュメントの内容が変更され,めでたく「090-1234-5678」となる。このように,TextEditクラスのサブクラスを用いることによって,元のドキュメントの内容を基準としたテキスト編集を手軽に行うことができる。しかもテキストの編集内容が複雑になればなるほど,TextEditクラスの恩恵を感じることができるはず。お試しあれ。

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

2004.08.02

テキストバッファとエディタ

突然だが,今回はテキストバッファについてもう少し知識を深めることにしよう。

テキストバッファを使ったIDocumentオブジェクトの取得」で紹介したテキストバッファをプラグイン開発者が直接利用する機会は,そう滅多にないかもしれない。やはりエディタに表示されている内容に対しての操作が中心になるだろうし,裏側で自動的にテキストファイルの内容が変更されているというのを,あまり良く思わない人もいるだろう。ただし,Eclipseはこのテキストバッファ抜きで語ることはできなくなった。

org.eclipse.core.filebuffersプラグインは,実はEclipse3.0から提供されたものである。Eclipse3.0になってから,このテキストバッファはEclipse内で大活躍している。つまり,テキストを扱うエディタは,テキストバッファを利用するように新たに書き直されているのだ。そして,あるファイルを複数のエディタで同時に開いたときは,それぞれのエディタはテキストバッファを共有する。つまり,片方のエディタで行った編集作業が,即座にもう1つのエディタにも伝達される,ということがテキストバッファの採用により実現されているのである(バッファ関連のコールバックメソッドはIFileBufferListenerインタフェースに規定されている)。

org.eclipse.core.filebuffersプラグインにより,テキストバッファは一元管理されている。つまり,

  IPath targetFilePath = ...; // 対象のファイルのパス

  ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
  manager.connect(targetFilePath, null);
  ITextFileBuffer buffer = manager.getTextFileBuffer(targetFilePath);

という処理を行った際に,例えば既にtargetFilePathで示されるファイルが何らかのエディタで開かれていた場合は,既にそのエディタがテキストバッファを作成しているので,上記のgetTextFileBufferメソッドは新たにテキストバッファを作成するのではなく,エディタにより作成されたテキストバッファが返される。よって,

  IDocument document = buffer.getDocument();

としてIDocumentオブジェクトを取得し,そのIDocumentオブジェクトに対して文字列の挿入などを行ったとすると,もちろんエディタにも文字列の挿入が反映される。エディタが持つIDocumentオブジェクトと,テキストバッファから取得したIDocumentオブジェクトが,同一のテキストバッファを指し示している証拠である。

ちなみに,

  buffer.commit(null, true);

とした場合は,エディタで保存を行ったのと同じ効果が得られる。もちろん,エディタ上でも「*」マークが消されるので,エディタの内容(すなわちテキストバッファの内容)が保存されたことがわかるだろう。

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

2004.07.30

あるファイルがエディタで開かれているかを取得する方法

今回は,あるファイルが何らかのエディタで開かれているかどうかを取得する方法を紹介する。

早速コードを下記に示す。

  IFile targetFile = ...; // 対象のファイルのハンドル

  IEditorInput input = new FileEditorInput(targetFile);
  IWorkbench workbench = PlatformUI.getWorkbench();
  IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
  for (int i = 0; i < windows.length; i++) {
    IWorkbenchPage[] pages = windows[i].getPages();
    for (int j = 0; j < pages.length; j++) {
      IEditorPart editor = pages[j].findEditor(input);
      if (editor != null) {
        // エディタで開かれている
        return true;
      }
    }
  }
  // エディタで開かれていない
  return false;

Eclipseのエディタは編集対象をIEditorInputオブジェクトで扱うため,まずは対象のファイルのハンドルを元にFileEditorInputクラスのインスタンスを生成する。この入力オブジェクトがエディタを探すためのキーとなる。

あるエディタを探し出すために,EclipseのUI構成を辿る処理を行う。Eclipseでは,

  「ワークベンチ → ワークベンチウィンドウ → ワークベンチページ → エディタパート」

というコンポジット構造になっている。これをプログラム上でぐるぐる回して,該当するエディタを探し出す。

まずワークベンチ(IWorkbench)オブジェクトをPlatformUIクラスのgetWorkbenchクラスメソッドを呼び出して取得する。ここが基点。ワークベンチは複数のワークベンチウィンドウにより視覚化されるため,次はワークベンチからワークベンチウィンドウ(IWorkbenchWindow)オブジェクトの配列をgetWorkbenchWindowsメソッドを用いて取得する。各ワークベンチウィンドウはいくつかのワークベンチパートを持っているため,さらにワークベンチウィンドウからワークベンチパート(IWorkbenchPart)オブジェクトの配列をgetPagesメソッドを用いて取得する

IWorkbenchPartインタフェースには,入力オブジェクトに対応するエディタを探してくれるfindEditorメソッドが用意されている。各ワークベンチパートオブジェクトに対して,findEditorメソッドに最初に生成したIEditorInputオブジェクトを渡して,IEditorPartオブジェクトを取得する。この際,指定した入力オブジェクトを入力とするエディタが存在すればIEditorPartオブジェクトが返却され,もし存在しなければnullが返却される

つまりコンポジット構造の走査中に,findEditorメソッドの戻り値としてIEditorPartオブジェクトが得られれば対象のファイルがエディタで開かれていることになり,findEditorメソッドの全呼び出しがnullになってfor文を抜けてしまった場合は対象のファイルはエディタで開かれていない,ということになる。

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

テキストバッファを使ったIDocumentオブジェクトの取得

Eclipseでの作業のほとんどは,エディタに対してテキスト(Javaのソースコードなど)を編集する作業だろう。そして各種プラグインがエディタに機能を提供し,編集作業の補佐を行っている。プラグインからエディタで編集されているテキストを操作するためには,「テキストエディタからのIEditorInput,IDocumentオブジェクトの取得」で紹介した方法でエディタからIDocumentオブジェクトを取得し,それに対して各種メソッドを呼び出して内容を操作すればよい。

しかし,時にはエディタで開かれていないファイルに対しても,その内容について何らかの操作を行いたくなる。「プラグイン内で自動的に対象のファイルをエディタを開いて,IDocumentオブジェクトを取得してテキストを操作すればいいじゃん」というのは確かにありだが,対象のファイルが数多くある場合には,この方法は良い選択とは言えない。このような,エディタを開かずにファイルの内容をIDocumentオブジェクトとして取得したい時のために,Eclipseではテキストバッファと呼ばれる機構を用意してくれている。

テキストバッファの機構は,org.eclipse.core.filebuffersプラグインにより提供されているので,下記のようにプラグイン・マニフェスト内でorg.eclipse.core.filebuffersプラグインの使用を宣言する。

  <plugin ...>
    ...
    <requires>
      ...
      <import plugin="org.eclipse.core.filebuffers"/>
    </requires>
    ...
  </plugin>

では,IDocumentオブジェクトの取得までのコードを見てみよう。

  IPath targetFilePath = ...; // 対象のファイルのパス

  ITextFileBufferManager manager = FileBuffers.getTextFileBufferManager();
  manager.connect(targetFilePath, null);
  ITextFileBuffer buffer = manager.getTextFileBuffer(targetFilePath);
  IDocument document = buffer.getDocument();

最初にテキストバッファを管理してくれるITextFileBufferManagerオブジェクトをFileBuffersクラスのgetTextFileBufferManagerメソッドを使って生成する。その後,connectメソッドに処理対象のファイルのパス(IPathオブジェクト)を渡して,管理オブジェクトにファイルを接続する。第2引数にはIProgressMonitorインタフェースを渡せるようになっている。

管理オブジェクトへファイルを接続できれば,あとはgetTextFileBufferメソッドを使ってITextFileBufferオブジェクトを取得する。この時点でテキストバッファがVM内に存在するようになる。そして,getDocumentメソッドを呼び出すことでIDocumentオブジェクトを取得することができる。IDocumentオブジェクトへの操作は,テキストバッファ内の内容に反映される

IDocumentインタフェースを通じたテキストへの操作は,テキストバッファには反映されているが,ファイルに即座に反映されるわけではない。テキストバッファの内容をファイルに反映するためのコードが下記である。

  buffer.commit(null, true);
  manager.disconnect(targetFilePath, null);

ITextFileBufferオブジェクトのcommitメソッドを呼び出すことにより,テキストバッファの内容がファイルに書き出される。進捗状況を表示したいときは,第1引数にIProgressMonitorオブジェクトを渡す。第2引数は,commitメソッドの呼び出し時に,もしテキストバッファとファイルシステム上のファイルの内容が同期が取れていなかった場合に,問答無用でテキストバッファ内の内容でファイルを上書きするかどうかを指定する。

最後にITextFileBufferManagerオブジェクトのdisconnectメソッドを呼び出して,ファイルを管理オブジェクトから切断する。もちろん,commitメソッドを呼び出さずにdisconnectメソッドを呼び出せば,テキストバッファの内容は破棄されてファイルの内容も元のままとなる。

上記の方法を使えば,エディタに頼ることなく,ファイルの内容をIDocumentオブジェクトとして取り出すことができ,エディタからIDocumentオブジェクトを取得して内容を操作するときと全く同じことを,エディタを開かずに行うことができるようになる。いい感じ。

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

2004.07.17

Javaエディタのコンテキストメニュー

Eclipseを使っているほとんどの開発者は,Javaの開発が目的で利用していることだろう。Javaの開発者がEclipseに期待することといえば,やはりJavaのソースコードを編集するための充実した機能だ。しかし,ベーシックな機能だけでは物足りないことも出てくる。人間,欲に限りはない。今回は,Javaエディタのコンテキストメニューに独自の項目を追加する方法を紹介する。

ビューのコンテキストメニューに項目を追加する方法は,「特定ビューへのコンテキストメニュー項目の追加」ですでに紹介している。Javaエディタのコンテキストメニューに項目を追加する方法も基本的には同じ。

プラグイン・マニフェスト中にviewerContribution要素を記述してビューのコンテキストメニューに項目を追加するのだが,その際に対象となるビューをtargetID属性で指定する。Javaエディタの場合,targetID属性の値として#CompilationUnitEditorContextを指定する

  <extension point="org.eclipse.ui.popupMenus">
    <viewerContribution
      id="yoichiro.sample.popupMenus.javaeditor"
      targetID="#CompilationUnitEditorContext">
      <action
        id="yoichiro.sample.JavaEditorSampleAction"
        label="Sample Action"
        menubarPath="additions"
        class="yoichiro.sample.JavaEditorSampleAction">
      </action>
    </viewerContribution>
  </extension>

これにより,以下のようにJavaエディタのコンテキストメニューに項目が追加される。

javaeditor-menu1.gif

そして,メニュー項目が選択されたときに駆動されるクラス(class属性で指定するクラス)は,IEditorActionDelegateインタフェースを実装することが必要となる。

  public class JavaEditorSampleAction implements IEditorActionDelegate {
    private IEditorPart targetEditor;
    public void setActiveEditor(IAction action, IEditorPart targetEditor) {
      this.targetEditor = targetEditor;
    }
    public void run(IAction action) {
      if (targetEditor != null) {
        ...
      }
    }
    public void selectionChanged(IAction action, ISelection selection) {
    }
  }

IEditorActionDelegateインタフェースを実装することにより,setActiveEditorメソッド,runメソッド,そしてselectionChangedメソッドを実装することになる。通常は,setActiveEditorメソッドに渡される対象のエディタのオブジェクト(引数のtargetEditor)をフィールドに保持しておき,実際にメニューが選択されたときに呼び出されるrunメソッド内で,保持しておいたエディタオブジェクトに対して何らかの処理を行う。プラグイン・マニフェストでtargetID属性値に#CompilationUnitEditorContextを指定しているので,上記の例の場合はtargetEditorはJavaエディタのオブジェクトとなる。

メニュー項目の追加位置を,

javaeditor-menu2.gif

というようにSourceグループの中にしたいときは,プラグイン・マニフェストのmenubarPath属性を,

  menubarPath="org.eclipse.jdt.ui.source.menu/additions"

というように指定する。

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

2004.07.14

ダイアログボタンの変更

前回の「ダイアログの自作」では,JFaceのDialogクラスを拡張して,独自のダイアログを作成してみた。そこでは,[OK]ボタンと[Cancel]ボタンが自動的にダイアログに追加されていることが見て取れた。しかし,場合によっては他のボタンを配置したいときもあるだろう。今回は,自作したダイアログに対して[OK][Cancel]以外のボタンを配置する方法を紹介する。

[OK]ボタンと[Cancel]ボタンの配置は,DialogクラスのcreateButtonsForButtonBarメソッドで行われている。つまり,ボタンを自作したい場合は,createButtonsForButtonBarメソッドをオーバーライドしてあげればよいボタンの作成については,createButtonメソッドが準備されているので,それをcreateButtonsForButtonBarメソッドの中で使用してあげればよい。

以下のコードは,前回の「ダイアログの自作」で取り上げたソースコードに対して追記したものである。ダイアログのボタンを[Close]ボタンに変更している。

  public class MyDialog extends Dialog {
    ...
    public static final int CLOSE_ID = 777;
    ...
    protected void createButtonsForButtonBar(Composite parent) {
      createButton(parent, CLOSE_ID, "Close", true);
    }
    protected void buttonPressed(int buttonId) {
      if (buttonId == CLOSE_ID) {
        setReturnCode(CLOSE_ID);
        close();
      } else {
        super.buttonPressed(buttonId);
      }
    }
  }

まず,[Close]ボタンのIDをCLOSE_ID定数として作成しておく。そして,オーバーライドしたcreateButtonsForButtonBarメソッドの中で,createButtonメソッドを使って[Close]ボタンを作成している。createButtonメソッドの引数は,貼り付け先のCompositeオブジェクト,ボタンのID(先ほど作ったCLOSE_ID),ボタンの表示文字列,デフォルトボタンにするかどうか,である。

createButtonsForButtonBarメソッドのオーバーライドにより,表示されるダイアログは以下のようなものになる。

my-dialog2.gif

実行してみればわかるのだが,createButtonsForButtonBarメソッドをオーバーライドしただけでは,[Close]ボタンを押下しても何も起きない。[Close]ボタンが押されたときの処理として,openメソッドの戻り値となる値をセットしてダイアログを閉じる,という動作を記載しなければならない。ダイアログ上に配置されたボタンが押されたときは,buttonPressedメソッドが呼び出されるので,その中でボタンが押された後の処理を記述する。上記のコードでは,引数に渡されたボタンのIDがCLOSE_IDであるかどうかをチェックし,もしそうだった場合はsetReturnCodeメソッドを使ってopenメソッドの戻り値となる値をセットし,closeメソッドを呼び出してダイアログを閉じている。引数の値がCLOSE_ID以外だった場合(この場合ダイアログの[閉じる]ボタンが押されたとき)は,親クラスのbuttonPressedメソッドを呼び出して処理を行わせている。

上記の例では[Close]ボタン1つだったが,もちろんcreateButtonsForButtonBarメソッドの中で複数回createButtonメソッドを使用することによって,複数のボタンを配置することも可能である。

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

2004.07.13

ダイアログの自作

Eclipseでは,ウィザードやプロパティ編集のためのダイアログが標準でいくつか準備されている。しかし,ダイアログを自作したいときも出てくるだろう。今回はカスタムダイアログの作成方法について紹介する。

ダイアログの作成は,SWTのDailogクラスを拡張する方法と,JFaceのDialogクラスを拡張する方法の2種類がある。JFaceの方がもちろんダイアログを作りやすくしてくれているので,JFraceのDialogクラスを用いる。自作ダイアログは,Dialogクラスを継承して作成する

  public class MyDialog extends Dialog {
    public MyDialog(Shell parent) {
      super(parent);
    }
    protected Point getInitialSize() {
      return new Point(400, 300);
    }
    protected void configureShell(Shell newShell) {
      super.configureShell(newShell);
      newShell.setText("Title of MyDialog");
    }
    protected Control createDialogArea(Composite parent) {
      Composite composite = (Composite)super.createDialogArea(parent);
      Text text = new Text(composite,
        SWT.MULTI | SWT.V_SCROLL | SWT.BORDER | SWT.WRAP);
      text.setLayoutData(new GridData(GridData.FILL_BOTH));
      text.setText("Hello!");
      return composite;
    }
  }

どんなウィジェット(AWTとかMotifとかGTKとかXawとか)でも,大抵ダイアログを表示するためには,その親となるウィンドウが必要となる。JFaceのDialogクラスの場合も,コンストラクタの呼び出しにShellオブジェクトを必要とする。もちろんこれはnullが許容されるが,ダイアログは何らかのウィンドウに属することは必然だと思うので,実際にはnullを渡すことは避けるべき。

ダイアログに関する各種設定やクライアント領域のコンテンツの作成などは,Dialogクラスで規定されたメソッドをオーバーライドすることにより記述する。

まずはダイアログの大きさを決めるために,getInitialSizeメソッドをオーバーライドし,縦横の大きさを持つPointオブジェクトを返すようにする。ダイアログが開かれる際にこのメソッドが自動的に呼び出され,返却された値がダイアログのサイズとして使用される。

次にダイアログのタイトルバーに表示する文字列だが,これはダイアログのShellオブジェクトに設定することになる。ダイアログが開かれるとき,新しく内部(親のWindowクラスのcreateShellメソッド内)でShellオブジェクトが作られる。作成されたShellオブジェクトに対して操作を行うために,configureShellメソッドをオーバーライドする。configureShellメソッドにはShellオブジェクトが渡されてくるので,まずは親のcnofigureShellメソッドを呼び出し(デフォルトイメージ・レイアウトの設定が行われる),その後でsetTextメソッドを使ってタイトルバーの文字列をセットしている。ここでShellオブジェクトのsetImageメソッドを使えば,イメージを変えることも可能。

そして一番肝心なメソッドがcreateDialogAreaメソッド。createDialogAreaメソッドをオーバーライドし,その中でダイアログのクライアント領域に対してコンポーネントを配置していくコードを記述する。引数で渡されてくるCompositeオブジェクトに対してコンポーネントを配置するのが基本なのだが,実はそれよりも上記のコードのように,一旦親のcreateDialogAreaメソッドを呼び出し,その結果得られるCompositeオブジェクトに対してコンポーネントを配置するほうが良い。親のcreateDialogAreaメソッドの中では,クライアント領域の上下左右に余白が作られ,フォントの設定も施されたCompositeオブジェクトを返却してくれる。上記のコードでは,Textコンポーネントを配置している。

これで基本的には「タイトルバーに文字列が表示され,大きさが300x200のテキストエリアを持つダイアログ」が表示できるようになった。

  Shell shell = ...;
  MyDialog dialog = new MyDialog(shell);
  dialog.open();

というコードを実行することで,以下のようなダイアログが表示される。

my-dialog.gif

JFaceのDialogクラスでは,何もしなくても[OK]ボタンと[Cancel]ボタンを配置してくれる。そしてopenメソッドの戻り値として,何のボタンが押されたかが返却されるので,

  int ret = dialog.open();
  if (ret == IDialogConstants.OK_ID) {
    // [OK]ボタン押下
  } else if (ret == IDialogConstants.CANCEL_ID) {
    // [Cancel]ボタン押下
  }

という判断ができる。ちなみに,ウィンドウの[閉じる]ボタン押下時は,IDialogConstants.CANCEL_IDが返却される。

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

2004.07.03

ワークベンチ・ウィンドウの表示・消去時の処理

「最初のリッチクライアント(1)(2)(3)(4)」シリーズで紹介したリッチクライアント・アプリケーションは,ホントに簡素なものだった。ウィンドウの大きさも不定だし,タイトルバーにも何も表示されていない。

リッチクライアントの場合,WorkbenchAdvisorクラス(のサブクラス)にてワークベンチ・ウィンドウへの設定を行う。「最初のリッチクライアント(2)」で作成したSmileWorkbenchAdvisorクラスを例として取り上げよう。

  public class SmileWorkbenchAdvisor extends WorkbenchAdvisor {
    ...
    public void preWindowOpen(IWorkbenchWindowConfigurer configurer) {
      super.preWindowOpen(configurer);
      configurer.setTitle("Smile application");
      configurer.setInitialSize(new Point(400, 300));
      configurer.setShowCoolBar(false);
      configurer.setShowStatusLine(false);
    }
  }

最初のリッチクライアント(2)」の時のコードに加えて,preWindowOpenメソッドをオーバーライドしている。preWindowOpenメソッドは,ワークベンチ・ウィンドウが開かれる直前にEclipseランタイムから呼び出されるコールバックメソッドである。preWindowOpenメソッドに引数として渡されるIWorkbenchWindowConfigurerオブジェクトの各メソッドを呼び出すことによって,ワークベンチ・ウィンドウに対して設定を行うことができる。

上記のコードでは,setTitleメソッドによりワークベンチ・ウィンドウのタイトルバーに文字列をセットしている。また,setInitialSizeメソッドにPointオブジェクトを渡すことで,ワークベンチ・ウィンドウの大きさを指定している。残りのsetShowCoolBarメソッドおよびsetShowStatusLineメソッドにfalseを渡すことで,名前の通りクールバー領域およびステータス表示領域をワークベンチ・ウィンドウから消去している。

これにより,アプリケーションを起動すると,以下のようなワークベンチ・ウィンドウが表示される。

rcp-wbw1.gif

最初のリッチクライアント(3)」の場合のワークベンチ・ウィンドウと比べると,クライアント領域内の上下の余白がなくなっているのがわかるだろう。

WorkbenchAdvisorクラスでは,ワークベンチ・ウィンドウが閉じられるときの処理もちゃんと考慮されている。ワークベンチ・ウィンドウ(正確にはワークベンチ・ウィンドウのShellオブジェクト)が閉じられるときには,preWindowShellCloseメソッドがコールバックされる

  public boolean preWindowShellClose(IWorkbenchWindowConfigurer configurer) {
    Shell shell = configurer.getWindow().getShell();
    return MessageDialog.openConfirm(shell, "Confirm", "Are you sure?");
  }

preWindowShellCloseメソッドの戻り値にfalseを渡すことによって,ワークベンチ・ウィンドウのクローズを拒否することができる。上記のコードでは,ワークベンチ・ウィンドウが閉じられようとした際に確認ダイアログを表示し,[Cancel]ボタンが押下された場合はワークベンチ・ウィンドウを閉じないようにしている。

rcp-wbw2.gif

ワークベンチ・ウィンドウが開かれた直後にはpostWindowOpenメソッドが呼び出されるので,この中でコンポーネントにフォーカスを当てるなどの処理を行うことができる。

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

最初のリッチクライアント(4) - 単独で実行

前回「デバッガで実行」で実行したリッチクライアント,一刻も早く単独で動かしたいところだろう。リッチクライアントはIDEの拡張機能ではなく,単独で動作するGUI付きのアプリケーションなので,最終的にはRun-time Workbenchで実行していても意味がない。今回は,作成したリッチクライアントをEclipse-IDEから切り離し,単独で動作させるための方法を紹介する。

プラグインの開発後,いろんな人に使ってもらうために,プラグインをエクスポートするだろう。リッチクライアントいえどプラグインに代わりはないので,まずは前回までに作成したyoichiro.rcp.smileプラグインをエクスポートする。Package Explorerビューのyoichiro.rcp.smileプロジェクトで右クリックを行い,表示されたコンテキストメニューの[Export...]を選択する。

表示されたExportダイアログでは,[Select an export destination]欄の中から[Deployable plug-ins and fragments]を選択し,[Next]ボタンを押下する。そして次のページでは,[Available Plug-ins and Fragments]欄で[yoichiro.rcp.smile]にチェックが入っていることを確認する。また,[Export Options]内の[Deploy as]は[a directory structure]を選択し,[Destination]の[Directory]に適当なディレクトリを入力する(例えば「C:\SmileApplication」)。

rcp-run1.gif

ウィザードの最後に[Finish]ボタンを押せば,指定したディレクトリにyoichiro.rcp.smileプラグインがエクスポートされる。指定したディレクトリに移動すると,pluginsディレクトリ内にyoichiro.rcp.smile_1.0.0プラグインディレクトリが存在し,その中にplugin.xml,smile.jarファイルが生成されているのが確認できる。

rcp-run2.gif

エクスポートは完了したが,それだけで動かせるほど世の中甘くない。いくつかファイルをEclipseからコピーする必要がある。

まずはEclipseをインストールしたディレクトリにあるstartup.jarを,先ほどエクスポートしたディレクトリにコピーする。次に,実行に必要な依存プラグインを,エクスポートしたディレクトリにあるpluginsディレクトリ内にコピーする。ここで「依存プラグインって,どれだよ!?」と思うのは当然。「プラグイン・マニフェストの作成」で記載した2つの依存プラグインだけであればそれらのみをコピーすればよいのだが,実は他にもいくつか必要なプラグインが存在する。

実行に必要なプラグインがどれなのかを調べるにはどうすればよいか,実はその答えは前回の「デバッガで実行」で既に示している。デバッガで実行する際に,Debugダイアログ内の[Plug-ins]タブにおいて,一旦全プラグインのチェックをはずし,再度依存しているプラグインのみにチェックを入れるための方法を示した。そこでチェックが自動的に入ったプラグインについて,startup.jarファイルと同様に,Eclipseをインストールしたディレクトリからコピーする。

しかし,上記の方法だけで発見した依存プラグインをコピーしただけでは,実行に失敗してしまう。Eclipseランタイムがプラグインを発見できないのが理由である。これを解決するためには,org.eclipse.update.configuratorプラグインもコピーする必要がある。

依存プラグインのコピー後,エクスポートしたディレクトリ内はこんな構造になっているはずである。

rcp-run3.gif

以上でファイルが揃ったので,実行できるようになる。コマンドプロンプトでエクスポートしたディレクトリに移動し,以下のコマンドを実行してほしい。

javaw.exe -cp startup.jar org.eclipse.core.launcher.Main -application yoichiro.rcp.smile.SmileApplication

前回と同じように空のウィンドウが表示されれば,単独実行は成功である。

もちろん,ユーザにコマンドプロンプトで起動させるわけにはいかないので,通常はバッチファイルやショートカットを作成しておくことが必要になるだろう。

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

2004.07.02

最初のリッチクライアント(3) - デバッガで実行

プラグイン・マニフェストの作成」「クラスの作成」が完了したところで,Eclipseのデバッガを使えば実行を行うことができる。今回は,早速作成したリッチクライアントをデバッガで実行する方法を紹介する。

通常のプラグイン開発と同じように,Run-time Workbenchを使って実行を行う。ただし,Eclipse-SDKに含まれるさまざまなプラグインを読み込まないようにしなければならないなど,いくつかの設定変更を行う必要がある。

まず,[Run]-[Debug...]メニューを選択し,Debugダイアログを表示させる。そして左の[Configurations]内のRun-time Workbenchを選択し,[New]ボタンを押下する。

このままでは普通にワークベンチが起動してしまうので,作成したアプリケーションが実行されるように設定しなければならない。これを行うのが[Arguments]タブ内の[Program to Run]の部分である。この部分の[Run an application]にチェックを入れ,右のコンボボックスから[yoichiro.rcp.smile.SmileApplication]を選択する。これにより,SmileApplicationクラスのインスタンスが生成され,runメソッドが呼び出されるようになる。

rcp-debug1.gif

次に,[Plug-ins]タブを選択する。ここでは,実行に必要な最低限のプラグインを選択する。まず,[Choose plug-ins and fragments to launch from the list]にチェックを入れる。すると,プラグインのリストが表示される。ここで[Deselect All]ボタンを押下し,全プラグインのチェックを一旦はずす。次に,[yoichiro.rcp.smile]プラグインにチェックを入れる。最後に[Add Required Plug-ins]ボタンを押下すると,実行に必要なプラグインが自動的に検出され,チェックが入る。

rcp-debug2.gif

以上で設定は終了。[Name]に適当に名称を入れ,[Debug]ボタンを押下する。以下のようなウィンドウが開かれれば,成功である。

rcp-debug3.gif

これが最初のリッチクライアントである。ウィンドウ内に描画されている線をEclipse-IDEのウィンドウと見比べてみると,確かにEclipseのワークベンチ・ウィンドウっぽいことがわかるだろう。[閉じる]ボタンを押せば,ちゃんと(?)終了することができる。

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

2004.07.01

最初のリッチクライアント(2) - クラスの作成

前回はリッチクライアントの作成の第1歩として,「プラグイン・マニフェストの作成」の方法を紹介した。もちろんプラグイン・マニフェストだけではなく,いくつかクラスを作成することも必要となる。今回は,最低限必要となるクラスの作成方法について紹介する。

リッチクライアントを作成するために最低限必要なクラスは,以下の3つである。

  (1) IPlatformRunnableインタフェースの実装クラス
  (2) WorkbenchAdvisor抽象クラスのサブクラス
  (3) IPerspectiveFactoryインタフェースの実装クラス

IPlatformRunnableインタフェースの実装クラスは,アプリケーションのエントリポイントとなるクラスである。つまり,リッチクライアントの起動は,IPlatformRunnableインタフェースの実装クラスのインスタンス生成&runメソッド呼び出しから開始される。

WorkbenchAdvisor抽象クラスのサブクラスは,リッチクライアントの土台となるワークベンチへの各種設定を行うためのクラスである。例えば,デフォルトのパースペクティブを決定するのはWorkbenchAdvisorオブジェクトである。また,アプリケーションのライフサイクルを管理する役目も持つ。つまり,アプリケーションの動作中のさまざまなタイミングで,WorkbenchAdvisorオブジェクトの各種メソッドがコールバックされる。

前回「最低1つのパースペクティブを持つ」と解説した。その解説どおり,パースペクティブを準備するためにIPerspectiveFactoryインタフェースの実装クラスが必要となる

では順番にクラスを作成していく。まずは(1)から。

  public class SmileApplication implements IPlatformRunnable {
    public Object run(Object args) throws Exception {
      WorkbenchAdvisor advisor = new SmileWorkbenchAdvisor();
      Display display = PlatformUI.createDisplay();
      try {
        int ret = PlatformUI.createAndRunWorkbench(display, advisor);
        if (ret == PlatformUI.RETURN_RESTART) {
          return IPlatformRunnable.EXIT_RESTART;
        } else {
          return IPlatformRunnable.EXIT_OK;
        }
      } finally {
        display.dispose();
      }
    }
  }

IPlatformRunnableインタフェースで規定されているrunメソッドを実装する。runメソッド内では,最初にWorkbenchオブジェクトに対する設定を行うためのWorkbenchAdvisorオブジェクトのインスタンスを生成する。次に,PlatformUIクラスのcreateDisplayメソッドを使って,Displayオブジェクトを生成する。そしてこの2つのオブジェクトを使ってPlatformUIクラスのcreateAndRunWorkbenchメソッドを呼び出し,ワークベンチを開始する

ワークベンチが何らかの理由で終了されるまで,createAndRunWorkbenchメソッドはブロックされる。ワークベンチ終了後,createAndRunWorkbenchメソッドは,以下のいずれかの値を返してくる

  PlatformUI.RETURN_OK - 通常終了
  PlatformUI.RETURN_RESTART - 再起動要求による終了
  PlatformUI.RETURN_UNSTARTABLE - ワークベンチ開始失敗
  PlatformUI.RETURN_EMERGENCY_CLOSE - 異常終了

runメソッドの戻り値は,IPlatformRunnableインタフェースに定義された定数(EXIT_OK,EXIT_RESTART,EXIT_RELAUNCH)のどれかである。上記のコードでは,createAndRunWorkbenchメソッドの戻り値がPlatformUI.RETURN_RESTARTだった場合はIPlatformRunnable.EXIT_RESTARTを,そうでない場合はIPlatformRunnable.EXIT_OKをrunメソッドの戻り値として返却している。

ワークベンチが終了したからといってDisplayオブジェクトが破棄されるわけではないので,必ずDisplayオブジェクトのdisposeメソッドを後始末として呼び出すようにする。

次に,(2)のWorkbenchAdvisorサブクラス。

  public class SmileWorkbenchAdvisor extends WorkbenchAdvisor {
    public String getInitialWindowPerspectiveId() {
      return "yoichiro.rcp.smile.SmilePerspective";
    }
  }

WorkbenchAdvisorクラスのサブクラスでは,最低限getInitialWindowPerspectiveIdメソッドを実装することが求められる。getInitialWindowPerspectiveIdメソッドは,ワークベンチに必要となるパースペクティブのIDの文字列を返却する

最後に(3)のIPerspectiveFactoryインタフェースの実装クラスを以下に示す。

  public class SmilePerspective implements IPerspectiveFactory {
    public void createInitialLayout(IPageLayout layout) {
    }
  }

ホントはcreateInitialLayoutメソッド内でビューやエディタなどを組み立てる必要があるが,今回は最低限のアプリケーションなので,何も行わない。

以上でクラスの作成は終了。これによるアプリケーションは単にウィンドウが開くだけなのだが,思ったよりも簡単だと思われるのではないだろうか。次回は,このリッチクライアント・アプリケーションの実行方法を紹介する予定。

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

最初のリッチクライアント(1) - プラグイン・マニフェストの作成

Eclipse3.0が2004年6月30日に正式リリースされた。この3.0で非常に重要な点は,リッチクライアントのプラットフォームとして3.0が仕立て上げられたということである。Webアプリケーションからリッチクライアントへと時代が流れている今日,Eclipseはリッチクライアントを開発,運用する上で非常に優位な選択肢になったということができる。今回は,成長したEclipse3.0で,簡単なリッチクライアントを作成する方法を紹介する。もちろんEclipse3.0が必要なので,Eclipse2.1系の方はバージョンアップが必要。

早速始めよう。リッチクライアントといえど,要は今までのプラグイン開発と何ら変わらない。まずはPlug-in Projectの新規作成ウィザードを使って,プラグインの雛形を作成する。ほぼデフォルトの設定でOKなのだが,ウィザードの2枚目にあるPlug-in Classの作成は行わないようにする(理由は後述)。ここでは,

  [1枚目]
    Project name: yoichiro.rcp.smile
  [2枚目]
    Generate the Java class ... plug-in's life cycle: OFF

として[Finish]ボタンを押下し,プラグインの雛形を作成する。

作成されたプラグイン・マニフェストを見てみると,2行目に<?eclipse version="3.0"?>と記載されているあたりに「進化したんだなぁ」と実感することだろう。3行目以降は,今までどおりの見慣れた記述が並んでいるはず。これに対して,リッチクライアント開発に必要な記述を行っていく。

まずは必要最低限の依存プラグインとして,org.eclipse.core.runtimeとorg.eclipse.uiをrequires要素を使って記述する

  <plugin ...>
    ...
    <requires>
      <import plugin="org.eclipse.core.runtime"/>
      <import plugin="org.eclipse.ui"/>
    </requires>
  </plugin>

次に,リッチクライアントとして作成するアプリケーションの定義を行う。アプリケーションの定義は,org.eclipse.core.runtime.applications拡張ポイントを使って記述する

  <extension
      point="org.eclipse.core.runtime.applications"
      id="SmileApplication">
    <application>
      <run class="yoichiro.rcp.smile.SmileApplication"/>
    </application>
  </extension>

id属性を使って,アプリケーションの識別子を記述する。実際のアプリケーションの識別子は,プラグインIDが先頭に付与された形になるので,yoichiro.rcp.smile.SmileApplicationが正式なアプリケーションの識別子となる。org.eclipse.core.runtime.applications拡張ポイントでは,子要素としてapplication要素を必要とする。application要素の子要素にrun要素を記述し,そのclass属性にアプリケーションのエントリポイントとなるクラスの名前を記述する。ここで記述するクラスは,IPlatformRunnableインタフェースの実装クラスである。

Eclipseのリッチクライアント・アプリケーションは,最低1つのパースペクティブを持つことになるので,パースペクティブの定義もプラグイン・マニフェストに必要となる。org.eclipse.ui.perspectives拡張ポイントを使って,パースペクティブの定義を以下のように記述する

  <extension
      point="org.eclipse.ui.perspectives">
    <perspective
        name="Smile perspective"
        class="yoichiro.rcp.smile.SmilePerspective"
        id="yoichiro.rcp.smile.SmilePerspective">
    </perspective>
  </extension>

perspective要素のname,class,idの各属性にそれぞれ名前,IPerspectiveFactoryインタフェース実装クラス名,そしてパースペクティブのIDを記述する

プラグイン・マニフェストに対するリッチクライアントのための必要最低限の記述は以上のとおりとなる。アプリケーションとパースペクティブの関連などは,Javaソースコード中に記載される。プラグイン・マニフェスト上では,リッチクライアントは単なるプラグインに過ぎないことがわかるだろう。

次回は,リッチクライアントの作成で最低限作成しなければならないクラスの紹介を行う予定。

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

2004.06.11

イメージレジストリ

イメージの扱い方」で紹介したように,イメージリソースは利用されなくなったら速やかにdisposeメソッドを呼び出して解放してあげなければならない。しかし,CやC++のfreeやdeleteなどのメモリ割り当ての解放作業をしたくないがためにJavaのGC(ガーベージコレクション)ができたように,イメージリソースに関しても勝手に解放してくれるような機構が欲しくなるものである。また,イメージリソースは複数の利用箇所で同一リソースを共有することができるということを考えると,実はイメージリソースの解放のタイミングは一概に決められない,といった状況も発生してくる。

そこでEclipseでは,イメージリソースを一括管理してくれるImageRegistryクラスが提供されている。ImageRegistryクラスを使うと,イメージの登録と取得,登録されたイメージの自動一括解放ができるようになる

  // レジストリの準備
  ImageRegistry registry = new ImageRegistry();

  // イメージの登録
  ImageDescriptor descriptor = ...; // 「イメージの扱い方」参照
  registry.put("smile", descriptor);

  // イメージの取得
  Image image = registry.get("smile");

使い方としては,まずImageRegistryクラスのインスタンスを生成する。そして「イメージの扱い方」で取り上げたImageDescriptorオブジェクトを生成し,putメソッドを使ってImageRegistryオブジェクトを登録する。この際,イメージの名前(何でも良い)をつけておく。

そしてイメージを利用したいときには,ImageRegistryオブジェクトからgetメソッドに登録時につけた名前を渡すことで,イメージが自動的に生成されてImageオブジェクトを得ることができる。過去にgetメソッドで取得したイメージについては,新たにイメージリソースが作成されるのではなく,内部で保持されているイメージリソースが使いまわされる

ImageRegistryクラスには,getメソッドとputメソッドしかない。登録されたイメージの破棄に関しては,完全自動化が実現されている。Imageオブジェクトの生成には,Deviceオブジェクト,通常はDisplayオブジェクトが必要となる。ImageRegistryクラスのコンストラクタ(引数なし版)では,コンストラクタを呼び出した処理のスレッドに紐づくDisplayオブジェクトをDisplay.getCurrent()で取得し,内部に保持している。そして,そのDisplayオブジェクトが破棄されたタイミングで,登録されているImageオブジェクトのdisposeメソッドを呼び出して破棄している。この処理は,DisplayオブジェクトのdisposeExecメソッドにRunnbaleオブジェクトの形でImageRegistryオブジェクトが内部で登録している。

ImageRegistryクラスを使えば,イメージリソースは勝手に破棄される。しかし,やはりイメージリソースの無駄遣いは避けるべきである。ImageRegistryクラスの使用するしないに関わらず,イメージリソースに関しては注意深く扱うべきである。

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

2004.06.10

イメージの扱い方

GUIを伴うアプリケーションを作っていると,どうしても見た目に凝りだすのは仕方のないこと。たいていはアイコンやイメージを作ってGUIに貼り付けることを考えるのではないだろうか。実はアイコンやイメージは,OSから見るとちょっとした重いリソースであり,気にしなければならないことがいくつか出てくる。今回は,Eclipseでイメージを扱うためにはどうしたら良いか,について紹介する。

イメージはgifやjpegなどの形式のバイト配列が実体だが,それはファイルとして保存されていたり,データベースにBLOGとして格納されていたりする。または,どこかのHTTPサーバに配置されているかもしれない。もちろん,それぞれでバイト配列の取得方法に違いが出てくるし,イメージの形式によっても処理内容が違ってくる。

Eclipseでは,イメージリソースの生成を統一化された方法で行うために,ImageDescriptorクラスが提供されている。ImageDescriptorクラスは,ファイル名あるいはURLにより特定される場所から,イメージリソースを生成する方法を知っているクラスである。例えば,あるプラグインのiconsフォルダに存在するsmile.gifファイルからイメージリソースを生成するためのコードは以下のようになる。

  ImageDescriptor descriptor;
  try {
    URL url = MyPlugin.getInstance().getDescriptor().getInstallURL();
    descriptor = ImageDescriptor.createFromURL(new URL(url, "icons/smile.gif"));
  } catch(MalformedURLException e) {
    descriptor = ImageDescriptor.getMissingImageDescriptor();
  }
  Image image = desriptor.createImage();

まず,Pluginクラスのインスタンスを取得し,getDescriptorメソッドでIPluginDescriptorオブジェクトを取得する。そしてIPluginDescriptorオブジェクトのgetInstallURLメソッドを呼び出すことで,プラグインがインストールされている場所を示すURLオブジェクトを得る。これは「Pluginクラスの作成」を参照されたし。

そしてImageDescriptorクラスのcreateFromURLクラスメソッドにイメージの場所を示すURLオブジェクトを渡すことで,ImageDescriptorオブジェクトを取得する。上記では,プラグインがインストールされた場所を示すURLオブジェクトに新たにicons/smile.gifというパスを追加したURLオブジェクトを生成し,createFromURLメソッドに渡すことでImageDescriptorオブジェクトを生成している。その後,createImageメソッドを呼び出すことで,イメージリソースを表すImageオブジェクト(SWTのクラス)を生成する

上記のコードの場合,もしかしたらイメージの場所を示すURLの生成に失敗するかもしれない。その場合,MalformedURLException例外が発生するが,プラグイン的には何らかのイメージが表示されないと都合が悪いこともある。そんなときのために,ImageDescriptorクラスには「イメージの読み込み,ミスったぞ」状態を表すイメージを生成するためのgetMissingImageDescriptorメソッドが用意されている。これを使うと,「missing-image.gif」というイメージリソースがcreateImageメソッドで得られる。

あとはいろんな場所で好きなようにImageオブジェクトを利用すればよい。

さて,イメージリソースはその使用が終わったときには,ちゃんと破棄しなければならない。冒頭で述べたとおり,イメージリソースは重いので,必要最低限のイメージリソースの確保時間にとどめる事がプラグイン開発者に求められる。さもないと,他のプラグイン,しいてはEclipse全体のパフォーマンスに影響を及ぼしかねない。

イメージリソース,つまりImageオブジェクトの破棄は,

  Image image = ...;
  image.dispose();

というようにdisposeメソッドを呼び出すだけである。非常に簡単。いかに必要最低限の確保にとどめ,最適なタイミングでdisposeするか,はプログラマの腕の見せ所だし,楽しく感じる部分である(特にGCに頼り切っているJavaしか知らない開発者は新鮮だろう)。タイミングという点では,JFaceの各種Viewerでは,Viewerが破棄されるときにViewerのdisposeメソッドが呼び出されるので,その中で使用したイメージリソースの破棄を行えばよいし,その他必ずイメージリソースを破棄できるタイミングは存在するはずなので,忘れずにdisposeするように心がけたい。間違っても「Pluginクラスのshutdownメソッド内で一気に破棄すればいいじゃん」なんて横着なことは考えないように。

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

2004.06.09

Pluginクラスの作成

Eclipseプラグインは,各プラグインの生成と破棄のタイミング(ライフサイクル)で何らかの処理を行うために,Pluginクラスを準備しておくことができる。各プラグインでは,Pluginクラスのサブクラスを作成しておき,プラグイン・マニフェストにそのクラス名を記載しておくことによって,Eclipseプラットフォームが自動的にPluginクラスのインスタンスを生成し,所定のメソッドを呼び出してくれる

  public class MyPlugin extends Plugin {
    public MyPlugin(IPluginDescriptor descriptor) {
      super(descriptor);
    }
    public void startup() throws CoreException {
      // プラグイン活性時に行いたい処理
    }
    public void shutdown() throws CoreException {
      // プラグイン非活性時に行いたい処理
    }
  }

プラグイン・マニフェストでは,plugin要素のclass属性にPluginクラス名を指定する。

  <?xml version="1.0" encoding="UTF-8"?>
  <plugin
      id="yoichiro.myPlugin"
      name="Yoichiro Plugin"
      version="1.0.0"
      class="yoichiro.myPlugin.MyPlugin">
    ...
  </plugin>

Pluginクラスの典型的な作りとして,自身のインスタンスを返却するクラスメソッドを準備するということがあげられる。Pluginオブジェクトは,結構いろんな箇所で使われることになる。代表的な例としては,プラグインのインストールされている場所を取得するために使ったりする。ソースコードはこんな感じ。

  public class MyPlugin extends Plugin {
    private static MyPlugin instance;
    public MyPlugin(IPluginDescriptor descriptor) {
      super(descriptor);
      instance = this;
    }
    public static MyPlugin getInstance() {
      return instance;
    }
  }

コンストラクタでインスタンスをフィールドに保持して,getInstanceメソッドで取得できるようにしている。これにより,プラグイン内の任意のクラスから,

  URL url = MyPlugin.getInstance().getDescriptor().getInstallURL();

で,プラグインがインストールされている場所を取得することができる。

インストール場所の取得以外にも,Pluginクラスは言わばコントローラ的役割(処理の起点)を持たせたりもするので,上記のようにgetInstanceメソッドでインスタンスを何処からでも取得できるようにしておくと非常に有効である。

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

2004.06.05

ファイルの作成

ほとんどのソフトウェアが必ずといっていいほど行っている処理として,ファイルの作成があげられる。一番手軽な情報の永続化の方法である。今回は,プラグイン内で,あるプロジェクトまたはフォルダにファイルを作成するための方法を紹介する。

Eclipseでは,ファイルなどのリソースは「ハンドル」と呼ばれる識別子で表される。ハンドルはリソースの実体に関する情報を持つものだが,実体がなくても(あたかも実体があるかのように)作成することができる。ファイルの作成は,作成したいファイルのハンドルを作成し,そのハンドルに対して作成を指示する,という手順になる。

まずはソースコードを紹介しよう。

  IContainer container = ...; // IProjectまたはIFolderオブジェクト

  IFile file = container.getFile(new Path("hoge.txt"));

  String contents = "Hello world!";
  InputStream is = new ByteArrayInputStream(contents.getBytes());

  file.create(is, false, null);

ファイルはプロジェクトまたはフォルダに対して作成される。つまり,まずはプロジェクトを表すIProjectオブジェクトか,フォルダを表すIFolderオブジェクトを何らかの方法で取得する。この2つのインタフェースは,共にIContainerインタフェースを継承している。IContainerインタフェースには,ファイルハンドルを生成するためのgetFileメソッドが規定されているので,それを利用して作成したいファイルのハンドルオブジェクト(IFileオブジェクト)を生成する

getFileメソッドの引数には,ファイルのパスを表すIPathインタフェースのオブジェクトを指定する。EclipseではIPathインタフェースの実装クラスとしてPathクラスを用意してくれているので,Pathクラスのコンストラクタに作成したいファイルのファイル名を渡してインスタンスを生成している。ファイルのパスはcontainerオブジェクトが持つファイルシステム上の場所からの相対パスになる。

IFileオブジェクトによるファイルの作成には,作成するファイルの内容を入力ストリームの形で渡さなければならない。そこで上記のソースコードでは,”Hello world!”という文字列をバイト列に変換し,そのバイト列を元にByteArrayInputStreamオブジェクトを生成してファイルの内容としている。

そしてハンドルの実体,つまりファイルの作成はIFileオブジェクトのcreateメソッドを使って行う。第1引数に先ほど作成しておいた入力ストリームを渡し,第3引数にIProgressMonitorオブジェクトを渡す。第1引数の内容によってはファイルの作成に長時間かかる可能性があるため,進捗表示を行うための機能がcreateメソッドに備わっている。

第2引数だが,これはちょっと難しい。createメソッドの挙動を決定するためのフラグなのだが,基本的に第2引数は作成しようとしたファイルが既に存在したときにどうするか,ということを指定する。このフラグも含めて,createメソッドの挙動をまとめてみる。

  ・「hoge.txt」が存在し,既にプロジェクトに登録されていたとき
    第2引数の値に関係なく,エラー(Resource ... already exists.)が発生。

  ・「HOGE.TXT」が存在し,既にプロジェクトに登録されていたとき
    第2引数の値に関係なく,エラー(A resource exists with a different case: ...)が発生。

  ・「hoge.txt」が存在し,プロジェクトに登録されていないとき
    第2引数がtrueの場合,一旦削除されて新しく作成される
    第2引数がfalseの場合,エラー(A resource already exists on disk ...)が発生。

  ・「HOGE.txt」が存在し,プロジェクトに登録されていないとき
    第2引数の値に関係なく,エラー(A resource exists on disk with a different case: ...)が発生。

  ・「hoge.txt」「HOGE.TXT」が存在しないとき
    第2引数の値に関係なく,新しく作成される

安全性を求めるなら,第2引数はfalseを指定しておくのが無難だろう。以上の処理により,ファイルが作成されてプロジェクトに登録される。

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

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

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.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.03.29

デバッグ用コード切り替えスイッチ

プログラムの内部動作というのは,なかなか表向きからはわからない。原因不明のバグも1,2件はめずらしくない。開発途中であれば,開発者が内部の動作状況をトレースするためのコードを埋め込んで検証したりすることは容易だが,一旦リリースしてしまった後となると検証を行うことは非常に難しくなる。リリース時にデバッグ用コードをプログラムから削除してしまうことが多いからだ。

J2SEの1.4から追加されたアサーション機構は,VMの起動時にそれを有効にするかどうかを切り替えられる。それと同じように,プラグインを試験するためのRun-time Workbenchでは,トレース機能などのデバッグ用コードを有効にするかどうかを切り替えるための機構が備わっている

Eclipseを構成するプラグインをいろいろ覗いていくと,.optionsという名前のファイルを持っているのがわかる。この.optionsファイルには,各機能を有効にするかどうかのスイッチとなる名前と,有効にするかどうかの真偽値(trueかfalse)を記述する。スイッチの名前は,プラグインのIDに機能名を加えたものである。

  # Turn on debugging for the yoichiro.myPlugin
  yoichiro.myPlugin/debug=true
  # Displays file name.
  yoichiro.myPlugin/display/filename=false

先頭が「#」の行はコメントとみなされる。上記を見てわかるように,プラグインIDのあとは「/」で機能名を区切って記述する。機能名は任意に決めてよい。プロパティファイルと同じように,trueかfalseかの値を「=」のあとに記述する。

プログラムから上記のファイルにアクセスするために,PlatformクラスのgetDebugOptionメソッドを使用する。一般的には,クラスのstaticイニシャライザで値を読み込んで,実際のコード中で使用する。

  public class Hoge {
    private static boolean debug = false;
    static {
      String value = Platform.getDebugOption(
        "yoichiro.myPlugin/debug");
      if (value != null && value.equalsIgnoreCase("true")) {
        Hoge.debug= = true;
      }
    }
  }

PlatfromクラスのgetDebugOptionメソッドに.optionsファイルに記載した「プラグインID+機能名」の文字列を渡すことで,その値を文字列として取得することができる。そして,文字列がnull(指定された項目が見つからない)ではなく,しかも値がtrueだった場合は,debugフィールドの値を真としている。あとはdebugフィールドの値を使って,

  if (Hoge.debug)
    System.out.println("Debug: ...");

というようにしてデバッグコードを記載すればよい。

次に.optionsファイルに記載された項目のON/OFFを切り替える方法だが,もちろん.optionsファイルを直接書き換えてtrue/falseを切り替えてもよい。しかし,Eclipseではプラグインを試験するためのRun-time Workbench実行では,この.optionsファイルに記載された値をGUIを用いて変更することができる

[Run]-[Run...]メニューで表示される設定画面のRun-time Workbench項目の中には,Tracingという項目がある。Enable tracingチェックボックスにチェックを入れ,プラグインを選択すると,そのプラグインの.optionsファイルに記載された項目が表示される。そのValue列をクリックして,true/falseを切り替えることができ,Run-time Workbenchの実行時にtrueにした項目が有効になる。

さて,上記の機構は基本的にはRun-time Workbenchによる実行時の話であるが,一旦プラグインがリリースされればRun-time Workbenchによる実行ではなく,通常のEclipseの起動によってプラグインが組み込まれる。通常の起動では,.optionsファイルが読み込まれることはなく,一般的にデバッグ機能は有効にならない。しかし,以下のようにEclipseの起動時にオプションを記述すれば.optionsファイルを読み込ませることができ,.optionsファイル中のtrueの項目に関して,デバッグ機能を有効にすることができる。

  eclipse.exe -debug C:\eclipse\plugins\yoichiro.myPlugin\.options

プラグインのリリース後に問題が発生した場合は,上記のコマンドでユーザに起動してもらい,デバッグ情報を採取すると良いだろう。

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

2004.03.27

エラーログの出力

Eclipseにおいて,発生したエラーの状況がIStatusオブジェクトによって表現され,CoreException例外にIStatusオブジェクトが保持されて伝達される(「CoreExceptionとIStatus」参照)。そして,IStatusオブジェクトの内容をエラーダイアログに簡単に表示することができた(「エラーダイアログの表示」参照)。

しかし,エラーダイアログが表示できない(表示することがふさわしくない)状況も当然存在する。そんなときに頼るのがロギング。ちゃんと起きたことは残しときましょう,という機構。エラーダイアログは閉じてしまえばそれまでだが,同時にログに記録しておけば,ログをクリアしない限りいつでも見ることができるようになる。

IStatusオブジェクトがエラーダイアログに統合され簡単に表示できたように,IStatusオブジェクトの内容をログに記録することも簡単にできるようになっている

  try {
    ...
  } catch(CoreException e) {
    Plugin plugin = ...;
    ILog logger = plugin.getLog();
    IStatus status = e.getStatus();
    logger.log(status);
  }

ログの出力は,ILogインタフェースにより規定されているILogオブジェクトの取得は,Pluginオブジェクトが必要である。Pluginクラス(のサブクラス)を持たないプラグインでは,基本的にロギングはできないので,ちゃんとプラグインクラスを準備する必要がある(他のプラグインを強引に取得して出力もできるが,これはマナー違反だろう)。「Pluginクラスの作成」で取り上げたような感じでPluginクラスを作成し,MyPlugin.getInstance() という呼び出しを使って,簡単に任意の場所でPluginオブジェクトを取得できるようにしておくと良いだろう。

話を元に戻そう。Pluginオブジェクトを取得できたら,getLogメソッドを呼び出してILogオブジェクトを取得する。そして,ロギングの対象であるIStatusオブジェクトをCoreExceptionオブジェクトのgetStatusメソッドで取り出す。あとはILogオブジェクトのlogメソッドにIStatusオブジェクトを渡すことによって,IStatusオブジェクトの内容がログに書き出される

さて,一体どこに書き出されたのかと言うと,ちゃんとファイルに出力されている。ログのファイルのパスは,
  $ECLIPSE_HOME/workspace/.metadata/.log
である。Run-time workbenchを使った場合は,runtime-workspaceディレクトリになる。

そして,上記のログファイルの内容は,PDE Runtime/Error Logビューを使って閲覧できる。さらに,各ログ項目をダブルクリックすることにより,ログの詳細を閲覧できる。

error-log.gif

error-log-detail.gif

詳細の表示画面では,IStatusオブジェクトが持つ例外のスタックトレースなどが表示されていることがわかるだろう。

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

2004.03.26

エラーダイアログの表示

CoreExceptionとIStatus」で紹介したように,Eclipseでは例外のほとんどがCoreExceptionとIStatusの組み合わせで成り立っている。一般的に,例外が発生したときには,ユーザにそのことを通知するためのダイアログを表示する。CoreException例外の発生時には,それが持つIStatusオブジェクトの内容をエラーダイアログに表示することになるが,EclipseではIStatusオブジェクトの内容をエラーダイアログとして簡単に表示するためのErrorDialogクラスが用意されている

ErrorDialogを使ってIStatusオブジェクトの内容をエラーダイアログとして表示する手順は,非常に簡単である。

  try {
    ...
  } catch(CoreException e) {
    Shell parentShell = ...;
    IStatus status = e.getStatus();
    ErrorDialog.openError(
      parentShell, "エラー", "エラーメッセージ", status);
  }

まず,ダイアログの表示に必要な,ダイアログの親となるShellオブジェクトを取得する。そして,キャッチしたCoreException例外オブジェクトのgetStatusメソッドを使って,IStatusオブジェクトを得る。あとは,ErrorDialogクラスのクラスメソッドであるopenErrorメソッドに「Shellオブジェクト」「タイトルに表示する文字列」「エラーの概要を示すメッセージ」「IStatusオブジェクト」を渡して呼び出せば,以下のようなエラーダイアログが表示される。「Reason」のところに表示されているメッセージは,IStatusオブジェクトのgetMessageメソッドの戻り値である。

error-dialog.gif

もし,openErrorメソッドに渡しているIStatusオブジェクトのエラーレベル(正確にはステータスレベル)がIStatus.ERROR以外だった場合は,ちゃんとそれ相応のダイアログが表示される(ダイアログのアイコンが変わる)。

[IStatus.WARNINGだった場合]
warning-dialog.gif

[IStatus.INFOだった場合]
information-dialog.gif

うまくできていることに,IStatus.OKだった場合は,openErrorメソッドを呼び出してもダイアログは表示されない。「OKなんだからエラーダイアログを表示しなくてもいいじゃん」っていう考えだと思われる。

ちなみに,ダイアログの親となるShellオブジェクトを渡すところでnullを渡しても,一応「親のいないダイアログ」が表示されるが,あまりお勧めできることではない。これについては「Shellオブジェクトの取得」を参照されたし。

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

2004.03.25

CoreExceptionとIStatus

EclipseプラットフォームのAPIを見ていると,いたるところで出てくるのがCoreException例外クラス。プラグインを開発していると,頻繁に,try { ... } catch(CoreException e) { ... } している自分がそこにいる。

一般的に,Javaでは例外クラスの考え方は大きく2つに分けられる。

一つは,正常系の処理から外れた状況の数だけ例外クラスをどんどん作る考え方。その状況がある程度グループ化されるのであれば,そのグループとなる親クラスを作って,具体的なエラー状況ごとにサブクラスを作成する。例えば,入出力関係の例外というグループ的な意味でIOException例外クラスがあり,その具体的な状況(ファイルが見つからない,など)の表現として,IOException例外クラスを継承したFileNotFoundException例外クラスがある,という感じである。どんなエラーなのかは,キャッチする例外の種別で判断する。

もう一つは,SQLException例外クラスのようなものもある。データベースに関するエラーとしてSQLException例外クラス一つしかなく,具体的なエラーの状況はgetErrorCodeメソッドの戻り値で判断する,という感じのものである。

Eclipseプラットフォームでは,後者の考え方が取り入れられている。つまり,Eclipse内での例外は,ほとんどの場合CoreException例外クラス一つでまかなわれている。では,どんな事態になってCoreException例外がおきたのかということを判断するにはどうしたらいいかというと,CoreException例外クラスが持つIStatusオブジェクトの中身を見てどんなエラーなのかどうかを判断するようになっている。CoreException例外クラスは,IStatusオブジェクトを例外発生元から例外処理側に運ぶためのコンテナと考えることができる。その証拠に,CoreException例外オブジェクトの生成には,必ずIStatusオブジェクトが必要である。

IStatusインタフェースでは,いくつかの情報を提供する処理が規定されている。CoreException例外に格納する目的で使用される場合,IStatusオブジェクトが持つ情報は以下のものになる。

  ・エラーが発生したプラグインのID (必須)
  ・エラーコード (必須)
  ・対人間用のメッセージ (必須)
  ・エラーレベル (必須)
  ・エラーの原因となった例外オブジェクト (任意)

エラーレベルは,IStatusインタフェースに定義されている以下の定数から選択する。

  ・IStatus.INFO - 情報
  ・IStatus.WARNING - 警告
  ・IStatus.ERROR - エラー

自分でCoreException例外をスローするときは,IStatusインタフェースの実装クラスを自作する必要は通常なく,プラットフォームが提供してくれているStatusクラスを使えばよい。以下に,CoreException例外を自分でスローする場合の例を示す。

  int ERROR_CODE_FILE_NOT_FOUND = 35;

  public void lodaFile(String fileName) throws CoreException {
    try {
      ...
    } catch(FileNotFoundException exception) {
      Plugin plugin = ...;
      String pluginId = plugin.getDescriptor().getUniqueIdentifier();
      IStatus status = new Status(
        IStatus.ERROR,
        pluginId,
        ERROR_CODE_FILE_NOT_FOUND,
        "File not found. " + fileName,
        exception);
      throw new CoreException(status);
    }
  }

上記では,ファイルを読み込む処理がFileNotFoundException例外の発生によって失敗したときにCoreException例外をスローする,という例である。PluginクラスのgetDescriptorメソッドでIPluginDescriptorオブジェクトを取得し,そのgetUniqueIdentifierメソッドを呼び出すことによって,プラグインのIDを取得できる。あとは,Statusクラスのコンストラクタに各種情報を渡してIStatusオブジェクトを生成し,CoreException例外クラスのコンストラクタにIStatusオブジェクトを渡して,できあがったCoreExceptionオブジェクトをスローしている。

ところで,IStatusインタフェースでは,IStatus.OKという定数も定義されているのだが,上記の説明ではIStatus.OKの存在に違和感を感じてしまうかもしれない。CoreException例外に格納されるという前提でステータスコードを考えてしまうとIStatus.OKは浮いてしまうのだが,IStatusオブジェクトは「ある状況」を汎用的に表現することを目的としたものであり,それはエラーという状況のみに限定されない,ということである。

さて,CoreException例外をキャッチする側はどうするのかというと,普通はエラーダイアログを表示したり,ログにエラーを書き出すことになる。

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

2004.03.24

プロパティ・ページの作成方法

Package ExplorerビューやNavigatorビューのコンテキストメニューの一番下に必ず表示されている「Properties」メニュー項目。Eclipseでは,IResourceオブジェクトであれば何でもプロパティを持つことができる。そのプロパティ値の編集を行う画面の呼び出しに使うのが「Properties」メニューである。ここでは,「Properties」メニューにより表示されるプロパティ・ページの作成方法を紹介する。

新規にプロパティ・ページを定義するには,org.eclipse.ui.propertyPages拡張ポイントを使用してプラグイン・マニフェストに記述を行う

  <extension point="org.eclipse.ui.propertyPages">
    <page
      id="yoichiro.myPlugin.myPropertyPage"
      name="My property page"
      objectClass="org.eclipse.jdt.core.IJavaProject"
      class="yoichiro.MyPropertyPage">
    </page>
  </extension>

org.eclipse.ui.propertyPages拡張ポイントでは,page要素を使って新規に作成したいプロパティ・ページの定義を行うid属性にプロパティ・ページを識別するためのID文字列を,name属性にプロパティ・ページの名前を記述する。プロパティ・ページの名前は,画面上のプロパティ・ページのタイトル部に表示される

objectClass属性は,このプロパティ・ページを適用するオブジェクトの種類を指定する。上記ではIJavaProjectクラス,つまりJavaプロジェクトに対してのみ適用されるプロパティ・ページということになる。そしてclass属性にプロパティ・ページの実装クラスを指定する。

プロパティ・ページのクラスは,IWorkbenchPropertyPageインタフェースを実装して作成するのだが,通常はプラットフォームが用意してくれているPropertyPageクラスを継承して作成する

  public class MyPropertyPage extends PropertyPage {
    public MyProperty() {
      super();
    }
    protected Control createContents(Composite parent) {
      // プロパティ・ページのGUI作成処理
    }
  }

基本的にはこれだけ。createContentsメソッドでプロパティ・ページの画面をSWTを使ってせっせと構築する。実は,「return null;」だけでも,下図のような空の画面が表示されるようになる。

property-page.gif

プロパティ・ページでは,自動的に[OK]ボタンと[Cancel]ボタンが提供される。これらのボタンが押されたときには,下記のメソッドがプラットフォームから呼び出されるので,その中にそれぞれの処理を記述する。

  ・[OK]ボタンが押されたとき - public boolean performOk() { ... }
  ・[Cancel]ボタンが押されたとき - public boolean performCancel() { ... }

上記のメソッドの結果として false を返却したときは,プロパティを確定しちゃいけない状況とプラットフォームは判断し,プロパティ・ページのダイアログは閉じずにそのままとなる。

プロパティ・ページでは,プロパティ・ページのダイアログを閉じずに「プロパティ値を初期値に戻す」「プロパティ値を確定させる」動作を行うことができる。それぞれ[Restore Defaults]ボタン,[Apply]ボタンがそれである。これらが押されたときには,下記のメソッドが呼び出されるので,その中で所定の処理を記述する。

  ・[Restore Defaults]ボタンが押されたとき - protected void performDefaults() { ... }
  ・[Apply]ボタンが押されたとき - protected void performApply() { ... }

なお,noDefaultAndApplyButtonメソッドをcreateContentsメソッド内で呼び出してあげると,[Restore Defaults]ボタンと[Apply]ボタンが表示されなくなるので,必要ないときは消してしまうことが可能である。

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

2004.03.21

ビルダー登録の担当者は?

新規ビルダーの定義」で紹介したビルダー。このビルダーのプロジェクトへの登録方法は「ビルダーの登録方法」で取り上げたが,一体どこでビルダーを登録する処理を書けばよいのだろうか?

実は「プロジェクト・ネーチャーの定義方法」で答えを書いているのだが,プロジェクトにビルダーを登録する処理は,プロジェクト・ネーチャーの仕事である。もちろんそうでない場合もあるが,99%はプロジェクト・ネーチャーの中でプロジェクトにビルダーを登録する処理を記述する。

つまり,こんな感じになる。

  public class MyNature implements IProjectNature {
    private IProject project;
    ...
    public void configure() throws CoreException {
      // projectに対してビルダーを登録する
    }
    public void deconfigure() throws CoreException {
      // projectからビルダーを解除する
    }
  }

プロジェクトにビルダーを登録すること,それこそが「プロジェクトに特徴付けを行う」ことである。プロジェクトに登録付けを行うものといえば,プロジェクト・ネーチャーである。つまり,
  (1) 新規プロジェクト作成ウィザードで,プロジェクトにプロジェクト・ネーチャーが登録される。
  (2) プロジェクト・ネーチャーにより,ビルダーが登録される。
  (3) ビルダーがリソースを変換する。
という流れになる。

何らかのビルダーを自作したら「プロジェクト・ネーチャーも作らなくっちゃ」ということになると思って,間違いない。逆に「プロジェクト・ネーチャーはプロジェクトにビルダーを登録するものである」と思っていても,間違いない。

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

ビルダーの解除方法

ビルダーの登録方法」を紹介したら,解除方法を紹介しない理由はないだろう。ここでは,プロジェクトに登録されているビルダーを登録解除する手順を紹介する。

ビルダーはIProjectDescriptionオブジェクトにICommandオブジェクトの配列として登録されている。「プロジェクト・ネーチャーの解除方法」のときと同じように,解除したいビルダーのICommandオブジェクトを配列から削除することで,ビルダーを解除できる

  String removeBuilderId = "yoichiro.myPlugin.myBuilder";

  IProject project = ...;
  IProjectDescription description = project.getDescription();
  ICommand[] commands = description.getBuildSpec();
  for (int i = 0; i < commands.length; i++) {
    if (commands[i].getBuilderName().equals(removeBuildName)) {
      ICommand[] newCommands = new ICommand[commands.length - 1];
      System.arraycopy(commands, 0, newCommands, 0, i);
      System.arraycopy(commands, i + 1, newCommands, i, commands.length - i - 1);
      description.setBuildSpec(newCommands);
      project.setDescription(description, null);
      return; // or break;
    }
  }

IProjectDescriptionオブジェクトからgetBuildSpecメソッドで,登録されているビルダーのICommandオブジェクトの配列を取得する。その後,配列の要素ごとにgetBuilderNameメソッドでビルダーのIDを取得して,解除したいビルダーを見つける。もし見つかったら,あとはそのICommandオブジェクトを除外したICommandオブジェクトの配列を生成し,IProjectDescriptionオブジェクトのsetBuildSpecメソッドに新しい配列をセットする。そして最後にIProjectDescriptionオブジェクトをIProjectオブジェクトのsetDescriptionメソッドに渡して,完了。

上記の処理を行うことにより,ビルダーは解除され,.projectファイルからも該当ビルダーの記述は削除される。

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

ビルダーの登録方法

新規ビルダーの定義」で紹介したビルダーは,プロジェクトに登録されることにより機能する。ここでは,ビルダーをプロジェクトに登録する方法を紹介する。

プロジェクト・ネーチャーの適用方法」では,プロジェクト・ネーチャーはIProjectDescriptionオブジェクトを使用して登録を行った。プロジェクト・ネーチャーの登録方法と同じように,ビルダーの登録はIProjectDescriptionインタフェースを使って行う

  String newBuilderId = "yoichiro.myPlugin.myBuilder";

  IProject project = ...;
  IProjectDescription description = project.getDescription();
  ICommand[] commands = description.getBuildSpec();
  for (int i = 0; i < commands.length; i++) {
    if (commands[i].getBuilderName().equals(newBuilderId)) {
      return; // ビルダー登録のキャンセル
    }
  }
  ICommand command = description.newCommand();
  command.setBuildName(newBuilderId);
  ICommand[] newCommands = new ICommand[commands.length + 1];
  System.arraycopy(commands, 0, newCommands, 0, commands.length);
  newCommands[commands.length] = command;
  description.setBuildSpec(newCommands);
  project.setDescription(description, null);

プロジェクト・ネーチャーの場合は,プロジェクトに登録されているプロジェクト・ネーチャーのIDの配列がIProjectDescriptionオブジェクトに格納されていたが(「プロジェクト・ネーチャーの適用方法」参照),ビルダーの場合は,各ビルダーがICommandオブジェクトで表現され,ICommandオブジェクトの配列がIProjectDescriptionオブジェクトに格納される。つまり,新規にビルダーを登録したいときは,そのビルダーを表すICommandオブジェクトをIProjectDescriptionオブジェクトが持つ配列に追加すればよい

最初に,既に登録したいビルダーがプロジェクトに適用されているかどうかをチェックする。登録されているビルダーのICommandオブジェクトの配列は,IProjectDescriptionオブジェクトのgetBuildSpecメソッドにより取得できる。そして,得た配列の要素の中で,登録したいビルダーのIDを持つICommandオブジェクトがあるかどうかを見る。ビルダーのIDはICommandオブジェクトのgetBuilderNameメソッドで取得することができる。もし見つかれば,登録処理は行わない。

登録されていなければ,新規にビルダーを登録する。新規ビルダー用に,新しくICommandオブジェクトを生成する。ICommandオブジェクトの生成は,IProjectDescriptionオブジェクトのnewCommandメソッドを使用する。そして,生成したICommandオブジェクトのsetBuilderNameメソッドに登録したいビルダーのID文字列をセットする。あとは「プロジェクト・ネーチャーの適用方法」のときと同じように,新規にICommandオブジェクトの配列を生成し,元々登録されていたICommandオブジェクトをコピーし,新規に生成したICommandオブジェクトを最後の要素にセットする。さらにIProjectDescriptionオブジェクトのsetBuildSpecメソッドに,新規ビルダーを追加したICommandオブジェクトの配列をセットして,最後にIProjectオブジェクトにIProjectDescriptionオブジェクトをsetDescriptionメソッドでセットして,新規ビルダーの登録が完了となる。

上記の処理により,プロジェクトにビルダーが追加される。もちろん,登録されたビルダーは.projectファイルに追記される

  <?xml version="1.0" encoding="UTF-8"?>
  <projectDescription>
    <name>TestProject</name>
    <comment></comment>
    <buildSpec>
      ...
      <buildCommand>
        <name>yoichiro.myPlugin.myBuilder</name>
        <arguments>
        </arguments>
      </buildCommand>
    </buildSpec>
    ...
  </projectDescription>

このように,ビルダーの登録方法はプロジェクト・ネーチャーの適用方法とほとんど同じである。

・・・長文になってしまった。反省。

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

新規ビルダーの定義

Eclipseを使っている人のほとんどが,Javaプログラムの開発のためにEclipseを使用している。Eclipse JDTでは,コンパイルという作業は完全にEclipseの裏方作業であり,Eclipse使用者が明示的にコンパイル作業を行う必要がない。ソースコードを変更するとすぐに裏でコンパイルが実装され,エラーがあれば自動的にエディタ上にマーカーが表示される。

ソースコードをコンパイルしてクラスファイルを生成するように,あるリソースを元にして新規のリソースを作成する機能のことをビルダーと呼ぶ。ビルダーはプラグインにより定義され,プロジェクトに対して割り当てられる。例えば,JavaプロジェクトはJDTプラグインに定義されているJavaビルダーが割り当てられている。Javaビルダーは,変更されたソースファイルに対してコンパイルを行い,その結果をエディタに対してマーカーにより反映する処理を行っている。

では,新規にビルダーを定義する方法を紹介する。ビルダーはorg.eclipse.core.resources.builders拡張ポイントを使用してプラグイン・マニフェストに定義される

  <extension
      point="org.eclipse.core.resources.builders"
      id="myBuilder"
      name="My builder">
    <builder>
      <run class="yoichiro.MyBuilder"/>
    </builder/>
  </extension>

id属性はビルダーに付与するIDを,name属性はビルダーの名前を記述する。上記のプラグイン・マニフェストがyoichiro.myPluginというIDのプラグインに定義されていれば,このビルダーはyoichiro.myPlugin.myBuilderというIDになる。ビルダーの実体は,builder要素の子要素としてrun要素を定義し,そのclass属性の値として実装クラスを指定する。

ビルダーの処理を記述するクラスは,IncrementalProjectBuilder抽象クラスの実装クラスとして作成する

  public class MyBuilder extends IncrementalProjectBuilder {
    public MyBuilder() {
      super();
    }
    protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
        throws CoreException {
      IProject target = getProject();
      // targetに対するビルド処理
    }
  }

ビルド作業は,buildメソッドに記述する。ビルダーのオブジェクトはJavaのリフレクションによりインスタンスが生成されるので,引数なしのコンストラクタが必要となる(やることがなければ上記のように再定義する必要はない)。

このビルダーはEclipseプラットフォームにより自動的に利用される。この際,Eclipseプラットフォームはビルド対象のプロジェクトをビルダーにセットする。buildメソッドの中では,getProjectメソッドによりビルド対象のプロジェクトオブジェクトを取得し,プロジェクトが持つリソースに対してビルド処理を行えばよい。

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

2004.03.17

プロジェクト・ネーチャーの解除方法

プロジェクト・ネーチャーの適用方法」を紹介したが,その解除方法も気になるところだろう。ここではプロジェクト・ネーチャーの解除方法を紹介する。

プロジェクト・ネーチャーの適用は,適用したいプロジェクト・ネーチャーのIDをIProjectDescriptionオブジェクトが持っている配列に追加することで実現できた。ということは,適用されているプロジェクト・ネーチャーの解除は,解除したいプロジェクト・ネーチャーのIDをIProjectDescriptionオブジェクトが持つ配列から除外することで実現できる。理屈は至極単純である。

  String removeNatureId = "yoichiro.myPlugin.myNature";

  IProject project = ...;
  IProjectDescription description = project.getDescription();
  String[] ids = description.getNatureIds();
  for (int i = 0; i < ids.length; i++) {
    if (ids[i].equals(removeNatureId)) {
      String[] newIds = new String[ids.length - 1];
      System.arraycopy(ids, 0, newIds, 0, i);
      System.arraycopy(ids. i + 1, newIds, i, ids.length - i - 1);
      description.setNatureIds(newIds);
      project.setDescription(description);
      return; // or break;
    }
  }

最初にIProjectオブジェクトのgetDescriptionメソッドでIProjectDescriptionオブジェクトを取得し,そのgetNatureIdsメソッドを呼び出して適用されているプロジェクト・ネーチャーのIDの配列を取得している。そして,配列の中に解除したいプロジェクト・ネーチャーのIDがあるかどうかをチェックする。もし見つかったら,発見場所のインデックスを境に,その前後の要素を新しい配列にコピーして,解除したいプロジェクト・ネーチャーのIDを除外した配列を作成する。

あとは「プロジェクト・ネーチャーの適用方法」のときと同じように,IProjectDescriptionオブジェクトのsetNatureIdsメソッドで新しいIDの配列をセットし,IProjectDescriptionオブジェクトをIProjectオブジェクトのsetDescriptionメソッドに渡して,プロジェクト・ネーチャーのIDの配列の更新を反映させる。

上記の処理が完了すれば,プロジェクトからプロジェクト・ネーチャーが解除され,.projectファイルからも解除したプロジェクト・ネーチャーに関する記載が削除される。

プロジェクト・ネーチャーを解除しなければならないときって,そう多くはないはず。というか,ほぼないと思って間違いないだろう。プロジェクトの特徴が途中で変更になることってないだろうし。上記の処理は知っていて損はないが,きっと使わないもの,だと思う。

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

2004.03.16

プロジェクト・ネーチャーの適用方法

プロジェクト・ネーチャーの定義方法」において解説したプロジェクト・ネーチャーだが,プロジェクトへの適用方法はまだ紹介していなかった。ここでは,プロジェクト・ネーチャーのプロジェクトへの適用方法について解説する。

プロジェクトを表すクラスはIProjectクラスだが,そのプロジェクトの名前や説明文など,プロジェクト自身を説明するための情報(メタデータ)はIProjectクラスから直接取得することができない。その代わりに,プロジェクトの説明情報を扱うためのAPIが,IProjectDescriptionインタフェースに規定されている。IProjectDescriptionオブジェクトは,IProjectオブジェクトのgetDescriptionメソッドを使用することによって取得することができる

プロジェクトにどんなプロジェクト・ネーチャーが適用されているか,という情報についても,IProjectDescrptionオブジェクトが保持している。もう少し細かく言うと,プロジェクト・ネーチャーはそれを特定するためのIDが定義されるが,IProjectDescriptionオブジェクトは,プロジェクトに適用されているプロジェクト・ネーチャーのID文字列の配列を保持している

つまり,新規にプロジェクト・ネーチャーを適用したい場合は,適用したいプロジェクト・ネーチャーのIDを,IProjectDescriptionオブジェクトが保持しているIDの配列に追加してあげればよい。逆に,あるプロジェクト・ネーチャーの適用を解除したければ,IProjectDescriptionオブジェクトが保持しているIDの配列から,解除したいプロジェクト・ネーチャーのIDを削除すればよい。

例として,「プロジェクト・ネーチャーの定義方法」で新規に定義したyoichiro.myPlugin.myNatureというIDのプロジェクト・ネーチャーを,あるプロジェクトに適用するための手順を以下に示す。

  String newNatureId = "yoichiro.myPlugin.myNature";

  IProject project = ...;
  IProjectDescription description = project.getDescription();
  if (!description.hasNature(newNatureId)) {
    String[] ids = description.getNatureIds();
    String[] newIds = new String[ids.length + 1];
    System.arraycopy(ids, 0, newIds, 0, ids.length);
    newIds[ids.length] = newNatureId;
    description.setNatureIds(newIds);
    project.setDescription(description);
  }

重複登録を避けるため,最初にhasNatureメソッドを用いて既に登録されているかどうかをチェックしている。その後,getNatureIdsメソッドにより,適用されているプロジェクト・ネーチャーのIDの配列を取得し,その個数+1個の文字列配列を新規に作成,続いて配列の内容をコピーしている。そして,新規に生成した配列の最後の要素に,適用したいプロジェクト・ネーチャーのIDをセットしている

新規に適用するプロジェクト・ネーチャーのIDが追加された配列をsetNatureIdsメソッドに渡してIProjectDescriptionオブジェクトにセットし,IProjectDescriptionオブジェクトをIProjectオブジェクトのsetDescriptionメソッドに渡してセットして完了となる。setDescriptionして初めてプロジェクト・ネーチャーが適用される点に注意。

一度上記の処理を行ってプロジェクト・ネーチャーを適用すれば,そのプロジェクトを閉じようが,Eclipseを終了しようが,プロジェクトを開いたときにプロジェクト・ネーチャーの適用は継続される。なぜかというと,setDescriptionメソッドでIProjectDescriptionオブジェクトをセットしたと同時に,それが持つ情報が.projectファイルに書き込まれるからである。

  <?xml version="1.0" encoding="UTF-8"?>
  <projectDescription>
    <name>TestProject</name>
    <comment></comment>
    ...
    <natures>
      <nature>org.eclipse.jdt.core.javanature</nature>
      <nature>yoichiro.myPlugin.myNature</nature>
    </natures>
  </projectDescription>

さて,まだ疑問に思うことがあるだろう。プロジェクト・ネーチャーを適用するのは,一体いつなのか?これの明確な正解はなく,もちろん任意のタイミングで適用させることができる。しかし,一般的にはプロジェクトが作成されるタイミング,すなわち新規プロジェクト作成ウィザードの処理の中で行うことがほとんどである。

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

2004.03.15

プロジェクト・ネーチャーの定義方法

Eclipse(Platform,JDTおよびPDEが導入されている環境)では,以下のプロジェクトを作成できる。
  ・Java (Java Project)
  ・Plug-in Development (Plug-in Projectなど)
  ・Simple (Project)
Platformが標準で提供している単純なプロジェクト(Simple)に比べて,Java ProjectやPlug-in Projectはそれぞれ固有の機能拡張がなされている。Java ProjectであればJavaソースファイルを自動的にコンパイルしてくれるし,Plug-in ProjectはJava Projectに加えてプラグイン・マニフェストを自動的にチェックしてくれる(両方とも「ビルダー」と呼ぶ)。

あるプロジェクトに対して機能追加などの「特徴付け」を行うには,プロジェクト・ネーチャー(Project Nature)という機構を使用する。先ほどの機能拡張はプロジェクト・ネーチャーが行っているものであり,例えばJava NatureがJava Builder(自動コンパイル機能など)をプロジェクトに付加することで,プロジェクトをJavaプロジェクトとしている。

新規にプロジェクト・ネーチャーを定義するには,org.eclipse.core.resources.natures拡張ポイントを使用する

  <extension
      point="org.eclipse.core.resources.natures"
      id="myNature"
      name="My nature">
    <runtime>
      <run class="yoichiro.MyNature"/>
    </runtime>
  </extension>

id属性に新規プロジェクト・ネーチャーを識別するための文字列を,name属性に新規プロジェクト・ネーチャーの名前を記述する。このプロジェクト・ネーチャーのIDは,例のごとく「プラグインID+ネーチャーID」であり,上記のプラグイン・マニフェストがyoichiro.myPluginというIDのプラグインであれば,プロジェクト・ネーチャーのIDは「yoichiro.myPlugin.myNature」となる。プロジェクト・ネーチャーの実体は,runtime要素のrun子要素のclass属性を使って指定する

プロジェクト・ネーチャーの実体は,IProjectNatureインタフェースの実装クラスである。この実装クラス内で,プロジェクトに特徴付けを行う(ビルダーの登録処理がほとんど)。

  public class MyNature implements IProjectNature {
    private IProject project;
    public void setProject(IProject project) {
      this.project = project;
    }
    public IProject getProject() {
      return project;
    }
    public void configure() throws CoreException {
      // projectへ特徴付けを行う(ビルダーの登録など)
    }
    public void deconfigure() throws CoreException {
      // projectから特徴付けを解除する(ビルダーの削除など)
    }
  }

あるプロジェクトにこのプロジェクト・ネーチャーが登録されると,まずsetProjectメソッドに対象のプロジェクト(IProjectオブジェクト)が渡される(通常はフィールドに保持しておく)。そしてその後configureメソッドが呼び出される。このconfigureメソッド内で,保持しておいたprojectオブジェクトに対して,ビルダーの登録などの特徴付けの処理を行う

プロジェクトからプロジェクト・ネーチャーが解除されたときは,deconfigureメソッドが呼び出される。deconfigureメソッドでは,configureメソッド内で対象プロジェクトに登録したビルダーの登録解除など,特徴付けを解除する処理を行う

このように,プロジェクト・ネーチャーは,あるプロジェクトに対するライフサイクル(プロジェクト活性化の際はconfigureメソッドが呼ばれ,非活性化の際はdeconfigureメソッドが呼ばれる)を担当するということができる。

ここで,大きな疑問が浮かび上がる。「プロジェクト・ネーチャーをプロジェクトに関連付けるにはどうするの?いつやるの?」とお思いのことだろう。この話は後のお楽しみ,として今回はこれまで。

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

2004.03.11

型の仲間達(サブクラス,スーパークラス)の取得方法

型の発見方法で取得したITypeオブジェクト。それが表す型を起点として,そのスーパークラスやサブクラス(の列挙)を取得したいことが「ごくたまに」ある。例えば,junit.framework.TestCaseクラスのサブクラスの列挙を取得することによって,Javaプロジェクト内に存在するテストクラスの一覧を得る,などである。

ある型のスーパークラスやサブクラスを取得する,という処理は,対象となる型に強く依存した機能のはずなので,当然ITypeインタフェースに規定されていると思ってしまうが,ITypeインタフェースに直接関連のあるクラスを取得できるような機能は規定されていない。その代わりに,型の継承関係を表す階層構造への操作を規定したITypeHierarchyインタフェースが存在する

ある型を含む型の階層構造にアクセスしたいときは,ITypeインタフェースのnewTypeHierarchyメソッドを利用する。newTypeHierarchyメソッドにはいくつか種類があって,必要に応じて使い分ける。

  (1) newTypeHierarchy(IJavaProject project, IProgressMonitor monitor)
      - あるプロジェクトのクラスパス内に限定して,型の階層構造を構築する。
  (2) newTypeHierarchy(IProgressMonitor monitor)
      - ワークスペース内の全部の型を対象として,型の階層構造を構築する。

もうひとつnewTypeHierarchy(IWorkingCopy[] workingCopies, IProgressMonitor monitor)というメソッドもあるのだが,IWorkingCopyインタフェースがよくわからないので,用途不明。IWorkingCopyインタフェースのサブインタフェースとしてICompilationUnitインタフェース(Javaソースファイルを表す)があるので,特定のソース群を対象としてその中から型の階層構造を作り出すのかな,と勝手に想像するが,真相はわかりません。

newTypeHierarchyメソッドの結果のITypeHierarchyオブジェクトを得られれば,あとはgetAllSubtypesメソッドやgetAllSuperclassesメソッドを使って,サブクラスやスーパークラスのITypeオブジェクトの配列を得ることができる

  IJavaProject project = ...;
  IType baseType = ...; // 型の発見方法参照
  ITypeHierarchy hierarchy = baseType.newTypeHierarchy(project, null);
  // スーパークラス群の取得
  IType[] superclasses = hierarchy.getAllSuperclasses(baseType);
  // プロジェクト内を対象にしたサブクラス群の取得
  IType[] subclasses = hierarchy.getAllSubclasses(baseType);

メソッドの引数にIProgressMonitorオブジェクトがあることでもわかるように,newTypeHierarchyメソッドの実行には多くのコストが必要になる。型の継承関係を得るためには,特にサブクラスを見つけるためには,全ての型の情報を参照していかなければならないことは容易に想像つく。しかも,型の階層構造を求めなければならないことは,滅多にない。ITypeインタフェースから型の階層構造に関する処理をITypeHierarchyインタフェースに抜き出した理由は,利用頻度の少ない情報は極力ITypeインタフェースの実装に持たせないで軽くしておこうという配慮からだと思われる。

ちなみに,ある型のスーパークラス群だけを得たい(サブクラスはいらない)ときは,newSupertypeHierarchyメソッドを利用すべきである。このメソッドはnewTypeHierarchyメソッドよりも劇的にパフォーマンスはいいはず。なぜなら,スーパークラス群を求める処理は,対象となるJavaプロジェクト内に限定されるし,各型にはスーパークラスの指定が記載されているはずなので単純にその指定を追っていくだけで済むから,である。

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

2004.03.10

その型は誰の持ち物?

型の発見方法」で解説したITypeオブジェクトの取得方法だが,IJavaProject#findType()ではJavaプロジェクトのクラスパス全体から型を検索してしまう。つまり,ライブラリ(JARファイル)や,依存関係にある他プロジェクトのクラスがfindTypeメソッドの結果としてヒットしてしまうのである。あくまで対象のJavaプロジェクトで新規に作成された型のITypeオブジェクトを取得したいときにはどうしたら良いのだろうか?IJavaProjectオブジェクトからパッケージをたどってクラスを自分で見つけることもできなくもないが,せっかくあるIJavaProject#findType()を使って何とかしたい。

ITypeインタフェース(の親のIJavaElementインタフェース)に,getUnderlyingResourceメソッドがある。これは対象となるITypeオブジェクトを含んでいる最も根本的な(?)リソース(IResourceオブジェクト)を返してくれるメソッドである。このメソッドの結果を使うことによって,ITypeオブジェクトがどこに所属しているかを判断することができる。

例を見てみよう。

  [Javaプロジェクト]
    ・プロジェクトA - projectA
    ・プロジェクトB - projectB
      - 依存プロジェクト projectA
      - 依存ライブラリ hoge.jar

というプロジェクト構成で,この中のいずれかにyoichiro.Testクラスが存在するとする。そして,

  IJavaProject projectB = ...;
  IType type = projectB.findType("yoichiro.Test");
  IResource resource = type.getUnderlyingResource();

としてfindTypeメソッドの結果のITypeオブジェクトからgetUnderlyingResourceメソッドでIResourceオブジェクトを取得する。このresourceオブジェクトを以下のように判断することによって,存在場所を特定できる。

  (1) resourceオブジェクトがnullだった場合
      - 依存ライブラリに存在(例では依存ライブラリは1つなのでhoge.jar内)
  (2) resource.getProject().equals(projectB.getProject()) == true の場合
      - プロジェクトBに存在
  (3) resource.getProject().equals(projectB.getProject()) == false の場合
      - 依存プロジェクトに存在(例では依存プロジェクトは1つなのでプロジェクトA内)

IResourceクラスのgetProjectメソッドを使ってJavaプロジェクトの作成元のIProjectオブジェクトを取得し,ITypeオブジェクトから得たIProjectオブジェクトとIJavaProjectオブジェクトから得たIProjectオブジェクトが同一かどうかを判断している。上記の(1)と(2)を使えば,対象のプロジェクトで作成された型かどうかを判断することができる。

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

型の発見方法

Eclipse JDTでは,Javaの型(クラスやインタフェースなどのCompilation Unit)はITypeインタフェースで表現される。ITypeインタフェースはバイナリ(Javaクラスファイル)またはテキスト(Javaソースファイル)を指している。開発するプラグインの中で,型の情報を知りたいな,と思ったときは,その型のITypeオブジェクトを取得することが第一歩となる。

型は,Javaプロジェクト内またはプロジェクトに登録されているライブラリ(JARファイルなど)のどこかに存在する。つまり,ITypeオブジェクトを得るためには,Javaプロジェクトのオブジェクト,すなわちIJavaProjectインタフェースのオブジェクトをまずは得る必要がある(ビューアクションなどのElementオブジェクトから取得,などなど)。

IJavaProjectオブジェクトを得られれば,あとはfindTypeメソッドに得たい型のFQN(Fully Qualified Name:パッケージ名もちゃんとついた完全な名前)を渡せば,ITypeオブジェクトを得ることができる

  IJavaProject project = ...;
  IType type = project.findType("junit.framework.TestCase");

上記の例では,Javaプロジェクト内からJUnitのTestCaseクラスのITypeオブジェクトを探している。Javaプロジェクトのクラスパスにjunit.jarファイルが登録されていれば,TestCaseクラスのITypeオブジェクトを得ることができる。もちろんjunit.jarが登録されていなければTestCaseクラスを見つけることができないので,findTypeメソッドの結果はnullになる。

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

2004.03.05

APIリファレンスの抜き出し方法

EclipseのプラットフォームやJDTのAPIリファレンスは,HelpメニューのHelp Contentsから閲覧することができる。しかし,やっぱりAPIリファレンスの類は,普通にWebブラウザでいつでも閲覧できる状態にしておきたいものである。

Eclipseはプラグインの集合体であるので,Help Contentsで閲覧できるドキュメントはプラグインが提供してくれているものだと想像できる。実際そのとおりで,オンラインヘルプ用の拡張ポイントがあり,各種プラグインはその拡張ポイントを利用してオンラインドキュメントを提供している(オンラインヘルプの作成方法については,わかり次第紹介する予定)。オンラインドキュメントのファイル群は,docsディレクトリに配置するか,docsディレクトリを圧縮したdocs.zipというファイルに配置するかのいずれかである。

プラグインを開発するにあたり,最も頻繁に閲覧するものは,プラットフォームのAPIリファレンスと,JDTのAPIリファレンスだろう。これらのAPIリファレンスは,それぞれ以下のファイルに格納されている。

 ・プラットフォームのAPIリファレンス
    $ECLIPSE_HOME/plugins/org.eclipse.platform.doc.isv_2.1.0/docs.zip
    ZIPファイル内: /reference/api

 ・JDTのAPIリファレンス
    $ECLIPSE_HOME/plugins/org.eclipse.jdt.doc.isv_2.1.0/docs.zip
    ZIPファイル内: /reference/api

各ZIPファイルから,/reference/api以下を任意のディレクトリに展開すれば,その中にあるindex.htmlを閲覧することによって,各APIリファレンスをHelp Contentsを使わずに,Webで閲覧することができるようになる。

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

2004.03.04

マーカーレゾリューション

Eclipseの便利な機能に,Quick Fixがある。例えば,例外をcatchしなくちゃいけないのにそれをしなかった場合,コンパイルエラーの箇所で Ctrl + 1 キーを押すと「Add throws declaration」「Surround with try/catch」というポップアップメニューが表示され,「Surround with try/catch」を選べば自動的にtry-catchで括ってくれる,というのがQuick Fix機能である。

あるマーカー(特に何らかの問題を示すマーカー)に対して,マーカーを解決する(マーカーの原因となったコンパイルエラー箇所を自動修正してマーカーを削除できる状態にする,など)ための処理を自作し,何らかのマーカーに関連付けることができる。この,マーカーを解決するためのオブジェクトのことをマーカーレゾリューションと呼ぶ。マーカーレゾリューションは,Quick Fixの実装ということができる。

マーカーレゾリューションは,org.eclipse.ui.markerResolution拡張ポイントを利用して定義する

  <extension point="org.eclipse.ui.markerResolution">
    <markerResolutionGenerator
      markerType="yoichiro.myplugin.mymarker"
      class="yoichiro.MyMarkerResolutionGenerator">
      <attribute name="myattribute" value="myvalue"/>
    </markerResolution>
  </extension>

org.eclipse.ui.markerResolution拡張ポイントでは,マーカーレゾリューションの定義をmarkerResolutionGenerator要素を使って記述する。「なんでmarkerResolution要素じゃないの?」という疑問が沸くが,それは後述。

markerType属性で,対象マーカーのIDを記述する。そしてclass属性でマーカーレゾリューションの生成処理を持つクラスを指定する。

マーカーはそれぞれ属性(IMarker.LOCATIONなど)を持っているが,attribute要素を使うことによって,マーカーレゾリューションが有効になるための属性値を定義することができる。上記では,myattribute属性の値としてmyvalueを持つマーカーのみ有効,という指定を行っている。

さて,マーカーの解決方法は1つとは限らない。冒頭で紹介した「例外処理どうすんだエラー」では,「Add throws declaration」「Surround with try/catch」という2つの解決方法が存在した。つまり,なんでmarkerResolutionGeneratorという名前なのかというと,上記のclass属性で指定しているクラスは,あるマーカーに対する複数の解決処理を生成して返す処理を担当しているから,である。

class属性で指定するクラスは,IMarkerResolutionGeneratorインタフェースの実装クラスとして作成する。

  public class MyMarkerResolutionGenerator
      implements IMarkerResolutionGenerator {
    public IMarkerResolution[] getResolutions(IMarker marker) {
      IMarkerResolution resolution = new IMarkerResolution() {
        public String getLabel() {
          return "My resolution";
        }
        public void run(IMarker marker) {
          // 解決処理
        }
      };
      return new IMarkerResolution[]{resolution};
    }
  }

IMarkerResolutionGeneratorインタフェースの実装クラスでは,getResolutionsメソッドを実装しなければならない。getResolutionsメソッドの結果として,IMarkerResolutionインタフェースのオブジェクトの配列を返す。マーカーの解決処理は,IMarkerResolutionインタフェースの実装クラスとして作成する。つまり,マーカーの解決方法が複数ある場合は,それぞれIMarkerResolutionインタフェースの実装クラスとして作成し,それらのインスタンスの配列をgetResolutionsメソッドの戻りとして返す。上記ではIMarkerResolutionインタフェースの実装クラスを匿名クラスとして作成し,それを返却している。

IMarkerResolutionインタフェースの実装クラスでは,2つのメソッドを実装する必要がある。getLabelメソッドでは,マーカーレゾリューションを説明するための短い文字列(「Surround with try/catch」など)を返す。これが Ctrl + 1 キー押下時やQuick Fixメニュー選択時に表示されるポップアップメニューの選択肢の文字列になる。そして,その選択肢を選択した際に呼び出されるメソッドがrunメソッドである。runメソッド内に,マーカーを解決するための処理を記述する(解決しなくてもいいけど)。

このマーカーレゾリューションという機構は,マーカーが持つ属性の値をうまく利用することが求められるだろう。

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

2004.03.02

マーカーのアイコン

新規マーカーの作成」で定義したマーカーに,アイコンを登録することができる。

アイコンはもちろん自分で用意する必要あり。16x16のJPEGかGIFでイメージファイルを用意する。イメージファイルの配置場所は,
  $PLUGIN_HOME/icons
ディレクトリとするのが慣習らしい。

マーカーのアイコンは,「新規マーカーの作成」で紹介したorg.eclipse.core.resources.markers拡張ポイントの要素や属性で定義できるかと思いきや,そんなものはない。ではどうするのかというと,マーカーのアイコンはorg.eclipse.ui.markerImageProvider拡張ポイントを利用して定義する

  <extension point="org.eclipse.ui.markerImageProvider">
    <imageProvider
      markertype="yoichiro.plugin.mymarker"
      icon="icons/mymarker.gif"
      id="yoichiro.plugin.mymarker.icon"/>
  </extension>

org.eclipse.ui.markerImageProvider拡張ポイントでは,imageProvider要素を使ってアイコンの定義を行う。markertype属性には,対象とするマーカーのIDを,icon属性にはアイコンのイメージファイルをプラグインディレクトリからの相対パスで,id属性にはこのアイコンを特定するためのIDを記述する。これだけで,「リソースへのマーキング」を行って対象マーカーを作成した際に,エディタの左縦ルーラーにアイコンが表示されるようになる。

ちなみに,マーカーのアイコンを定義しただけで,「リソースへのマーキング」で問題にした「マーキングしても,エディタの左縦ルーラーにマーカーがすぐに描画されない」という問題は解決する。新規にマーカーを定義したときには,いっしょにアイコンも登録しておきましょう,ということです。

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

2004.03.01

新規マーカーの作成

自分なりの新しいマーカーを定義するには,プラグイン・マニフェストに新規マーカー拡張ポイントを利用して定義すればよい。記述内容は今までの中でもかなり単純な部類に入る。

  <extension point="org.eclipse.core.resources.markers"
      id="mymarker"
      name="My Marker">
    <super type="org.eclipse.core.resources.problemmarker"/>
    <super type="org.eclipse.core.resources.textmarker"/>
    <persistent value="true"/>
    <attribute name="myattribute"/>
  </extension>

新規マーカーは,org.eclipse.core.resources.markers拡張ポイントを利用する。id属性はマーカーを特定するためのIDを任意に指定するのだが,プレフィックスとしてプラグインのIDが付加される。例えば,上記の定義がyoichiro.mypluginというIDのプラグインのプラグイン・マニフェストに定義されれば,
  ・yoichiro.myplugin.mymarker
というIDになる。name属性には,このマーカーの名前を値として記述する。

新規マーカーは,他のマーカーを継承して定義する。多重継承が許されているので,上記のようにsuper要素を複数記述することができる。普通はIMarkerインタフェースに定義されている標準マーカーを継承すればよい。ほかには,JDTで定義されているマーカーを継承することもあるだろう。super要素のtype属性に,継承元のマーカーのIDを記述する

定義するマーカーが,一時的なものか,永続化(Eclipseを再起動してもマーカーが消えない)されるものかを,persistent要素で決定する。value属性にtrueを指定すれば永続化falseを指定すれば一時的なマーカーになる。

最後に,attribute要素を使って,マーカーが持つことができる属性を定義する。上記では,myattribute属性をmymarkerマーカーが持つことができると定義している。

これで新規マーカーの定義は完成。マーカーは特別何かクラスを作成する必要はないので,上記だけでOK。
  IResource resource = ...;
  resource.createMarker("yoichiro.myplugin.mymarker");
で,上記の新規マーカーのオブジェクトを作成することができる。

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

2004.02.25

標準マーカーの属性

標準マーカーで紹介した標準マーカーでは,それぞれいくつかの属性を持つように定義されている。標準マーカーの属性については,IMarkerインタフェースで定数として定義されている

各属性についての解説を以下に示す。カッコ内は属性値の型。

  ・transient - IMarker.TRANSIENT(boolean)
    永続可能マーカーに対して,永続化させないようにするかどうかのフラグ属性。
    falseを指定すれば,一時的なマーカーとなる。

  ・severity - IMarker.SEVERITY(int)
    重要度の値の属性。以下の値のどれかから選択。
      IMarker.SEVERITY_INFO - 情報
      IMarker.SEVERITY_WARNING - 警告
      IMarker.SEVERITY_ERROR - エラー

  ・message - IMarker.MESSAGE(String)
    説明文の属性。任意の文字列を指定可能。

  ・location - IMarker.LOCATION(String)
    位置を表す説明文の属性。任意の文字列を指定可能。
    プラットフォームで解釈させるわけではない。
    対人間用の説明文。

  ・priority - IMarker.PRIORITY(int)
    優先度の値の属性。以下の値のどれかから選択。
      IMarker.PRIORITY_LOW - 低
      IMarker.PRIORITY_NORMAL - 通常
      IMarker.PRIORITY_HIGH - 高

  ・done - IMarker.DONE(boolean)
    処理済のものかどうかのフラグ属性。
    Taskマーカーにおいて,タスクが完了したかどうかを指定する。

  ・userEditable - IMarker.USER_EDITABLE(boolean)
    マーカーについてユーザが編集できるかどうかのフラグ属性。
    プラットフォームで解釈されるわけではない。
    各種エディタやビューでuserEditable属性の解釈が行われる。

  ・character start -