子コンポーネント作成時にアクセス

JEditorPaneにフォームのコンポーネントが作成されるときにアクセスし、リスナーを登録します。 「子コンポーネントを列挙してフォームにアクセス」の項目ではhtmlタグのname属性、id属性などとswingコンポーネントを結びつける手段がありませんでした。 ここでは標準ライブラリの子コンポーネントを作成する部分でアクセスして、htmlタグとコンポーネントとを結び付けられるようにします。

※サンプルを実行するにはhtmlファイルなどのテスト用リソースが必要です。 ネタのトップからダウンロードしてください。

子コンポーネントを作成しているクラスはFormViewです。 createComponentメソッドで作成しています。 FormViewのサブクラスを作り、createComponentメソッドをオーバーライドします。 スーパークラスにコンポーネントを作らせ、戻り値にリスナーを登録します。 コードのイメージは次のようになります。

public class CustomView extends FormView
{
    protected Component createComponent()
    {
        Component res = super.createComponent();
        // resにリスナーを登録
        return res;
    }
}

FormViewはHTMLDocumentのElementを持っているので、そのElementが持つAttributeSetを調べればhtmlタグに書かれた属性がわかります。 属性値をリスナーに登録するなどしてイベントを発行したコンポーネントが判別できるようにします。

FormViewを含め、htmlのビューは全てHTMLFactoryが作成します。 カスタマイズしたFormViewを使うには、HTMLFactoryのサブクラスを作り、目当てのhtmlタグからビューを作るときだけカスタムビューを返すようにします。 目当てのタグ以外はスーパークラスに作成させます。 inputタグのボタンタイプのコンポーネントに対してカスタムビューを作成したい場合、次のようになります。

// CustomViewFactory.java
import javax.swing.text.*;
import javax.swing.text.html.*;

public class CustomViewFactory extends HTMLEditorKit.HTMLFactory
{
    private CustomViewTest _owner;

    public CustomViewFactory(CustomViewTest owner)
    {
        _owner = owner;
    }

    public View create(Element elem)
    {
        View res = null;

        String elemName = elem.getName();
        if("input".equals(elemName) )
        {
            AttributeSet attrs = elem.getAttributes();
            String type = (String)attrs.getAttribute(HTML.Attribute.TYPE);

            // ボタンの場合のみカスタマイズしたFormViewを返す
            if( ("button".equals(type) ) ||
                ("submit".equals(type) ) ||
                ("reset".equals(type) ) ||
                ("image".equals(type) ) ||
                ("checkbox".equals(type) ) ||
                ("radio".equals(type) ) )
            {
                res = new CustomButtonView(_owner, elem);
            }
        }

        if(res == null)
            res = super.create(elem);
        return res;
    }
}

ボタン用のカスタムビュークラスは次のようになります。 サンプルではついでにjdk1.6で作成されないbuttonタイプのコンポーネントを作成しています。 CustomButtonViewのgetMaximumSpanメソッドはオーバーライドしたものです。 FormViewが作ったコンポーネントを使うだけならオーバーライドの必要は無いのですが、独自のコンポーネントを作ったときは適切な値を返すようにしなければなりません。 (詳細は調べていません)

// CustomButtonView.java
import java.awt.Component;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.text.html.*;

public class CustomButtonView extends FormView
{
    private CustomViewTest _owner;

    public CustomButtonView(CustomViewTest owner, Element elem)
    {
        super(elem);
        _owner = owner;
    }

    public float getMaximumSpan(int axis)
    {
        // ボタンはgetPreferredSpanの値を返すらしい
        return getPreferredSpan(axis);
    }

    protected Component createComponent()
    {
        // super.createComponentの戻り値を利用する
        AbstractButton res = (AbstractButton)super.createComponent();
        AttributeSet attrs = getElement().getAttributes();

        // jdk6ではbuttonフォームが作成されないので自前で作る
        String type = (String)attrs.getAttribute(HTML.Attribute.TYPE);
        if("button".equals(type) )
            res = createButton(attrs);

        // super.createComponentの戻り値にリスナーを登録
        String buttonName = (String)attrs.getAttribute(HTML.Attribute.NAME);
        res.addActionListener(new ActionListenerBridge(_owner, buttonName) );

        return res;
    }

    private AbstractButton createButton(AttributeSet attrs)
    {
        String value = (String)attrs.getAttribute(HTML.Attribute.VALUE);
        if(value == null)
            value = "";

        JButton res = new JButton(value);
        res.setAlignmentY(1.0f); // jdkのソースも即値

        return res;
    }
}

ActionListenerだとactionPerformedメソッドでname属性などを特定できないので、橋渡しのActionListenerBridgeを作ります。 ボタン系のswingコンポーネントはアクションコマンドを設定すればラッピングの必要はないのですが、他のコンポーネントでも応用できるようにこの方法にしました。

// ActionListenerBridge.java
import java.awt.event.*;

public class ActionListenerBridge implements ActionListener
{
    private CustomActionListener _listener;
    private String _name;

    public ActionListenerBridge(CustomActionListener listener, String name)
    {
        _listener = listener;
        _name = name;
    }

    public void actionPerformed(ActionEvent event)
    {
        _listener.actionPerformed(_name, event);
    }
}

name属性などを元に場合分けして処理するコードは次のリスナーを実装したクラスでやります。

// CustomActionListener.java
import java.awt.event.ActionEvent;

public interface CustomActionListener
{
    public void actionPerformed(String name, ActionEvent event);
}

htmlファイルを読み込むときにカスタマムビューファクトリーが使用される様にするためには、EditorKitのgetViewFactoryメソッドがカスタマムビューファクトリーを返さなくてはなりません。 しかし、EditorKitにはgetViewFactoryがあってもsetViewFactoryはありません。 EditorKitもサブクラス化してgetViewFactoryをオーバーライドする必要があります。

次のコードがサブクラス化したHTMLEdiotrKitです。 サンプルコードではsubmitボタンで移動させないのでsetAutoFormSubmissionをfalseに設定しています。

// CustomEditorKit.java
import javax.swing.text.ViewFactory;
import javax.swing.text.html.HTMLEditorKit;

public class CustomEditorKit extends HTMLEditorKit
{
    private CustomViewTest _owner;
    private CustomViewFactory _viewFactory;

    public CustomEditorKit(CustomViewTest owner)
    {
        _owner = owner;
        _viewFactory = new CustomViewFactory(_owner);
        setAutoFormSubmission(false);
    }

    public ViewFactory getViewFactory()
    {
        return _viewFactory;
    }
}

JEditorPaneにカスタムエディターキットを登録して使用します。 ユーザがボタンを操作したときactionPerformedメソッドが呼ばれます。 サンプルではhtmlタグのname属性を表示しています。

// CustomViewTest.java
import java.io.File;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.text.html.*;

public class CustomViewTest extends JFrame implements CustomActionListener
{
    public static final String TEST_HTML = "jeditorpane_test.html";
    public static final String TEST_CSS = "jeditorpane_test.css";

    private JEditorPane _editorPane;

    public CustomViewTest()
    {
        setDefaultCloseOperation(EXIT_ON_CLOSE);

        try
        {
            CustomEditorKit editorKit = new CustomEditorKit(this);
            File cssFile = new File(TEST_CSS);
            editorKit.getStyleSheet().importStyleSheet(cssFile.toURI().toURL() );

            _editorPane = new JEditorPane();
            _editorPane.setEditable(false);
            _editorPane.setContentType("text/html");
            _editorPane.setEditorKit(editorKit);
            _editorPane.setPage(new File(TEST_HTML).toURI().toURL() );

            JScrollPane scrollPane = new JScrollPane(_editorPane);
            getContentPane().add(scrollPane);
        }
        catch(Exception exc)
        {
            exc.printStackTrace();
        }
    }

    public static void main(String[] args)
    {
        CustomViewTest app = new CustomViewTest();
        app.setSize(640, 480);
        app.setVisible(true);
    }

    public void actionPerformed(String name, ActionEvent event)
    {
        JOptionPane.showMessageDialog(this, name);
    }
}

子コンポーネントの概要

FormViewのcreateComponentメソッドが返すコンポーネントは次のようになっています。

タグ/タイプクラスリスナー
text JTextField ActionListener※
password JPasswordField ActionListener※
checkbox JCheckBox なし
radio JRadioButton なし
file Boxの子にJTextField, Box.Filler, JButton ActionListener(JButtonのみ)
button - -
image JButton MouseListener
select(list) JScrollPaneの子にJList なし
select(combobox) JComboBox なし
textarea JScrollPaneの子にJTextArea なし
reset JButton ActionListener※
submit JButton ActionListener※

textareaなど、コンテナクラスに配置される物もいくつかあります。 コンテナから目的のコンポーネントを取り出して操作してください。

FormViewがコンポーネントになんらかのリスナーを登録する場合、表のリスナー欄に記述があります。 ユーザがフォームのコンポーネントを使ったときのデフォルト処理をするリスナーを表しています。 resetボタンが押されたときフォームの内容をクリアするリスナーや、fileタイプのボタンを押したときファイル選択ダイアログを表示するリスナーなどです。

これらのデフォルトの処理が邪魔な場合、リスナーを削除して無効にできます。 ※印の付いたものはFormView自身がリスナーとして登録されています。 例えば、submitボタンのリスナーを消すなら次のようにします。 FormSubmitEventは発行されなくなります。

public class CustomSubmitView extends FormView
{
    protected Component createComponent()
    {
        JButton res = (JButton)super.createComponent();
        res.removeActionListener(this);
        // resに自前のリスナーを登録
        return res;
    }
}

fileタイプのボタンとimageタイプには別のリスナーが登録されています。 例えば、imageボタンのリスナーを消すなら次のようにします。

public class CustomImageButtonView extends FormView
{
    protected Component createComponent()
    {
        JButton res = (JButton)super.createComponent();
        MouseListener[] removeLisneners = res.getMouseListeners();
        for(MouseListener removeLisnener : removeLisneners)
            res.removeMouseListener(removeLisnener);
        // resに自前のリスナーを登録
        return res;
    }
}