JavaFX でポップアップメニュー

プレゼンツールで使おうと思って JavaFX でポップアップメニューを作ってみました。

通常、Swing のコンポーネントJavaFX から使うときは SwingComponent クラスのサブクラスを作ればいいのですが、JPopupMenu や JOptionPane のように呼び出すコンポーネントを指定しなくてはいけないものはちょっとやっかいです。

つまり、JPopupMenu を表示するには show メソッドをコールしますが、show メソッドの第 1 引数は JComponent なのです。しかし、JavaFX からは JComponent がなにか分かりません。

もちろん、javafx.ext.swing パッケージの SwingXXX クラスは getJComponent メソッドをコールすれば、基の Swing コンポーネントは分かります。でも、単なる Node オブジェクトではベースとなる JComponent オブジェクトがよく分からないわけです。

で、調べてみたわけです。

JavaFX の Node は Scenegraphプロジェクトで SGNode として扱われます。そして、その SGNode を扱うのが JSGPanel です。で、Node オブジェクトから JSGPanel オブジェクトをどうやって見つければいいかです。

いろいろやってみたら、見つけました。

    var node: Node = ...;
    var pane: JSGPanel = node.impl_getSGNode().getPanel();

ただし、そのままだとコンパイルが失敗するので、Scenario.jar をコンパイル時にクラスパスに加えます。

これで、ポップアップメニューはできたも同然です ^ ^;;

とはいうものの、SwingComponent クラスはくせがあるので、ちょっとてこずりました。SwingComponent クラスは対応するコンポーネントを生成するのに createJComponent メソッドをコールするのですが、createJComponent メソッドがコールされるのは、init や postinit より前なのでプロパティがセットされていないことがあるということです。

しかたないので、postinit で作成した Swing コンポーネントにプロパティをセットしています。

ポップアップメニューは PopupMenu クラス。同様に Menu クラス、MenuItem クラス、RadioButtonMenuItem クラスも作成しました。

使い方はこんな感じ

import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.Scene;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import net.javainthebox.menu.*;

var buttonGroup = ButtonGroup {}

var popupMenu = PopupMenu {
    label: "Sample"
    items: [
        MenuItem {
            label: "item1"
            onActionPerformed: function() {
                println("select item1")
            }
        },
        MenuItem {
            label: "item2"
            onActionPerformed: function() {
                println("select item2")
            }
        },
        Menu {
            label: "item3"
            items: [
                RadioButtonMenuItem {
                    label: "item3-1"
                    selected: true
                    group: buttonGroup
                    onActionPerformed: function() {
                        println("select item3-1")
                    }
                },
                RadioButtonMenuItem {
                    label: "item3-2"
                    group: buttonGroup
                    onActionPerformed: function() {
                        println("select item3-2")
                    }
                }
            ]
        }
    ]
}

var rect: Rectangle = Rectangle {
    x: 20, y: 20
    width: 120, height: 120
    fill: Color.SKYBLUE
    onMouseReleased: function(event: MouseEvent) {
        if (event.popupTrigger) {
            popupMenu.show(rect, event.x, event.y);
        }
    }
};

Stage {
    title: "PopupMenu Sample"
    scene: Scene {
        width: 200
        height: 200
        content: rect
    }
}

これで、青い Rectangle を右クリックするとポップアップメニューが表示されます。

f:id:skrb:20090517005210p:image

RadioButtonMenuItem は排他制御もできます。

f:id:skrb:20090517005211p:image

ここでitem3-2 を選んでから、もう一度ポップアップメニューを表示させると...

f:id:skrb:20090517005212p:image

となります。

ちょっと長いけど、ソース。

上述したようにコンパイルに Scenario.jar が必要です (実行時には Scenario.jar はクラスパスに含まれているので特に設定はいりません)。

PopupMenu.fx

package net.javainthebox.menu;

import javafx.ext.swing.SwingComponent;
import javafx.scene.Node;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;

public class PopupMenu extends SwingComponent {
    public var label: String;
    public-init var items: Menu[];
    
    var popupMenu: JPopupMenu;

    postinit {
        popupMenu.setLabel(label);
        for (item in items) {
            popupMenu.add(item.getJComponent());
        }
    }

    override function createJComponent(): JComponent {
        popupMenu = new JPopupMenu();
        return popupMenu;
    }

    public function show(node: Node, x: Integer, y: Integer): Void {
        var panel = node.impl_getSGNode().getPanel();
        popupMenu.show(panel, x, y);
    }
}

Menu.fx

package net.javainthebox.menu;

import javafx.ext.swing.SwingComponent;
import javax.swing.JComponent;
import javax.swing.JMenu;

public class Menu extends SwingComponent {
    public-init var label: String;
    public var items: SwingComponent[];

    var menu: JMenu;

    postinit {
        for (item in items) {
            menu.add(item.getJComponent());
        }
    }


    override function createJComponent(): JComponent {
        menu = new JMenu(label);
        return menu;
    }
}

MenuItem.fx

package net.javainthebox.menu;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JComponent;
import javax.swing.JMenuItem;

class ActionAdapter extends ActionListener {
    public var onActionPerformed: function();

    override function actionPerformed(event: ActionEvent): Void {
        onActionPerformed();
    }
};

public class MenuItem extends Menu {
    public-init var label: String;
    public var onActionPerformed: function();
    
    protected var menuItem: JMenuItem;

    postinit {
        var listener = ActionAdapter {
            onActionPerformed: onActionPerformed
        }
        menuItem.addActionListener(listener);
    }
    
    override function createJComponent(): JComponent {
        menuItem = new JMenuItem(label);
        return menuItem;
    }
}

RadioButtonMenuItem.fx

package net.javainthebox.menu;

import javax.swing.JComponent;
import javax.swing.JRadioButtonMenuItem;

public class RadioButtonMenuItem extends MenuItem {
    public var group: ButtonGroup;
    public var selected: Boolean on replace {
        menuItem.setSelected(selected);
    };

    postinit {
        if (group != null) {
            group.add(menuItem);
        }
    }

    override function createJComponent(): JComponent {
        menuItem = new JRadioButtonMenuItem(label);

        return menuItem;
    }
}

ButtonGroup.fx

package net.javainthebox.menu;

import javax.swing.AbstractButton;

public class ButtonGroup {
    var group: javax.swing.ButtonGroup;

    init {
        group = new javax.swing.ButtonGroup();
    }

    package function add(button: AbstractButton) {
        group.add(button);
    }
}