はじめに
GEF(Graphical Editing Framework)は、その名の通り、モデルを「グラフィカル」に「編集」するアプリケーション(またはプラグイン)を作成するためのフレームワークです。この記事では、単純なGEFアプリケーションの作成を通して、GEFの概略と、その使用法を説明します。
対象読者
Eclipseプラグイン、およびDraw2Dを使用したアプリケーションを作成したことがある方。Draw2D、またはEclipseのプラグインについては、Eclipseのヘルプ、およびAPIドキュメントを参照してください。また、Draw2Dに関する詳細については、GEF SDKに付属するDraw2Dのプログラマーズ・ガイドを参照してください。
必要な環境
サンプルは、Eclipse SDK 3.0.1、およびGEF SDK 3.0.1を使用して作成しました。それ以前のバージョンでは動作しません。
GEFアプリケーションの概要
冒頭でも触れましたが、GEFはモデルをグラフィカルに編集するアプリケーションを作成するためのフレームワークです。GEFは、UMLダイアグラムのように、グラフィカルに表示・編集されることに適したモデルを持つアプリケーションを作成するために使用されます。
GEFアプリケーションは、MVC(Model-View-Controller)アーキテクチャーに基づいて作成された、「EditPartViewer
」と呼ばれるビューアーを中心に作成します。MVCアーキテクチャーは、GUIアプリケーションを「モデル」、「ビュー」、「コントローラ」の3つの要素に分割する考え方です。
また、EditPartViewer
にはビューとしてDraw2Dのフィギュアを使用する「グラフィカル・ビューアー」と、SWTのTreeItem
を使用する「ツリー・ビューアー」の2種類が用意されています。この内、ツリー・ビューアーは、主にGEFアプリケーションにEclipseの「アウトライン」ビューを提供するために使用されるものです。
MVCアーキテクチャーにおいて、モデルはアプリケーションで扱うデータです。また、ビューはモデルの情報をユーザーに表示するためのオブジェクト、つまり、モデルの外観です。コントローラは、モデルとビューを結びつけ、ユーザーからの入力により、モデルの変更を行う、といった役割を担います。
GEFは、モデルの形態に依存しないビューとコントローラ、およびアプリケーションの動作に必要な、その他の部品で構成されています。つまり、GEFアプリケーションを作成するには、自分でモデルを持ち込む必要があるということになります。
これに対して、ビューとコントローラに関しては、GEFに用意されている部品をモデルに合わせてカスタマイズして使用します。GEFアプリケーションにおけるモデル、ビュー、およびコントローラの概略は以下の通りです。
モデル
モデルは、GEFアプリケーションで表示・編集されるデータそのものです。GEFアプリケーションにおいて、保持され続けるデータは、モデルだけです。ビュー、またはコントローラは、編集動作によっては、破棄、再作成されるので、一時的なデータ以外の情報を持たせることはできません。同様の理由で、モデルは、ビュー、およびコントローラについての情報を参照しないようにする必要があります。
また、モデルは自身のプロパティが変更された場合に、それを通知する機能を含んでいる必要があります。前述の通り、GEFはモデルの形態に依存しないので、上記の条件を満たせば、モデルの定義は基本的に自由に行うことができますし、既存のモデルを持ち込む、といったこともできます。
ビュー
ビューは、モデルのプロパティをユーザーに表示する役割を担います。GEFでは、グラフィカル・ビューアー内ではDraw2Dのフィギュア(あるいは、フィギュアを複数組み合わせたもの)がビューとして使用され、ツリー・ビューアー内では、SWTのTreeItem
がビューとして使用されます。GEFアプリケーションでのビューは、モデルのプロパティを表示するためだけのものなので、モデルやコントローラについての情報は何も持たせないようにします。
コントローラ(EditPart)
GEFでは、コントローラのことを「EditPart
」と呼びます。EditPart
は、モデルの情報を基にビューを作成します。また、モデルの情報に変更があった場合は、それをビューに反映させる役割も担います。
MVCアーキテクチャーでのコントローラは、これ以外にもユーザーからの入力を処理して、モデルを変更する、といった役割も担いますが、GEFでは、モデルの編集に関する機能の多くは、EditPart
から分離されています。
上図のように、EditPart
は、モデルとビューのマッピングを行い、これらを制御しますが、ユーザーからのモデルの変更要求は、EditPart
が管理する全ての「EditPolicy
」に渡されます。EditPolicy
は、ユーザーからのモデルの変更要求を処理して、モデルの変更を行うための「コマンド」を作成します。実際にモデルに変更を加えるのは、コマンドの役目です。
つまり、モデルの編集機能は、EditPart
ではなく、EditPart
にインストールされる、EditPolicy
にあるということになります。
モデルの編集
GEFアプリケーションでのモデルの編集は、「ツール」、およびアクションによって行われます。ツールは、ユーザーが行ったマウス操作や、キー操作を「Request
」と呼ばれる、モデルの変更要求を表すオブジェクトに変換します。同様に、アクションも実行された時点でRequest
を作成します。
作成されたRequest
は、その時点で選択されている(アクティブな)EditPart
に送信されます。EditPart
は、送られてきたRequest
を、自身にインストールされている、全てのEditPolicy
(各EditPolicy
は、Role
と呼ばれる文字列をキーとしてインストールされます)に渡します。Request
を渡されたEditPolicy
は、そのRequest
が表す、モデルの変更要求に応じたコマンドを作成します。その後、コマンドが実行され、モデルの変更が行われることになります。GEFアプリケーションでモデルに変更を加えることができるのは、コマンドだけです。
モデルの変更は、上図のような流れで行われます。次に、モデルの編集に関する、GEFの要素について取り上げます。
Request
Request
は、ツールやアクションによって作成される、モデルの変更要求を表すオブジェクトです。Request
は、モデルに対して、どのような編集動作を行うかを示すタイプと、その編集動作に必要な情報を持っています。
GEFには、一般的な編集動作に関するRequest
タイプと、そのタイプのRequest
を処理するために必要な情報を設定することのできる、いくつかのRequest
クラスが用意されています。
例えば、「モデル作成ツール」を使用して、クリックを行った場合は、モデル作成ツールによって、クリックしたポイントに、モデルを新しく作成するためのRequest
が作成されます。この時、作成されるRequest
は、REQ_CREATE
タイプのRequest
です。また、このタイプのRequest
には、新しく作成されるモデルのインスタンスと、モデルの作成位置や、サイズといった情報が設定されています。REQ_CREATE
タイプのRequest
は、このような情報を取得するために、CreateRequest
クラスにキャストすることができます。
また、デフォルトのドラッグ・ツールを使用して、EditPart
をドラッグした場合は、ドラッグ・ツールによって、モデルの位置情報を変更するためのRequest
が作成されます。この時作成されるRequest
は、REQ_MOVE
、あるいは、REQ_MOVE_CHILDREN
タイプで、ChangeBoundsRequest
クラスにキャストすることで、移動先の位置情報を取得することができます。
このようなRequest
のタイプや、その操作を行うために必要な情報は、EditPolicy
がコマンドを作成するときに使用されます。
GEFには、この他にも、いくつかのRequest
タイプと、そのタイプのRequest
を処理するために必要な情報を設定することのできる、いくつかのRequest
クラスが用意されています。全てのRequest
タイプ、およびRequest
クラスについては、GEF SDKに付属する「GEF Developers Guide」の「Programmer's Guide」とAPIドキュメントを参照してください。
また、アプリケーションによっては、独自のRequest
タイプと、Request
クラスが必要になることもあります。Request
タイプは、単なる文字列なので、RequestConstantsインターフェイスに定義された、既存のタイプ文字列と重複しないようにすれば、独自のRequest
タイプを定義することができます。また、Request
クラスに関しては、org.eclipse.gef.Request
クラスのサブクラスを作成して、そのタイプのRequest
を処理するために必要な情報を保持できるようにします。
EditPolicy
EditPolicy
は、EditPart
が何を行うか、あるいは、EditPart
がモデルをどのように編集するか、といったEditPart
の動作を定義するためのプラガブル・オブジェクトです。EditPart
は、EditPolicy
をインストールすることで、モデルの編集機能を持つようになります。
具体的には、EditPolicy
は、ツール、またはアクションによって作成されたモデルの変更要求(Request
)を処理して、モデルを変更するためのコマンドを作成する役割と、必要に応じて、ユーザーの操作の内容を視覚的に表現するための「フィードバック」を表示する役割を担います。
EditPolicy
は、1つのEditPart
に対して、複数インストールすることができます。それぞれのEditPolicy
は、「Role
」と呼ばれる文字列によって識別されます。
前述の通り、EditPolicy
には、編集動作の種類によって、様々なタイプのRequest
が送信されてくることになります。全てのタイプのRequest
を1つのEditPolicy
で処理することも可能ですが、RoleをキーとしてEditPolicy
をインストールすれば、Request
の処理を、関連するRequest
タイプごとに、グループとして分割し、複数のEditPolicy
に分けて行うことができます。
また、EditPolicy
はRoleをキーとして、動的に入れ替えたり、アンインストールすることができます。したがって、モデルの状態によって、モデルの編集方針を変更する、といったようなこともできます。
例えば、モデルのプロパティによって、レイアウトの方法をXYLayout
からFlowLayout
に変更するような場合があったとします。その場合、XYLayout
であれば、子の移動は、モデルの位置情報の変更を意味しますが、FlowLayout
の場合は、子の移動は、モデルの順番の変更を意味するはずです。したがって、子の移動要求を表すRequest
(REQ_MOVE_CHILDREN
)を処理する方法も、実行すべきコマンドも違うものになるでしょう。
このような場合、レイアウトに関連するRequest
をまとめて処理するEditPolicy
をXYLayout
用のものと、FlowLayout
用のものを作成しておけば、レイアウトの変更が行われた際、これらのEditPolicy
を動的に入れ替えて、モデルの編集方針を変更することができます。また、レイアウトに影響を受けないモデルの変更要求を、別のRole
をキーとしてインストールされるEditPolicy
で処理するようにしておけば、上記のようなEditPolicy
の入れ替えが行われたとしても、その部分の編集機能には、全く影響がありません。
また、共通する編集動作が必要なEditPart
があれば、スーパークラスで共通する編集動作を定義したEditPolicy
をインストールして、サブクラスで、それぞれに固有の編集動作を定義したEditPolicy
をインストールする、といったことも可能になります。
このように、処理するRequest
タイプを、お互いに関連性のあるグループに分けて、グループごとに複数のEditPolicy
に分割して処理するようにしておけば、EditPart
に柔軟性を持たせることができます。
GEFには、このようなRequest
のグループ分けを行うための指針として、いくつかのRoleが定義されています。また、そのRoleで処理するべきRequest
のタイプと、それらのRequest
を処理するのに便利なEditPolicy
も用意されています。GEFが用意している全ての定義済みのRole、およびEditPolicy
クラスに関しては、GEF SDKのドキュメントの「Programmer's Guide」の「Using EditPolicies, Requests, and Roles」セクション以下の表に示されています。
GEFのドキュメント記述されているRole
および、そのRole
が処理すべきRequest
タイプは、どのようにRequest
タイプをグループ分けするのかを示した指針なので、完全にこれに従う必要はありません。また、Role
は、単なる文字列ですから、EditPolicy
インターフェイスに定義されている、定義済みのRole
文字列と重複しなければ、独自のRole
を定義することもできます。独自のEditPolicy
が必要であれば、AbstractEditPolicy
クラスなどを拡張することもできます。
コマンド
コマンドは、モデルに対して行う編集操作をカプセル化したもので、ユーザーからのモデルの変更要求を実際に実行するためのものです。GEFのCommand
クラスには、execute()
、undo()
、redo()
といったコマンドの実行、およびコマンドの取り消し、やり直しを行うためのメソッドが定義されています。したがって、コマンド・クラスは、これらのメソッドをオーバーライドして、モデルを変更したり、変更を取り消したりするだけの単純なものになります。
また、全てのコマンドは、「コマンド・スタック」によって管理されます。コマンド・スタックは、コマンドが実行された場合には、execute()
メソッドを呼び出してから、Undo用のスタックにコマンドをプッシュし、コマンドが取り消された場合は、Undo用のスタックからコマンドをポップして、undo()
メソッドを呼び出すといったような方法で、編集操作の取り消し・やり直しといった機能や、ドキュメントのダーティ・チェック機能をGEFアプリケーションに提供しています。
GEFアプリケーションの作成
ここからは、単純な図を編集するためのサンプル・アプリケーションを使用して、GEFアプリケーションの実装方法を段階ごとに説明します。GEFアプリケーションは基本的に、以下のような手順で実装します。
- モデルの定義
- ビューの定義
EditPart
の作成- モデルの編集機能の追加(
EditPolicy
の作成とインストール) EditPartViewer
の実装
モデルの定義
前述の通り、モデルの定義は基本的に自由です。この記事のサンプルGEFアプリケーションでは、下図のようなモデルが定義されています。
モデルの構造は単純で、ExampleDiagram
クラスが、NodeElement
を管理し、NodeElement
が、ConnectionElement
を管理するようになっています。ExampleDiagram
クラスは、全てのモデル・クラスに対して、直接的、あるいは間接的にアクセスすることができます。このような「トップレベル」のモデル・クラスは、EditPartViewer
に対する入力として扱われます。
モデルの変更通知機能の追加
前述の通り、モデルは、自身の変更をEditPart
に通知する機能を持つ必要があります。サンプルで使用しているモデル・クラスでは、PropertyChangeSupport
を使用してモデルの変更を通知する機能を実現しています。
abstract public class ModelElement implements IPropertySource, Serializable { ... // モデルの状態変化を通知するリスナのリスト transient private PropertyChangeSupport listeners = new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener l) { listeners.addPropertyChangeListener(l); } protected void firePropertyChange( String propName, Object oldValue, Object newValue) { listeners.firePropertyChange(propName, oldValue, newValue); } public void removePropertyChangeListener (PropertyChangeListener l) { listeners.removePropertyChangeListener(l); } ... }
プロパティ・ソース・サポートの追加
サンプルで使用しているモデル・クラスは、IPropertySource
インターフェイスを実装しています。これにより、GEFアプリケーションにプロパティ・ソース・サポートを追加しています。
Eclipseの[プロパティ]ビューは、ワークベンチ上の現在の選択に対して、プロパティ・ソース(IPropertySource
インターフェイスを実装するクラス)を要求します。GEFアプリケーションでは、EditPart
が現在の選択として、ワークベンチのセレクション・サービスに提供されることになるので、EditPart
がプロパティ・ソースを返す役割を担います。
デフォルトでは、EditPart
がプロパティ・ソースの要求を受けたとき、そのEditPart
に設定されているモデルが、IPropertySource
インターフェイスを実装していれば、モデルをプロパティ・ソースとして返します。また、モデルがIAdaptable
インターフェイスを実装しているクラスであれば、getAdapter
メソッドを通して、IPropertySource
型のアダプターを要求し、その結果を返します。
これ以外の方法で、プロパティ・ソース・サポートを追加するには、EditPart
クラスでgetAdapter
メソッドをオーバーライドして、IPropertySource
型のアダプターを返すようにします。
また、プロパティ・ソースの詳細については、このサンプルのソース、およびEclipse SDKの「プロパティ・シート・サンプル」等を参照してください。