NetBeans セレクション管理のチュートリアル II—ノードを使う方法
前回のチュートリアルでは、 TopComponent の Lookup からセレクト可能なオブジェクトを提供する方法や、フォーカス中のコンポーネントの Lookup に応じて変化するコンポーネントの実装方法など、コンポーネント間のセレクション管理の基礎について学びました。
今回は、より詳細なビューや、単にコンポーネントレベルにとどまらないセレクションを可能にするために、ノード API について学びます。もちろん Lookup を読み書きするコンポーネントを書き、より詳細なセレクションロジックを持たせることもできないわけではありません。しかし、ノード API はそのようなことが簡単に実現できるだけでなく、多くの利点を持っています。
1つ目の利点はノード API が提供するプレゼンテーションレイヤーです。このレイヤーは何らかの形で編集中のデータモデルと、このデータモデルをユーザーに見せる UI コンポーネントの間にある層です。これは、あるモデルを様々な形で、もしくは様々な UI で表現する上で、とても便利で強力なのです。
2つ目の利点はエクスプローラ API です。org.openide.explorer モジュールは、ツリーやリスト、ツリーテーブルなどの、ノードや、その子ノードを扱うことのできるコンポーネントを数多く提供しています。
ノードは、階層的なオブジェクトです。ノードは以下のものを持っています。
- 子ノード—ツリーに表示することができる、階層的に配下にあるノード
- アクション—ポップアップメニューに表示することができるアクション
- 表示名—UI コンポーネントに表示することができる、判読可能 (human-readable) で、ローカライズされた名前
- アイコン—UI コンポーネントに表示することができるアイコン
ノードは上のどれに対しても変更通知を発行することができ、エクスプローラ UI コンポーネントの表示は自動的に更新されるでしょう。
決して前回のチュートリアルが無意味だというわけではありません。むしろ、ノード API が動作可能な理由がそこにあるのですから。お察しの通り、org.openide.nodes.Node の getLookup() というメソッドがその理由です。事実、 IDE のプロジェクトタブで選択を変更した時何が起きているかというと...例えば、プロジェクトタブはトップコンポーネントです。Utilities.actionsGlobalContext() で取得した Lookup がフォーカス中の様々なコンポーネントの代わり (proxy) になり、フォーカスが移動する際に変更通知を発行するように、ツリー内で選択中のオブジェクトの Lookup の代わりとして使えます。
エクスプローラ API のコンポーネントのおかげで非常に簡単にツリーノードの体裁を整え、とても少ないコードでこのような代用 (proxying) を行うことができます。前回のチュートリアルに出てきた MyViewer のようなビューアタイプのコンポーネントは、自動的に選択変更を通知されるので、 エクスプローラコンポーネントで起きた選択変更に対応するために特に何かする必要はありません。
はじめに
あなたがその内容をよく知っているものとして、前回のチュートリアルのサンプルコードから始めます。
サンプルをダウンロードするにはここをクリックしてください。
エクスプローラビューの作成
まず最初にすることは、エディタコンポーネントMyEditor の大幅な変更です。MyEditor をエディタで開きます。- まず My Editor プロジェクトのプロパティーダイアログを開きます。「ライブラリ」カテゴリの「依存関係を追加」をクリックし、ダイアログに "BeanTreeView" と入力します。以下のように「エクスプローラおよびプロパティーシート API」が表示されるのを確認して「了解」をクリックします。これでエクスプローラ API モジュールへの依存関係が追加され、モジュールのクラスを使用することができます。
- 次にアクションハンドラメソッドの中身を削除します:
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { }さらにコンストラクタからこのメソッドの呼び出しを削除します。こうしておけば、このハンドラメソッドが関連付けされているボタンを削除した時、ハンドラメソッドも一緒に削除されるでしょう。 - フォームデザイナーに切り替え、コンポーネントのすべての部品を選択し、削除します。
- コンポーネントインスペクタで
TopComponentのノードを右クリックし、「レイアウトを設定」>「BorderLayout」を選択します:
- パレットウィンドウの Swing コンテナの「スクロール区画」をクリックし、フォーム上にスクロールペインをドロップします。すると、このスクロールペインはフォーム全体に配置されるでしょう。ここでの秘密は、すべてのエクスプローラ UI コンポーネントは
JScrollPaneのサブクラスであるということです。そのため、その生成コードを変更するだけでエクスプローラビューを作成することができます。 - 追加した
JScrollPaneを右クリックし、「コードのカスタマイズ」を選択します。コードカスタマイザで、new BeanTreeView()を追加して、最初の行を変更します:
BeanTreeViewはエクスプローラ API のコンポーネントで、ポップアップメニューや検索機能などが内臓された、ノードとその子ノードを表示する基本的なJTreeベースのビューです。 - 下のようにインポート文の追加が必要になるので、ソースエディタに切り替え、 Ctrl-Shift-I キーを押して BeanTreeView をインポートします:
- 次にツリーに何か表示するものを追加します。エクスプローラ UI コンポーネントは、コンテナに追加されると、コンテナやそのスーパークラスが
ExplorerManager.Providerを実装していないか探します。ですから直接コンポーネントに表示するノードを追加するのではなくて、コンポーネントマネージャーに追加します。こうすることでマスター/詳細ビューなど、1つのマネージャーが管理する複数のビューに表示することが可能になります。MyEditor のシグネチャに以下のように追加します:public class MyEditor extends TopComponent implements ExplorerManager.Provider {Ctrl-Shift-I キーを押してインポートを修正します。シグネチャの行にカーソルを置いたままにしていると、欄外に電球が現れるはずです。Alt-Enter キーを押して、「すべての抽象メソッドの実装」を実行します。すると、getExplorerManager()というメソッドが追加されるでしょう。これを以下のように実装します:private final ExplorerManager mgr = new ExplorerManager(); public ExplorerManager getExplorerManager() { return mgr; } - ゴールは複数の
APIObjectを表示できるコンポーネントなのですから、1つか2つのノードが必要です。それぞれAPIObjectインスタンスを所有することになるでしょう。まずはツリービューにルートノードを作成するコードを追加します。コンストラクタに以下の行を追加します:mgr.setRootContext(new AbstractNode(new MyChildren()));
このコードではMyEditorのすべてのエクスプローラビューのルートノードを設定しています。 - インポートを修正しようとすると、
AbstractNodeもMyChildrenも見つけられないとのエラーダイアログが表示されるでしょう。AbstractNodeを解決するためには、ノード API への依存関係を追加する必要があります。My Editor プロジェクトを右クリックし、 「プロパティー」を選択して、「ライブラリ」カテゴリの「依存関係を追加」をクリックします。追加ダイアログで "AbstractNode" と入力し、リスト中で「ノード API」を選択して、「了解」をクリックするか Enter キーを押します 。 - ソースエディタに戻り、 Ctrl-Shift-I キーを押してインポートを修正します。依然
MyChildrenが見つからないとのエラーが出るでしょう。このクラスは今から作成しますから問題ありません。
ノードと子ノードの実装
上で AbstractNode というクラスを使っていますね。これは名前の通りの抽象クラスではありません!これはいくらかの時間と手間を省くための org.openide.nodes.Node のユーティリティー実装クラスです。1から Node を実装するのではなく、ただ AbstractNode を作成して子ノードを提供する Children オブジェクトを渡し、必要に応じてアイコンと表示名を設定するだけでよいのです。これが Node そのもののサブクラスを作成することなく、何かを表す Node オブジェクトを作成するための簡単な方法です。
これより、 MyChildren を実装して、先頭ノードの下に子ノードを作成します。
- My Editor プロジェクトの
org.myorg.myeditorパッケージを右クリックし、ポップアップメニューから「新規」>「Java クラス」を選択します。 - 「新規 Java クラス 」ウィザードで、「クラス名」を "MyChildren" とし、「完了」をクリックするか Enter キーを押します。
Children.Keysを拡張するようにクラスのシグネチャを変更します:class MyChildren extends Children.Keys {- Ctrl-Shift-I キーを押してインポートを修正します。
- シグネチャの行にカーソルを移動します。欄外に電球が表示されたら、Alt-Enter キーを押して、「すべての抽象メソッドの実装」を実行します。これで
createNodes (Object key)メソッドが追加されます。ここでルートノードの子ノードを作成します。 - まずは先に、
addNotifyをオーバーライドします。Swing コンポーネントのaddNotify()と同じで、Children.Keys.addNotify()は子ノードへの注意が最初に向けられた時、つまり子ノードについて最初に尋ねられた時に呼ばれます。ですから、ユーザーが親ノードを展開し表示が必要になる瞬間まで、子ノードの作成を遅らせることができます。ソースコード上にカーソルを置いて、 Alt-Insert キーを押します。そして「メソッドをオーバーライド...」を選択します。 出てきたダイアログで「Children」を展開し、addNotify()メソッドを選択して、「生成」をクリックするか Enter キーを押します。 addNotify()メソッドを以下のように実装します:protected void addNotify() { APIObject[] objs = new APIObject[5]; for (int i = 0; i < objs.length; i++) { objs[i] = new APIObject(); } setKeys (objs); }Children.Keysという名前から想像したかもしれませんが、親ノードはキーオブジェクトの配列またはCollectionを持ち、それらに対するNodeを生成するファクトリのように振る舞います。addNotify()は何かが子ノードを必要としていることを知らせているので、setKeys()を呼びます。setKeys()に渡す配列またはコレクションの各要素に対して、createNodes()を1度呼びます (あなたが望むなら1つのオブジェクトに対して複数のノードを割り当てることもできます) 。- 実際にノードを作成するためのコードを実装する必要があります。
createNodes()を以下のように実装します:protected Node[] createNodes(Object o) { APIObject obj = (APIObject) o; AbstractNode result = new AbstractNode (new MyChildren(), Lookups.singleton(obj)); result.setDisplayName (obj.toString()); return new Node[] { result }; } - Ctrl-Shift-I キーを押してインポートを修正します。
- 最後に、エクスプローラマネージャーを TopComponent の Lookup につなぐための配線コードを少し追加します。まずクラス定義の先頭から以下の行を削除します。
private final InstanceContent content = new InstanceContent();
そして、選択されたノードの Lookup を TopComponent の Lookup につなぐために、ユーティリティを使用します。 MyEditorのコンストラクタを以下のように修正します:public MyEditor() { initComponents(); associateLookup (ExplorerUtils.createLookup(mgr, getActionMap())); mgr.setRootContext(new AbstractNode(new MyChildren())); setDisplayName ("My Editor"); }
サンプルの実行
お気づきでしょうが、それぞれの AbstractNode に対し MyChildren のインスタンスを生成するので、無限に APIObjects が作られ、それぞれのノードは APIObject を持つ5つの子ノードを持つことになります。
準備が整ったので、 SelectionSuite を右クリックしてポップアップメニューから「生成物を削除してすべてを構築」を選択し、再度右クリックして「実行」を選択します。NetBeans が起動したら、ファイルメニューの「Open Editor」アクションで MyEditor インスタンスを開きます。
ノードをクリックまたは展開すると、ビューアとプロパティーシートの内容が、以下のように選択したノードの APIObject の情報に更新されることに注目してください:
エクスプローラの探検
以上のサンプルコードを使って、ノードと子ノードを表示するために NetBeans で使用可能な他のコンポーネントを探検したらおもしろいでしょう。探検するには、MyEditor をフォームエディタで開き、「カスタム作成コード」プロパティーのコードを他のコンポーネントを使うように変更します。あるコンポーネントに対しては、 JScrollPane を他のコンポーネントに変更する必要があるでしょう。(単に JScrollPane をフォームエディタで削除してしまって、コンストラクタに (new BeanTreeView(), BorderLayout.CENTER) と追加してしまってもよいでしょう。) 例えば以下のようなオプションがあります:- ListView—ノードを JList に表示する (階層の深さを設定することができる)
- TreeTableView—一番左の列がツリーになっているツリーテーブル
- ChoiceView—ノードとその子ノードのコンボボックスビュー
- MenuView—ノードとその子ノードのポップアップメニューを表示する
JButton - IconView—Windows エクスプローラのように子ノードをアイコン表示するコンポーネント
複数オブジェクトの選択
基本的なツリービューであるBeanTreeView では、同時に複数のノードを選択できることに気づいたかもしれません。ですから、選択中のすべてのノードの情報が表示できるようにビューアコンポーネントを修正したほうが望ましいでしょう:- My Viewer プロジェクトの
org.myorg.myviewer.MyViewerTopComponentをエディタで開きます。 - リスナーメソッドの
resultChanged()を次のコードに置き換えます:public void resultChanged(LookupEvent lookupEvent) { Lookup.Result r = (Lookup.Result) lookupEvent.getSource(); Collection c = r.allInstances(); if (!c.isEmpty()) { StringBuffer text1 = new StringBuffer(); StringBuffer text2 = new StringBuffer(); for (Iterator i = c.iterator(); i.hasNext();) { APIObject o = (APIObject) i.next(); text1.append (o.getIndex()); text2.append (o.getDate().toString()); if (i.hasNext()) { text1.append (','); text2.append (','); } } jLabel1.setText (text1.toString()); jLabel2.setText (text2.toString()); } else { jLabel1.setText("[no selection]"); jLabel2.setText (""); } }
これで、ExplorerUtils によって作成された Lookup が様々なノードの Lookup のプロキシとして使用できるだけでなく、複数のノードの Lookup をも正確にプロキシすることがわかるでしょう。
コンセプトのおさらい
ここで学んだコンセプトをいくつかおさらいします:Lookupはキーはクラスで値はクラスインスタンスであるMapのようなものでした。Lookupは オブジェクトが出入りする場所であり、特定の型のオブジェクトが出入りした時に通知してもらうように設定できると考えてもよいでしょう。Utilities.actionsGlobalContext()により、フォーカス中の多様なTopComponentのLookupのプロキシLookupを取得できます。このLookupは、フォーカスが別のコンポーネントに移動すると変更通知を発行します。- ノードは、ツリーやリストなどのエクスプローラ API のコンポーネントで表示することができるプレゼンテーションオブジェクトです。それぞれのノードは自身の
Lookupを持っています。 Utilities.actionsGlobalContext()で取得したLookupが TopComponent のLookupのプロキシとして使用できるように、ExplorerUtils.createLookup(ExplorerManager, ActionMap)によって作成されるLookupは エクスプローラ内で選択された様々なNodeのLookupのプロキシとして使用できます。
次の手順
これで、配下にモデルオブジェクト (APIObject) を持つノードを表示するビューができました。次のチュートリアルでは、これまでに作成したノードにアクションやプロパティー、そしてカラフルな表示名などを付け足していきます。
