SHOEISHA iD

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

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

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

特集記事

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

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


モデルの編集機能の追加

 モデルの編集機能は、EditPartEditPolicyをインストールすることで追加されます。サンプル・アプリケーションで行う編集操作は、ノードの作成・削除と、移動・サイズ変更、およびコネクションの作成・削除、再接続です。これらの編集操作に伴って発生するRequestのタイプと、各Requestを処理するためのEditPolicyは、以下の通りです。

Role EditPolicyクラス(スーパークラス) 処理するRequestのタイプ
LAYOUT_ROLE DiagramXYLayoutEditPolicy
(XYLayoutEditPolicy)
REQ_MOVE_CHILDRENREQ_RESIZE_CHILDRENREQ_CREATE
COMPONENT_ROLE ExampleComponentEditPolicy
(ComponentEditPolicy)
REQ_DELETE
CONNECTION_ROLE ExampleConnectionEditPolicy
ConnectionEditPolicy
REQ_DELETE
GRAPHICAL_NODE_ROLE ExampleNodeEditPolicy
GraphicalNodeEditPolicy
REQ_CONNECTION_STARTREQ_CONNECTION_ENDREQ_RECONNECT_SOURCEREQ_RECONNECT_TARGET

 これらのEditPolicyクラスは、EditPartcreateEditPolicyメソッド内でインストールされます。

「ExampleDiagramEditPart.java」の抜粋
public class ExampleDiagramEditPart extends BaseGraphicalEditPart {
...
    protected void createEditPolicies() {
        // EditPolicyのインストール
        installEditPolicy(EditPolicy.COMPONENT_ROLE,
            new RootComponentEditPolicy());
        installEditPolicy(EditPolicy.LAYOUT_ROLE,
            new DiagramXYLayoutEditPolicy());
     }
...
}

EditPolicyの実装とコマンド・クラスの作成

 EditPolicyクラスは、通常GEFに用意されているEditPolicyクラスを拡張して作成します。ユーザーの操作によって発生したRequestは、EditPolicygetCommandメソッドに渡され、そのRequestが表すモデルの変更要求にそったコマンドの作成が要求されますが、GEFに用意されているEditPolicyクラスは、getCommandメソッドで、送られてきたRequestのタイプを調べ、RequestがそのEditPolicyで処理すべきタイプの場合は、対応するメソッドを呼び出すようになっています。

 例えば、XYLayoutEditPolicyクラスは、REQ_MOVE_CHILDREN、およびREQ_RESIZE_CHILDRENタイプのRequestを受け取ると、createChangeConstraintCommandメソッドを呼び出して、モデルの制約を変更するコマンドの作成を要求します。また、REQ_CREATEタイプのRequestを受け取ると、getCreateCommandメソッドを呼び出します。

 したがって、これらのクラスを利用すれば、必要なメソッドをオーバーライドして、コマンドを返すようにするだけでEditPolicyの実装を行うことができます。次に、GEFに用意されたクラスを使用したEditPolicyの実装例をいくつか示します。

DiagramXYLayoutEditPolicy

 このEditPolicyは、ExampleDiagramEditPartにインストールされるEditPolicyです。ExampleDiagramEditPartは、FreeformLayoutXYLayoutのサブクラス)をレイアウト・マネージャとして設定したフィギュアをビューとして作成するので、このEditPartが受け持つ編集動作は、XYLayoutに関連するものになります。このEditPartで処理するRequestのタイプは、REQ_CREATEと、REQ_MOVE_CHILDREN、およびREQ_RESIZE_CHILDRENです。

「DiagramXYLayoutEditPolicy.java」
public class DiagramXYLayoutEditPolicy extends XYLayoutEditPolicy {

    protected Command
        createAddCommand(EditPart child, Object constraint) {
        return null; // REQ_ADD
    }

    /**
* ノードの位置・サイズ変更コマンドの作成 (REQ_MOVE_CHILDREN,REQ_RESIZE_CHILDREN) */
protected Command createChangeConstraintCommand(EditPart child, Object constraint) { ChangeConstraintCommand command = new ChangeConstraintCommand ((NodeElement)child.getModel()); command.setConstraint((Rectangle)constraint); return command; } /**
* ノード作成コマンドの作成(REQ_CREATE)
*/
protected Command getCreateCommand(CreateRequest request) { // 作成するノードの追加先を取得 ExampleDiagram parent = (ExampleDiagram) getHost().getModel(); // 新しく作成するノード・モデルを取得 NodeElement node = (NodeElement) request.getNewObject(); CreateNodeCommand command = new CreateNodeCommand(parent, node); // 新しく作成するノードの制約(位置とサイズ)を設定 command.setConstraint((Rectangle)getConstraintFor(request)); return command; } protected Command getDeleteDependantCommand(Request request) { return null; // REQ_DELETE_DEPENDANT } }

 ここでは、Requestの持つ情報を利用してコマンドを作成しています。コマンド・クラスは、モデルのプロパティを変更するだけの単純なものです。また、コマンドは、モデルの情報のみを持ち、EditPartやビューについては、参照しないようにします。

「ChangeConstraintCommand.java」
public class ChangeConstraintCommand extends Command {
    private NodeElement nodeElement;
    private Rectangle oldConstraint,newConstraint;
    
    public ChangeConstraintCommand(NodeElement node) {
        super("制約の変更");
        nodeElement = node;
        oldConstraint = node.getConstraint();
    }
    
    public void execute() {
        // 制約の変更
        nodeElement.setConstraint(newConstraint);
    }
    
    public void setConstraint(Rectangle rect) {
        newConstraint = rect;
    }
    
    public void undo() {
        // 制約を元にもどす
        nodeElement.setConstraint(oldConstraint);
    }
}
「CreateNodeCommand.java」
public class CreateNodeCommand extends Command {
    private ExampleDiagram diagram;
    private NodeElement node;
    
    private Rectangle constraint;

    public CreateNodeCommand
        (ExampleDiagram diagram, NodeElement node) {
        super("ノードの作成"); 
        this.diagram = diagram;
        this.node = node;
    }

    public void execute() {
        // 制約の設定
        node.setConstraint(constraint);
        // ノードの追加
        diagram.addChild(node);
    }

    public void setConstraint(Rectangle r) {
        constraint = r;
    }
    
    public void undo() {
        // ノードの削除
        diagram.removeChild(node);
    }
}

ExampleNodeEditPolicy

 このEditPolicyは、コネクションの作成と再接続の操作を処理します。この内、コネクションの作成は、他の編集操作とは少し異なります。これは、コネクションEditPartには、ソースとターゲットが必要となるためです。

 コネクションの作成コマンドは、2段階に分けて作成されます。まず最初の段階では、ソースとなるEditPartが決定されます。次に、ターゲットEditPartが決定され、コネクションの作成コマンドが作成されることになります。

「ExampleNodeEditPolicy.java」の抜粋
public class ExampleNodeEditPolicy
    extends GraphicalNodeEditPolicy {
    // フィードバック用フィギュア
    private Shape targetFeedback;

    /** 
* コネクション作成コマンドを作成する第2段階
*/
protected Command getConnectionCompleteCommand( CreateConnectionRequest request) { CreateConnectionCommand command = (CreateConnectionCommand)request.getStartCommand(); command.setTargetNode((NodeElement)getHost().getModel()); return command; } /**
* コネクション作成コマンドを作成する最初の段階
*/
protected Command getConnectionCreateCommand(CreateConnectionRequest request) { CreateConnectionCommand command = new CreateConnectionCommand ((ConnectionElement)request.getNewObject()); command.setSourceNode((NodeElement)getHost().getModel()); request.setStartCommand(command); return command; } ... protected void eraseTargetConnectionFeedback(DropRequest request) { // フィードバックを消去 if (targetFeedback == null) return; getFeedbackLayer().remove(targetFeedback); targetFeedback = null; } protected void showTargetConnectionFeedback(DropRequest request) { if (targetFeedback != null) return; // フィードバック用のフィギュアの作成 targetFeedback = new RectangleFigure(); targetFeedback.setBackgroundColor(ColorConstants.darkBlue); targetFeedback.setLineWidth(2); // 背景をXORで描画して対象のノードに重ねる targetFeedback.setFillXOR(true); targetFeedback.setBounds(getHostFigure().getBounds()); // フィードバック用フィギュアを追加するためのレイヤーに追加 getFeedbackLayer().add(targetFeedback); } }

 このEditPolicyでは、コネクションの作成、および再接続の過程をユーザーに視覚的に示すために、独自のフィードバックを表示します。フィードバックは、RootEditPartが提供するフィードバック・レイヤーに配置されます。このフィードバックは、コネクションの作成時、あるいは再接続時に、対象となるノードをハイライト表示するためのものです。

 フィードバックの表示は、getCommandメソッドではなく、showSourceFeedback、およびshowTargetFeedbackメソッドを通して、要求されます。上記のコードでオーバーライドしているshowTargetConnectionFeedbackメソッドは、スーパークラスのshowTargetFeedbackメソッドが、コネクションに関連するRequestタイプを受け取ったときに呼び出されます。また、フィードバックの消去についても同様です。

DeleteNodeCommand

 ノードは、コネクションが接続されている可能性があります。削除されるノードにコネクションが接続されていた場合は、コネクションも一緒に削除する必要があります。したがって、ノードの削除を行うコマンド・クラスは、これに注意して作成する必要があります。

「DeleteNodeCommand.java」
public class DeleteNodeCommand extends Command {
    
    private ExampleDiagram diagram;
    private NodeElement node; // 削除対象のノード
    
    // 削除されるノードをソースとするコネクション
    private List oldSourceConnections = new ArrayList();
    // 削除されるノードをターゲットするコネクション
    private List oldTargetConnections = new ArrayList();
    
    public DeleteNodeCommand
        (ExampleDiagram parent, NodeElement child) {
        super(ExamplePlugin.getResourceString
            ("DeleteNodeCommand.Label")); //$NON-NLS-1$
        diagram = parent;
        node = child;
    }
    
    public void execute() {
        // ノードに接続されているコネクションを削除する
        removeConnections();
        // ノードを削除
        diagram.removeChild(node);
    }
    
    private void removeConnections() {
        // 削除されるノードに接続されているコネクションを記録
        oldSourceConnections.addAll(node.getSourceConnections());
        oldTargetConnections.addAll(node.getTargetConnections());
        
        // 削除されるノードをソースとするコネクションを削除
        Iterator it = oldSourceConnections.iterator();
        while(it.hasNext()) {
            ((ConnectionElement)it.next()).disconnect();
        }
        
        // 削除されるノードをターゲットとするコネクションを削除
        it = oldTargetConnections.iterator();
        while(it.hasNext()) {
            ((ConnectionElement)it.next()).disconnect();
        }
    }
    
    private void restoreConnections() {
        // このノードをソースとするコネクションの復元
        Iterator it = oldSourceConnections.iterator();
        while(it.hasNext()) {
            ((ConnectionElement)it.next()).connect();
        }
        
        // このノードをターゲットとするコネクションの復元
        it = oldTargetConnections.iterator();
        while(it.hasNext())
            ((ConnectionElement)it.next()).connect();
        
        oldSourceConnections.clear();
        oldTargetConnections.clear();
    }
    
    public void undo() {
        // ノードの再追加
        diagram.addChild(node);
        // コネクションの復元
        restoreConnections();
    }
}

グラフィカル・エディターの作成

 モデル、ビュー、EditPartの準備が終わったら、EditPartViewerを用意します。EditPartViewerは、SWTコントロールを貼り付けることができる場所であれば、どこでも配置できますが、一般的には、Eclipseの「エディター・ペイン」に配置されます。

 GEFには、GEFアプリケーションをEclipseのエディタ・プラグインとして作成するために、GraphicalEditorクラス、およびそのサブクラスのGraphicalEditorWithPaletteクラス、GraphicalEditorWithFlyoutPaletteクラスの3つのクラスが用意されています。サンプル・アプリケーションでは、この内のGraphicalEditorWithFlyoutPaletteクラスを使用しています。

 GraphicalEditorWithFlyoutPaletteは、Eclipseのエディタ・ペインに、グラフィカル・ビューアーと、エディタ・ペインの左右に配置することができる、折りたたみ可能なパレットを提供します。

折りたたみ可能なパレット
折りたたみ可能なパレット

EditDomainとグラフィカル・ビューアーの設定

 エディタ・ペインに配置される、グラフィカル・ビューアーや、パレットを構成するパレット・ビューアーを含む、全てのEditPartViewerは、「EditDomain」によって管理されます。EditDomainは、EditPartViewerの他にも、コマンド・スタックや、パレット、現在アクティブなツールなどを管理します。

 EditDomainは、通常エディタ・クラスのコンストラクタで設定されます(下記コード(1))。これにより、エディタ・ペインに配置されるグラフィカル・ビューアーは、このEditDomainによって管理されることになります。他のEditPartViewerを追加する場合は、EditDomain#addViewerメソッドを使用して、そのビューアーを追加します。このようにしておくと、複数のEditPartViewer間で、同一のモデルを同時に編集することができるようになります。

 グラフィカル・ビューアーの設定は、configureGraphicalViewerメソッド内で行います。EditPartFactoryは、このメソッド内で設定する必要があります(下記コード(2))。また、トップレベル・モデルの設定は、initializeGraphicalViewerメソッド内で行います(下記コード(3))。

「ExampleEditor.java」の抜粋
public class ExampleEditor
    extends GraphicalEditorWithFlyoutPalette {

    private ExampleDiagram diagram = null; // トップレベルのモデル
...
    public ExampleEditor() {
        // エディット・ドメインの設定
        setEditDomain(new DefaultEditDomain(this)); // (1)
    }
...
    protected void configureGraphicalViewer() {
        super.configureGraphicalViewer();
        
        GraphicalViewer viewer = getGraphicalViewer();
        
        // コンテンツEditPartのビューにFreeformLayoutを使用する場合は
        // RootEditPartをFreeformGraphicalRootEditPartに
        // 変更する必要がある
        viewer.setRootEditPart(new FreeformGraphicalRootEditPart());
        
        // EditPartFactoryの設定
        viewer.setEditPartFactory
            (new GraphicalEditPartsFactory()); // (2)
...
    }
    
    protected void initializeGraphicalViewer() {
        super.initializeGraphicalViewer();
        
        GraphicalViewer viewer = getGraphicalViewer();
        
        // トップレベル・モデルの設定
        viewer.setContents(getExampleDiagram()); // (3)
    }
...
    protected void setInput(IEditorInput input) {
        super.setInput(input);
        IFile file = (IFile)input.getAdapter(IFile.class);
        
        if (file != null) {
            setPartName(file.getName());
            diagram = load(file); // モデルのロード
        }
        
        if(diagram == null)
            diagram = new ExampleDiagram();
    }
    
    private ExampleDiagram load(IFile file) {
        ExampleDiagram model = null;
        try {
            ObjectInputStream in
                = new ObjectInputStream(file.getContents());
            model = (ExampleDiagram) in.readObject();
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (CoreException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        
        return model;
    }
...
}

ドキュメントのダーティ・チェック

 前述の通り、モデルの編集は、全てコマンドを使用して行われます。したがって、コマンド・スタックを調べれば、ドキュメントが変更されたかどうかを知ることができます。

 コマンド・スタックの変更は、CommandStackListenerを使用して知ることができます。GraphicalEditorクラスのサブクラスでは、スーパークラスによって、このリスナーが登録されていて、コマンド・スタックに変更があると、commandStackChangedが呼び出されるようになっています。

「ExampleEditor.java」の抜粋
public class ExampleEditor
    extends GraphicalEditorWithFlyoutPalette {
...
    public void commandStackChanged(EventObject event) {
        super.commandStackChanged(event);
        // エディターのプロパティを更新
        firePropertyChange(IEditorPart.PROP_DIRTY);
    }
...
    public boolean isDirty() {
        // ダーティー・チェックにはコマンド・スタックを使用する
        return getCommandStack().isDirty();
    }
...
}

パレットの作成

 GraphicalEditorWithFlyoutPaletteクラスは、折りたたみ可能なパレットを提供します。パレットは、getPaletteRootメソッドで返されるPaletteRootに、GEFツールを格納することで作成されます。

 また、パレットはEditDomainが管理するので、createPaletteViewerProviderメソッド内で、EditDomainへの登録を行います。

「ExampleEditor.java」の抜粋
public class ExampleEditor
    extends GraphicalEditorWithFlyoutPalette {
...
    protected PaletteViewerProvider createPaletteViewerProvider() {
        return new PaletteViewerProvider(getEditDomain());
    }
    
    protected FlyoutPreferences getPalettePreferences() {
        // パレット設定の保存と復元
        return new FlyoutPreferences() {
...
        };
    }

    protected PaletteRoot getPaletteRoot() {
        return ExamplePaletteFactory.createPaletteRoot();
    }
...
}
「ExamplePaletteFactory.java」の抜粋
public class ExamplePaletteFactory {
    public static PaletteRoot createPaletteRoot() {
        PaletteRoot palette = new PaletteRoot();
        palette.add(createToolGroup(palette));
        palette.add(createConnectionCreationGroup());
        palette.add(createNodeCreationGroup());
        return palette;
    }
    
    private static PaletteEntry createToolGroup(PaletteRoot root) {
        PaletteGroup group = new PaletteGroup("tools"); //$NON-NLS-1$
        
        // 選択ツールをデフォルトでアクティブになるように設定し、
        // パレットに追加する
        ToolEntry entry = new SelectionToolEntry();
        root.setDefaultEntry(entry);
        group.add(entry);
        
        // 囲み枠ツールの追加
        group.add(new MarqueeToolEntry());
        
        return group;
    }
...
}

アクションの作成

 GEFには、操作の取り消し・やり直しや、モデルの削除といった一般的な編集操作に関するアクションや、ビューの幅や高さを揃えたり、ビューを整列させるといったグラフィカルな編集に必要なアクションが用意されています。

 また、GEFアプリケーションをエディタ・プラグインとして作成する場合には、GEFで定義されているコントリビューター・クラスを使用してアクションを登録することができます。

「ExampleActionContributor.java」
public class ExampleActionContributor
    extends ActionBarContributor {

    public ExampleActionContributor() {
    }
    
    protected void buildActions() {
        addRetargetAction(new DeleteRetargetAction());
        addRetargetAction(new UndoRetargetAction());
        addRetargetAction(new RedoRetargetAction());
        
        // 整列アクション
        addRetargetAction(new
            AlignmentRetargetAction(PositionConstants.LEFT));
        addRetargetAction(new
            AlignmentRetargetAction(PositionConstants.CENTER));
        addRetargetAction(new
            AlignmentRetargetAction(PositionConstants.RIGHT));
        addRetargetAction(new
            AlignmentRetargetAction(PositionConstants.TOP));
        addRetargetAction(new
            AlignmentRetargetAction(PositionConstants.MIDDLE));
        addRetargetAction(new
            (PositionConstants.BOTTOM));
        
        // 幅、高さを合わせるためのアクション
        addRetargetAction(new MatchWidthRetargetAction());
        addRetargetAction(new MatchHeightRetargetAction());

    }

    protected void declareGlobalActionKeys() {
        addGlobalActionKey(ActionFactory.SELECT_ALL.getId());
    }
    
    public void
        contributeToToolBar(IToolBarManager toolBarManager) {

        toolBarManager.add(getAction(ActionFactory.DELETE.getId()));
        toolBarManager.add(new Separator());
        toolBarManager.add(getAction(ActionFactory.UNDO.getId()));
        toolBarManager.add(getAction(ActionFactory.REDO.getId()));
        
        toolBarManager.add(new Separator());
        toolBarManager.add(getAction(GEFActionConstants.ALIGN_LEFT));
        toolBarManager.add(
            getAction(GEFActionConstants.ALIGN_CENTER));
        toolBarManager.add(
            getAction(GEFActionConstants.ALIGN_RIGHT));
        toolBarManager.add(new Separator());
        toolBarManager.add(getAction(GEFActionConstants.ALIGN_TOP));
        toolBarManager.add(
            getAction(GEFActionConstants.ALIGN_MIDDLE));
        toolBarManager.add(
            getAction(GEFActionConstants.ALIGN_BOTTOM));
        
        toolBarManager.add(new Separator());    
        toolBarManager.add(
            getAction(GEFActionConstants.MATCH_WIDTH));
        toolBarManager.add(
            getAction(GEFActionConstants.MATCH_HEIGHT));
    }
}
「ExampleEditor.java」の抜粋
public class ExampleEditor
    extends GraphicalEditorWithFlyoutPalette {
...
    protected void createActions() {
        super.createActions();
        
        // アクションを登録するレジストリ
        ActionRegistry registry = getActionRegistry();
        IAction action;
        
        // 整列アクションの作成
        action = new AlignmentAction
            ((IWorkbenchPart)this, PositionConstants.LEFT);
        // アクションの登録
        registry.registerAction(action);
        // 選択によってアクションの有効・無効が
        // 切り替わるように設定する
        getSelectionActions().add(action.getId());
...
    }
}

おわりに

 これまで見てきたとおり、GEFアプリケーションは、何らかの図を描画するものです。したがって、どのようなモデルを編集するにしても、モデルの編集方針は、共通する部分が多くなるはずです。

 GEFは、モデルの形態に依存しないように設計されているので、EditPolicyEditPartを実装するうえで、共通する処理があった場合でも、これらのクラスを他のGEFアプリケーションで再利用することは難しいでしょう。しかし、モデルの削除や、移動といった一般的な編集操作を行うためのコマンドや、EditPolicyクラスの実装方法は、異なるGEFアプリケーションでもほとんど同じものになります。また、EditPartの実装方法も、対応するビューや、モデルのタイプによって大体決まっています。

 つまり、どのようなモデルを扱うにしても、GEFアプリケーションの実装方法は、同じようなものになるということができます。

GEFがサポートするその他の機能

 最後に、今回は取り上げなかったGEFの主な機能を一部紹介します。これらの機能の使用法は、Eclipse CVS レポジトリーから入手できる、「org.eclipse.gef.examples.logic」サンプルを参照してください(Webページの[Project Root]メニューから[Tools_Project]を選択)。

  • ズーム機能:グラフィカル・ビューアーは、図を拡大・縮小表示する機能を備えることができます。
  • コンテンツ・アウトライン:GEFのツリー・ビューアーを使用して、グラフィカル・エディタで編集中のモデルの構造を「アウトライン」ビューに表示することができます。
  • 図のサムネイル表示:グラフィカル・ビューアーで表示している図を基に、サムネイルを表示することができます。
  • 図の印刷:グラフィカル・ビューアーで表示している図は、印刷することもできます。
  • ルーラーとグリッド:ビューアー上にグリッドやルーラーを表示することができます。また、ビューは、スナップも行えます。
  • ドラッグ・アンド・ドロップ:GEFアプリケーションには、ネイティブなDND機能を追加することができます。これにより、他のアプリケーションからテキストをドラッグして、ビューアーにドロップした場合に、そのテキストを使用してモデルを作成するといったようなことが可能になります。

参考文献

  1. Eclipse.org - GEF SDK に付属するドキュメント、およびAPIリファレンス
  2. Eclipse Corner Article "A Shape Diagram Editor"
  3. Eclipse Corner Article "Building a Database Schema Diagram Editor with GEF"
  4. IBM RedBook "Eclipse Development using the Graphical Editing Framework and the Eclipse Modeling Framework"
  5. eclipse wiki

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

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
特集記事連載記事一覧

もっと読む

この記事の著者

TAK_A(タカハシアキラ)

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

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

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

この記事をシェア

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

イベント

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

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

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

メールバックナンバー