Eclipseプラグインを作ってみよう!

第9回画面の作成(4)

前回はMaster(一覧)で選択したフィールドをDetails(詳細)で表示できるようにしました。今回は最初にパッケージとクラスの構造を整理します。そのあとで、各ボタンが動作するようにし、Details(詳細)で変更された内容が反映されるようにします。

パッケージ・クラスの構造の整理

今までは「とりあえず実装して動作させる」ことに重点を置いてきたので、FieldsPageクラスにすべて詰め込んだかなり見にくいソースコードになっています。そこで新しい機能を実装する前に、パッケージとクラスの構造を整理することにします。

パッケージの分割

まず、パッケージの構成を見直すことにします。現在は、com.piece_framework.piece_ide.form_designerにすべてのクラスが配置されています。これらのパッケージ構成を以下のように変更します(パッケージ名にはプリフィックスとしてcom.piece_framework.piece_ide.form_designerを付けます⁠⁠。

パッケージ名説明所属するファイル
pluginプラグイン全体で使用するクラスを配置するActivator.java
modelモデルクラスを配置するField.java
uiユーザーインタフェースに関係するクラスを配置するFormDesignerEditor.java, FieldsPage.java
変更後のパッケージ構成
変更後のパッケージ構成

Master(一覧)のクラス化

現在のフォームデザイナーのソースコードの中で一番複雑なのは、なんといってもFieldsPageクラスでしょう。そこで、まずMaster(一覧)の部分を別クラスに抽出することにします。

マニフェストエディターの「拡張」ページではMaster(一覧)をSectionPartクラスを継承したクラスで実装しているので、同じように継承したFieldsMasterSectionPartクラスを作成します。Compositeオブジェクトと ManagedFormオブジェクトを引数として受け取るコンストラクターを作成し、その中で初期化とウィジェットの生成・配置を行います。

FieldsMasterSectionPartクラスでウィジェットの生成・配置を行うcreateContents()メソッドはFieldsBlockクラスのcreateMasterPart()メソッドをほぼそのまま移動します。この移動に伴い、サンプルデータを生成するcreateSample()メソッドもFieldsMasterSectionPartクラスに移動します。

FieldsMasterSectionPartクラス
public class FieldsMasterSectionPart extends SectionPart {
    public FieldsMasterSectionPart(Composite parent, IManagedForm managedForm) {
        super(parent, 
              managedForm.getToolkit(), 
              Section.TITLE_BAR | Section.DESCRIPTION);
        initialize(managedForm);
        createContents(getSection(), managedForm.getToolkit());
    }

    private void createContents(Section section, FormToolkit toolkit) {
        section.setText("フィールド一覧");
        section.setDescription("編集するフィールドを選択してください。");

        Composite composite = toolkit.createComposite(section);
        composite.setLayout(new GridLayout(2, false));
       
        Table table = toolkit.createTable(
                        composite, 
                        SWT.SINGLE | SWT.FULL_SELECTION);
        table.setHeaderVisible(true);
        table.setLinesVisible(true);

        GridData layoutData = new GridData();
        layoutData.horizontalAlignment = GridData.FILL;
        layoutData.verticalAlignment = GridData.FILL;
        layoutData.grabExcessHorizontalSpace = true;
        layoutData.grabExcessVerticalSpace = true;
        table.setLayoutData(layoutData);
       
        TableColumn column1 = new TableColumn(table, SWT.NULL);
        column1.setText("フィールド名");
        column1.setWidth(100);

        TableColumn column2 = new TableColumn(table, SWT.NULL);
        column2.setText("説明");
        column2.setWidth(100);

        TableViewer viewer = new TableViewer(table);
        viewer.setContentProvider(new ArrayContentProvider());
        viewer.setLabelProvider(new ITableLabelProvider() {

            public Image getColumnImage(Object element, int columnIndex) {
                return null;
            }

            public String getColumnText(Object element, int columnIndex) {
                if (element == null) {
                    return "";
                }
                if (!(element instanceof Field)) {
                    return "";
                }
                
                Field field = (Field) element;
                if (columnIndex == 0) {
                    return field.getName();
                } else if (columnIndex == 1) {
                    return field.getDescription();
                }
                return "";
            }

            public void addListener(ILabelProviderListener listener) {
            }

            public void dispose() {
            }

            public boolean isLabelProperty(Object element, String property) {
                return false;
            }

            public void removeListener(ILabelProviderListener listener) {
            }
        });
        final SectionPart part = new SectionPart(section);
        viewer.addSelectionChangedListener(new ISelectionChangedListener() {
            public void selectionChanged(SelectionChangedEvent event) {
                FieldsMasterSectionPart.this.getManagedForm().fireSelectionChanged(
                                part,  
                                event.getSelection());
            }
        });

        Composite buttons = toolkit.createComposite(composite);
        layoutData = new GridData();
        layoutData.verticalAlignment = GridData.FILL;
        buttons.setLayoutData(layoutData);

        buttons.setLayout(new GridLayout());

        Button addButton = toolkit.createButton(buttons, "追加(&A)...", SWT.PUSH);
        layoutData = new GridData();
        layoutData.horizontalAlignment = GridData.FILL;
        addButton.setLayoutData(layoutData);
        
        Button delButton = toolkit.createButton(buttons, "削除(&D)...", SWT.PUSH);
        layoutData = new GridData();
        layoutData.horizontalAlignment = GridData.FILL;
        delButton.setLayoutData(layoutData);

        Button upButton = toolkit.createButton(buttons, "上へ", SWT.PUSH);
        layoutData = new GridData();
        layoutData.horizontalAlignment = GridData.FILL;
        upButton.setLayoutData(layoutData);

        Button downButton = toolkit.createButton(buttons, "下へ", SWT.PUSH);
        layoutData = new GridData();
        layoutData.horizontalAlignment = GridData.FILL;
        downButton.setLayoutData(layoutData);

        section.setClient(composite);
        
        viewer.setInput(createSample());
    }

    private List createSample() {
        List fields = new ArrayList();
        
        Field field1 = new Field("name");
        field1.setDescription("名前");
        field1.setRequired(true);
        field1.setMessage("名前は必須です。");
        
        fields.add(field1);

        Field field2 = new Field("phone");
        field2.setDescription("電話番号");
        field2.setRequired(true);
        field2.setMessage("電話番号は必須です。");
        
        fields.add(field2);
        
        Field field3 = new Field("address");
        field3.setDescription("住所");
        field3.setRequired(false);
        
        fields.add(field3);
        
        return fields;
    }
}
FieldsBlockクラス
private class FieldsBlock extends MasterDetailsBlock {
    @Override
    protected void createMasterPart(
                    final IManagedForm managedForm,
                    final Composite parent) {
        new FieldsMasterSectionPart(parent, managedForm);
    }

    @Override
    protected void createToolBarActions(IManagedForm managedForm) {
    }

    @Override
    protected void registerPages(DetailsPart detailsPart) {
        ...
    }
}

ここまで実装できたら、実行して動作に影響がないことを確認します。

Details(詳細)のクラス化

続いて、Details(詳細)を別クラスに抽出します。このクラスはFieldsBlockクラスのregisterPages()メソッドでIDetailsPageインタフェースの無名クラスとして実装している部分をFieldDetailsPageクラスとして抽出します。

FieldDetailsPageクラス
public class FieldDetailsPage implements IDetailsPage {
   ...
   (ソースコードはFieldsBlockクラスのregisterPages()メソッドのIDetailsPageインタフェースの無名クラスをそのまま移動してきます。)
   ...
}
FieldsBlockクラス
private class FieldsBlock extends MasterDetailsBlock {
    @Override
    protected void createMasterPart(
                    final IManagedForm managedForm,
                    final Composite parent) {
        new FieldsMasterSectionPart(parent, managedForm);
    }

    @Override
    protected void createToolBarActions(IManagedForm managedForm) {
    }

    @Override
    protected void registerPages(DetailsPart detailsPart) {
        detailsPart.registerPage(Field.class, new FieldDetailsPage());
    }
}

ここまで実装できたら、実行して動作に影響がないことを確認します。

ボタンの実装

ソースコードはだいぶ整理できましたので、続いて各ボタンの実装を行います。ボタンにイベントを追加するにはaddSelectionListener ()メソッドを使用し、SelectionListenerインタフェースのインスタンスを追加します。このインタフェースにはwidgetSelected()メソッドとwidgetDefaultSelected()メソッドがあり、前者はウィジェットを選択したときに呼び出されるのに対して、後者はリストなどでダブルクリックされたときに呼び出されます。これはワンクリック時とダブルクリック時で異なる動作をさせたいときに有効です。今回はボタンですので、widgetSelected()メソッドのみを実装することにします。

[追加]ボタン

[追加]ボタンがクリックされたときの動作は、フィールド名を入力するダイアログを表示します。次にそのダイアログで入力された文字列をフィールド名としてFieldオブジェクトを生成、テーブルにその情報をセットして選択状態にします。

[追加]ボタンのイベント
private void createContents(Section section, FormToolkit toolkit) {
    ...
    final TableViewer viewer = new TableViewer(table);
    ...
    final Composite buttons = toolkit.createComposite(composite);
    ...
    Button addButton = toolkit.createButton(buttons, "追加(&A)...", SWT.PUSH);
    layoutData = new GridData();
    layoutData.horizontalAlignment = GridData.FILL;
    addButton.setLayoutData(layoutData);
    addButton.addSelectionListener(new SelectionListener() {
        public void widgetSelected(SelectionEvent e) {
            InputDialog dialog = new InputDialog(
                                    buttons.getShell(), 
                                    "フィールド名入力", 
                                    "フィールド名を入力してください。", 
                                    null, 
                                    null);
            dialog.open();

            Field field = new Field(dialog.getValue());
            viewer.add(field);
            viewer.setSelection(new StructuredSelection(field));
        }

        public void widgetDefaultSelected(SelectionEvent e) {
        }
    });
    ...
}

widgetSelected()メソッドの中でviewerとbuttonsを使用するので、これらをfinalにします。入力ダイアログは既存のInputDialogクラスを使用します。特筆すべきは生成したFieldオブジェクトをTableViewerオブジェクトにadd()メソッドで追加するだけで、テーブルに追加されているという点です。テーブルビューアーを導入したことでウィジェットとモデルの連携が簡単になっていることがわかります。

それでは実行してみましょう。⁠追加]ボタンをクリックするとダイアログが表示され、入力した文字列がフィールド名となってテーブルに追加されます。

[追加]ボタンをクリックするとフィールド名の入力を促すダイアログが表示される
[追加]ボタンをクリックするとフィールド名の入力を促すダイアログが表示される

[削除]ボタン

削除は選択されているFieldオブジェクトを取得し、それをTableViewerオブジェクトから削除することで実現できます。

[削除]ボタンのイベント
private void createContents(Section section, FormToolkit toolkit) {
    ...
    Button delButton = toolkit.createButton(buttons, "削除(&D)...", SWT.PUSH);
    layoutData = new GridData();
    layoutData.horizontalAlignment = GridData.FILL;
    delButton.setLayoutData(layoutData);
    delButton.addSelectionListener(new SelectionListener() {
        public void widgetSelected(SelectionEvent e) {
            Field field = 
                (Field) ((IStructuredSelection) viewer.getSelection())
                .getFirstElement();
            viewer.remove(field);
        }

        public void widgetDefaultSelected(SelectionEvent e) {
        }
    });
}

それでは実行してみましょう。Master(一覧)のテーブルから削除したいフィールドを選択し、⁠削除]ボタンをクリックします。これで選択したフィールドが削除されます。

[上へ][下へ]ボタン

最後に[上へ]ボタン、⁠下へ]ボタンを実装します。フィールドの順序の入れ替えはTableViewerクラスのreplace()メソッドを使用して行います。最後に入れ替えたFieldオブジェクトを選択することで、連続して移動できるようにします。

[上へ]ボタン、⁠下へ]ボタンのイベント
private void createContents(Section section, FormToolkit toolkit) {
    ...
    Button upButton = toolkit.createButton(buttons, "上へ", SWT.PUSH);
    layoutData = new GridData();
    layoutData.horizontalAlignment = GridData.FILL;
    upButton.setLayoutData(layoutData);
    upButton.addSelectionListener(new SelectionListener() {
        public void widgetSelected(SelectionEvent e) {
            int index = viewer.getTable().getSelectionIndex();
            if (index == 0) {
                return;
            }

            Field tmpField = (Field) viewer.getElementAt(index - 1);
            viewer.replace(viewer.getElementAt(index), index - 1);
            viewer.replace(tmpField, index);

            viewer.setSelection(
                    new StructuredSelection((Field) viewer.getElementAt(index - 1)));
        }

        public void widgetDefaultSelected(SelectionEvent e) {
        }
    });
    
    Button downButton = toolkit.createButton(buttons, "下へ", SWT.PUSH);
    layoutData = new GridData();
    layoutData.horizontalAlignment = GridData.FILL;
    downButton.setLayoutData(layoutData);
    downButton.addSelectionListener(new SelectionListener() {
        public void widgetSelected(SelectionEvent e) {
            int index = viewer.getTable().getSelectionIndex();
            if (index + 1 == viewer.getTable().getItemCount()) {
                return;
            }

            Field tmpField = (Field) viewer.getElementAt(index + 1);
            viewer.replace(viewer.getElementAt(index), index + 1);
            viewer.replace(tmpField, index);

            viewer.setSelection(
                    new StructuredSelection((Field) viewer.getElementAt(index + 1)));
        }

        public void widgetDefaultSelected(SelectionEvent e) {
        }
    });
    ...
}

それでは実行してみましょう。Master(一覧)のテーブルから移動したいフィールドを選択し、⁠上へ]ボタン、⁠下へ]ボタンをクリックすると、フィールドが上下に移動します。

[上へ]ボタン、⁠下へ]ボタンに合わせてフィールドが上下に移動する
[上へ]ボタン、[下へ]ボタンに合わせてフィールドが上下に移動する

Details(詳細)の変更の反映

Master(一覧)からDetails(詳細)への変更通知はManagedFormオブジェクトを介して行いました。同じくDetails(詳細)からMaster(一覧)への変更通知もManagedFormオブジェクトを介して行うことにします。

ManagedFormクラスのfireSelectionChanged()メソッドの実装を見るとわかるのですが、addPart()メソッドで追加されたフォームパートのうちIPartSelectionListenerインタフェースを実装しているものを順番に呼び出しています。そこでMaster(一覧)でも変更通知イベントを受け取れるように、FieldsMasterSectionPartクラスでIPartSelectionListenerインタフェースを実装します。

IPartSelectionListenerインタフェースはselectionChanged()メソッドを定義しているので、これを実装する必要があります。このメソッドではTableViewerクラスのrefresh()メソッドを使って、テーブルのデータを更新します。これに伴い、 TableViewerオブジェクトをfViewerとしてFieldsMasterSectionPartクラスのプロパティーに変更します。また、変更通知元が自分自身の場合には処理を行わないようにします。

FieldsMasterSectionPartクラス
public class FieldsMasterSectionPart extends SectionPart implements IPartSelectionListener {
    private TableViewer fViewer;
    ...
    private void createContents(Section section, FormToolkit toolkit) {
        ...
        fViewer = new TableViewer(table);
        ...
        fViewer.addSelectionChangedListener(new ISelectionChangedListener() {
            public void selectionChanged(SelectionChangedEvent event) {
                FieldsMasterSectionPart.this.getManagedForm().fireSelectionChanged(
                                FieldsMasterSectionPart.this,  
                                event.getSelection());
            }
        });
        ...
        section.setClient(composite);

        getManagedForm().addPart(this);

        fViewer.setInput(createSample());
    }
    ...
    public void selectionChanged(IFormPart part, ISelection selection) {
        if (part == this) {
            return;
        }
        if (!(selection instanceof IStructuredSelection)) {
            return;
        }

        fViewer.refresh((Field) ((IStructuredSelection) selection).getFirstElement());
    }
}

次に変更を通知するDetails(詳細)の実装を行います。まず、どのタイミングで変更を通知するかですが、今回は各ウィジェットがフォーカスを失ったタイミングで通知することにします。またFieldsMasterSectionPartクラスの実装と同じく、通知元が自分自身の場合は処理を行わないようにします。

FieldDetailsPageクラス
public class FieldDetailsPage implements IDetailsPage {
    private Field fField;

    private SectionPart fSectionPart;
    ...
    public void createContents(Composite parent) {
        ...
        layoutData = new GridData();
        layoutData.grabExcessHorizontalSpace = true;
        layoutData.horizontalAlignment = GridData.FILL;
        composite.setLayoutData(layoutData);

        fSectionPart = new SectionPart(section);

        FocusListener focusListener = new FocusListener() {
            public void focusGained(FocusEvent e) {
            }

            public void focusLost(FocusEvent event) {
                if (fNameText == event.widget) {
                    fField.setName(fNameText.getText());
                } else if (fDescriptionText == event.widget) {
                    fField.setDescription(fDescriptionText.getText());
                } else if (fRequiredYesButton == event.widget
                           || fRequiredNoButton == event.widget) {
                    fField.setRequired(fRequiredYesButton.getSelection());
                } else if (fMessageText == event.widget) {
                    fField.setMessage(fMessageText.getText());
                }

                fForm.fireSelectionChanged(
                            fSectionPart, 
                            new StructuredSelection(fField));
            }
        };

        toolkit.createLabel(composite, "フィールド名");
        ...
        fNameText.addFocusListener(focusListener);

        toolkit.createLabel(composite, "概要");
        ...
        fDescriptionText.addFocusListener(focusListener);

        toolkit.createLabel(composite, "必須");
        ...
        fRequiredYesButton.addFocusListener(focusListener);
        fRequiredNoButton.addFocusListener(focusListener);
        
        toolkit.createLabel(composite, "メッセージ");
        ...
        fMessageText.addFocusListener(focusListener);

        section.setClient(composite);
    }
    ...
    public void selectionChanged(IFormPart part,
                                 ISelection selection) {
        if (fSectionPart == part) {
            return;
        }
        ...
        fField = field;
    }
}

それでは実行してみましょう。フィールド名や概要を変更して、フォーカスを移すと即座にテーブルに反映されるのがわかります。また別のフィールドを選択して、再度変更したフィールドを選択しても変更した内容が保持されています。これはTableViewerオブジェクトがデータをFieldオブジェクトとして管理しているためです。このようにテーブル、Master(一覧⁠⁠、Details(詳細)の間のデータのやりとりをFieldオブジェクトで行うことができるため、データとユーザインタフェースの間をやりとりするコードをほとんど実装する必要がありません。

Details(詳細)の変更がMaster(一覧)に反映されている
Details(詳細)の変更がMaster(一覧)に反映されている

おわりに

今回はテーブル、Master(一覧⁠⁠、Details(詳細)といったユーザーインタフェースとFieldオブジェクトの連携部分を実装しました。従来のUIプログラミングでは煩雑であったUI部品とオブジェクトとの連携が、TableViewerクラスやManagedFormクラスを介して行うことで、スムーズに行われていることをご理解いただけたと思います。

次回はバリデータ定義フォームの実装を説明します。

おすすめ記事

記事・ニュース一覧