SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

GEFでグラフィック編集プラグインを作る

GEFによるアプリケーション作成のための基礎知識


ビューの準備

 グラフィカル・ビューアーを使用する場合、ここでの作業は、モデルのデータをどのように表示するかを決定し、必要があれば独自のフィギュア・クラスを作成するということになります。

 ビューとしてどのようなフィギュアを使用するかについては基本的に自由ですが、対応するモデルの形態によって、使用するフィギュアのタイプはある程度限定されます。

モデルのタイプ ビューとして使用するフィギュアのタイプ
ほかのモデルの親となるモデル この場合、ビューは、子モデルに対応するビューを格納することのできるコンテナーになります。したがって、このフィギュアは、レイアウト・マネージャー(あるいはそれと同様のもの)を使用して、子フィギュアを配置できるようにする必要があります。
子モデルを持たないモデル どのようなフィギュアでもかまいません。
モデル間の関係を表すモデル この場合、ビューとして使用できるのは、Draw2DのConnectionインターフェイスを実装した、コネクション・フィギュアです。通常は、PolylineConnectionクラスか、そのサブクラスが使用されます。

 また、複数のフィギュアで1つのビューを構成することもできます。サンプルでは、LabeledConnectionモデルを表現するビューとして、PolylineConnectionLabelフィギュアを組み合わせたものを使用しています。

 ビューアーとして、ツリー・ビューアーを使用する場合は、ビューはSWTのTreeItemを使用することが決まっているので、TreeItemのラベル文字列とアイコンを提供するだけです。

EditPartの作成

 EditPartは、GEFに用意されている3つのクラスのうちの、どれかを拡張することで作成します。どのクラスを選択するかは、対応するビューのタイプによって決まります。

ビューのタイプ 使用するEditPartクラス
コネクション・フィギュア AbstractConnectionEditPart
通常のフィギュア AbstractGraphicalEditPart
TreeItem AbstractTreeEditPart(この記事では使用しません)

 この内、AbstractGraphicalEditPartを使用する場合、そのEditPartに対してコネクションを張る必要があれば、さらに「NodeEditPart」インターフェイスを実装する必要があります。このような「ノードEditPart」は、コネクションを接続するために、コネクション・アンカーを提供する必要があります。

 サンプル・アプリケーションでは、「LabelNode」と「EllipseNode」に対応するEditPartがノードEditPartになります。また、「ExampleDiagram」に対応するEditPartは、コネクションを接続する必要がないので、AbstractGraphicalEditPartクラスのサブクラスとして作成します。「LabeledConnection」と「SolidConnection」モデルに対応するEditPartは、コネクション・フィギュアをビューとして使用するので、AbstractConnectionEditPartクラスを使用して作成します。

 前述の通り、EditPartは、ビューの作成と更新、および、モデルの状態変化の監視を行います。EditPartとしてAbstractGraphicalEditPartクラスを使用したとしても、AbstractConnectionEditPartクラスを使用したとしても、これらの機能を実装する方法は同じです。

ビューの作成

 ビューの作成は、EditPartクラスのcreateFigure()メソッド内で行います。AbstractConnectionEditPartクラスの場合は、デフォルトでPolylineConnectionフィギュアが作成されます。

「ExampleDiagramEditPart.java」の抜粋
public class ExampleDiagramEditPart extends BaseGraphicalEditPart {
...
    protected IFigure createFigure() {
        // ビューアー全体を占めるビューを作成
        IFigure figure = new FreeformLayer();
        figure.setLayoutManager(new FreeformLayout());
        return figure;
    }
...
}
「SolidConnectionEditPart .java」の抜粋
public class SolidConnectionEditPart
    extends BaseConnectionEditPart {
...
    protected IFigure createFigure() {
        PolylineConnection connection = new PolylineConnection();
        // コネクションに加えるデコレーション(デフォルトは三角型)
        PolygonDecoration decoration = new PolygonDecoration();
        
        // デコレーションの形を矢印型にする
        PointList pointList = new PointList();
        pointList.addPoint(0,0);
        pointList.addPoint(-2,2);
        pointList.addPoint(-1,0);
        pointList.addPoint(-2,-2);
        decoration.setTemplate(pointList);
        
        // デコレーションをコネクションのターゲット側に配置
        connection.setTargetDecoration(decoration);
        
        return connection;
    }
...
}

モデルの変更監視と、ビュー、EditPartの更新処理

 モデル・クラスでは、PropertyChangeSupportを使用して、モデルの変更を通知できるようになっているので、EditPartクラスでは、PropertyChangeListener インターフェイスを実装して、その通知を受け取ることができるようにします。したがって、ビューの更新は、propertyChangeメソッド内で行えばよいことになります。

 また、モデルの変更が行われる可能性があるのは、EditPartがアクティブな状態のときだけですから、EditPartactivate()メソッドで、状態監視を開始し、deactivate()メソッドで監視を終了するようにします。

「BaseGraphicalEditPart.java」
public abstract class BaseGraphicalEditPart
    extends AbstractGraphicalEditPart 
    implements PropertyChangeListener {

    public void activate() {
        super.activate();
        // モデルの状態変化の監視を開始
        getModelElement().addPropertyChangeListener(this);
    }
    
    public void deactivate() {
        // モデルの状態変化の監視の終了
        getModelElement().removePropertyChangeListener(this);
        super.deactivate();
    }
    
    private ModelElement getModelElement() {
        return (ModelElement)getModel();
    }
    
    public void propertyChange(PropertyChangeEvent event) {
        // このメソッドをオーバーライドして更新処理を行う
    }
}

 このような処理は、BaseConnectionEditPartクラスでも同様に行われています。

 モデルから変更通知を受けた場合(propertyChangeメソッドが呼び出された場合)は、以下のようなEditPartクラスに用意されている更新メソッドを呼び出して、ビューを更新したり、EditPartの構造を更新したりします。

更新メソッド 意味
refreshVisuals() ビューを更新します。デフォルトでは何もしません。
refreshChildren() EditPartの構造を更新します。子モデルが追加されたり、削除されたりした場合、モデルの構造をEditPartに反映させるために呼び出します。このメソッドを呼び出せば、削除されたモデルに対応しているEditPartは自動的に削除されますし、追加されたモデルに対応するEditPartは作成・追加されます。
refreshSourceConnections() このEditPartをソースとするコネクションEditPartの構造を更新します。コネクションが追加、あるいは削除された場合に呼び出します。
refreshTargetConnections() このEditPartをターゲットとするコネクションEditPartの構造を更新します。コネクションが追加、あるいは削除された場合に呼び出します。
refresh() 上記全てのメソッドを呼び出します。このメソッドは、EditPartが作成・初期化されるときに一度呼び出されます。独自の更新メソッドを用意する場合は、refresh()をオーバーライドして、その更新メソッドも呼び出すようにしておきます。

 上記の更新メソッドの中で、refreshVisuals()メソッドは、オーバーライドしてモデルのプロパティをビューに反映させる処理を記述する必要があります。

「BaseNodeEditPart.java」の抜粋
public abstract class BaseNodeEditPart
    extends BaseGraphicalEditPart 
    implements NodeEditPart {
...
    protected void refreshVisuals() {
        // モデルから制約を取得してビューの位置とサイズを更新する
        Rectangle constraint
            = (Rectangle) getNodeElement().getConstraint();
        ((AbstractGraphicalEditPart) getParent())
            .setLayoutConstraint(this, getFigure(), constraint);
    }
    
    public void propertyChange(PropertyChangeEvent event) {
        String propName = event.getPropertyName();
        if (NodeElement.ID_CONSTRAINT.equals(propName))
            refreshVisuals();
        else if (NodeElement.ID_SOURCE_CONNECTION.equals(propName))
            refreshSourceConnections();
        else if (NodeElement.ID_TARGET_CONNECTION.equals(propName))
            refreshTargetConnections();
    }
}

 上記のコードのpropertyChangeメソッド内で更新処理の振り分けに使用しているプロパティ名は、モデル・クラスのsetterメソッドで設定したものです。また、refreshVisuals()メソッド内で行っている、ビューの制約(位置とサイズ)を更新は、親EditPartから行うことに注意してください。

EditPartの構造

 ここまでの処理で、EditPartの主な役割である、ビューの作成・更新と、モデルの変更監視の機能を追加することができましたが、各EditPartは、モデルの構造を基した、階層構造をとります。このようなEditPartの構造を構築するために、子モデルを持つモデルを管理するEditPartは、getModelChildren()メソッドをオーバーライドして、子モデルのリストを返すようにしておく必要があります。親EditPartは、返された子モデルのリストを使用して、子EditPartを作成します。

 また、この階層構造は、トップレベル・モデル(このサンプルではExampleDiagram)を管理するEditPartを頂点とした構造になります。このトップレベル・モデルに対応するEditPartは、「コンテンツ」と呼ばれます。コンテンツEditPartは、ビューとしてグラフィカル・ビューアーの描画領域全体を占めるフィギュアを提供することになります。実際には、コンテンツEditPartの上位に「RootEditPart」と呼ばれるEditPartが存在しますが、このEditPartは対応するモデルを持たない特殊なEditPartで、コンテンツには含まれません(RootEditPartについては後述します)。

「ExampleDiagramEditPart.java」の抜粋
public class ExampleDiagramEditPart extends BaseGraphicalEditPart {
...
    protected IFigure createFigure() {
        // ビューアー全体を占めるビューを作成
        IFigure figure = new FreeformLayer();
        figure.setLayoutManager(new FreeformLayout());
        return figure;
    }
...    
    private ExampleDiagram getExampleDiagram() {
        return (ExampleDiagram)getModel();
    }
...
    protected List getModelChildren() {
        // ExampleDiagramモデルの子モデルを返す
        return getExampleDiagram().getChildren();
    }
...
}

 また、コネクションEditPartは、他のEditPartとは異なる方法で構築されます。これは、コネクションEditPartがソースとターゲットを必要とするためです(Draw2Dでも、コネクションを表示するためには、ソース・アンカーとターゲット・アンカーが必要です)。

 このため、コネクションEditPartを作成するノードEditPartクラスでは、getModelSourceConnections()メソッドと、getModelTargetConnections()メソッドという2つのメソッドを通して、コネクションEditPartの作成を行います。これらのメソッドは、先ほどのgetModelChildren()メソッドに相当するものです。

 また、ノードEditPartは、NodeEditPartインターフェイスで定義されたメソッドを実装して、コネクション・アンカーを提供する必要があります。これらのメソッドの内、引数としてRequestが渡されるものは、コネクションの作成、または再接続操作が行われている間に表示される、フィードバック用コネクションで使用されるアンカーを作成するためのものです。

「BaseNodeEditPart.java」の抜粋
public abstract class BaseNodeEditPart
    extends BaseGraphicalEditPart 
    implements NodeEditPart {
...
    private NodeElement getNodeElement() {
        return (NodeElement)getModel();
    }
    
    protected List getModelSourceConnections() {
        // このEditPartをソース(接続元)とする
        // コネクション・モデルを返す
        return getNodeElement().getSourceConnections();
    }
    
    protected List getModelTargetConnections() {
        // このEditPartをターゲット(接続先)とする
        // コネクション・モデルを返す
        return getNodeElement().getTargetConnections();
    }
    
    public ConnectionAnchor getSourceConnectionAnchor(
            ConnectionEditPart connection) {
        // コネクションをこのノードに接続するためのアンカーの作成
        return new ChopboxAnchor(getFigure());
    }
    
    public ConnectionAnchor
        getSourceConnectionAnchor(Request request) {
        // コネクションの作成中、または再接続中に表示される
        // フィードバック用コネクションで使用されるアンカーの作成
        return new ChopboxAnchor(getFigure());
    }
    
    public ConnectionAnchor getTargetConnectionAnchor(
            ConnectionEditPart connection) {
        // コネクションをこのノードに接続するためのアンカーの作成
        return new ChopboxAnchor(getFigure());
    }
    
    public ConnectionAnchor
        getTargetConnectionAnchor(Request request) {
        // コネクションの作成中、または再接続中に表示される
        // フィードバック用コネクションで使用されるアンカーの作成
        return new ChopboxAnchor(getFigure());
    }
...
}

RootEditPart

 前述の通り、RootEditPartは、モデルを持たないEditPartで、コンテンツEditPartの上位に位置します。RootEditPartは、ビューアーにダイアグラムを描画するための基礎となるレイヤーと、それらのレイヤーを格納するためのレイヤー・ペインを提供します。

 RootEditPartには、いくつかの種類があり、どのRootEditPartを使用するかによって、ビューアーの機能が異なってきます。RootEditPartは、GraphicalViewer#setRootEditPartメソッドを使用してデフォルトのものと置き換えることができます。

RootEditPartクラス ビューアーの機能
FreeformGraphicalRootEditPart ビューがマイナス位置に移動された場合、自動的にビューアーの描画領域を拡張します。このRootEditPartを使用する場合は、コンテンツEditPartのビューとして、FreeformLayoutが設定された、FreeformFigureを使用する必要があります。
ScalableFreeformRootEditPart ビューアーにズーム・サポートを追加します。また、ビューがマイナス位置に移動された場合、自動的にビューアーの描画領域を拡張します。また、コンテンツEditPartのビューは、FreeformGraphicalRootEditPartの場合と同じ条件を満たす必要があります。
ScalableRootEditPart グラフィカル・ビューアーを使用する場合のデフォルトRootEditPart。ビューアーにズーム・サポートを追加します。
RootTreeEditPart ツリー・ビューアーを使用する場合のデフォルトRootEditPart

 サンプル・アプリケーションでは、ズーム機能は使用しないので、FreeformGraphicalRootEditPartクラスをRootEditPartとして使用しています。

EditPartFactory

 全てのEditPartは、EditPartViewerに設定される「EditPartFactory」のcreateEditPartメソッドを通して作成されます。

「GraphicalEditPartsFactory.java」
public class GraphicalEditPartsFactory
    implements EditPartFactory {

    public EditPart createEditPart(EditPart context, Object model) {
        EditPart part = null;
        // モデルに対応するEditPartを作成する
        if (model instanceof ExampleDiagram)
            part = new ExampleDiagramEditPart();
        else if (model instanceof EllipseNode)
            part = new EllipseNodeEditPart();
        else if (model instanceof LabelNode)
            part = new LabelNodeEditPart();
        else if (model instanceof SolidConnection)
            part = new SolidConnectionEditPart();
        else if (model instanceof LabeledConnection)
            part = new LabeledConnectionEditPart();

        if (part != null)
            part.setModel(model); // EditPartにモデルを設定する

        return part;
    }
}

 createEditPartメソッドには、モデル・クラスのインスタンスが渡されるので、対応するEditPartを作成し、モデルを設定してから返します。このcreateEditPartメソッドの呼び出しは、以下のようにして行われます。

  1. トップレベルのモデルが、EditPartViewer#setInputメソッドによって設定されると、EditPartFactory#createEditPartメソッドが呼び出され、対応するEditPart(コンテンツEditPart)の作成が要求されます。
  2. 作成されたEditPartgetModelChildrenメソッドが呼び出され、子モデルが要求されます。
  3. 子モデルが返された場合は、各子モデルごとに、EditPartFactory#createEditPartメソッドが呼び出され、対応するEditPartの作成が要求されます。
  4. 作成されたEditPartgetModelChildrenメソッドが空のリストを返すまで、2の手順が繰り返されます。

 また、作成されたEditPartがノードEditPartの場合は、getModelChildrenメソッドの場合と同じように、getModelSourceConnectionsgetModelTargetConnectionsメソッドが呼び出され、コネクション・モデルが返された場合は、createEditPartメソッドによって、コネクションEditPartが作成されます。

 EditPartの階層構造は、上記のような流れで作られます。例えば、サンプル・アプリケーションで、ビューアーが下図のように表示されている場合は、EditPartの構造、およびEditPartの作成に使用されるメソッドとの関係は、以下のようになっています。

EditPartの構造およびEditPartの作成に使用されるメソッドとの関係
EditPartの構造およびEditPartの作成に使用されるメソッドとの関係

 上図のコネクションEditPartが、2つのノードEditPartによって管理されていることに注意してください。コネクションEditPartは、getModelSourceConnectionsメソッドと、getModelTargetConnectionsメソッドの両方で同じコネクション・モデルが返されなければ、作成されません。

次のページ
モデルの編集機能の追加

この記事は参考になりましたか?

特集記事連載記事一覧

もっと読む

この記事の著者

TAK_A(タカハシアキラ)

Eclipseに関するコアな情報を提供する「ObserveEclipse」の管理人。なぜか海外の開発者から英訳してほしいとの依頼が日本語のメールで舞い込むことがあるとのこと。

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/40 2008/09/01 18:43

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー