コンテキストメニューを実装してみる

今日のJavaFXは右クリックなどで表示されるコンテキストメニューの実装です。

デフォルトで用意されていてもいいとは思いますが、各種プラットフォームで利用可能かどうかという問題もあります。したがって、ディスクトップ専用になりがちなリッチなUIはサードパーティ製ライブラリや自作になることが多いのではないでしょうか?とはいえ、簡単に作れてしまうのがJavaFXの最大の特徴であります。このようなカスタムGUIを実装するのに150行程度で済むとはほんとうに素晴らしい。
動作サンプルソースコードはそれぞれリンク先にて。

構成

コンテキストメニューはCustomNodeとして実装していますが、各メニュー項目についてはカスタムのControl(Button)として実装してます。カスタムButtonについては以前にも紹介したように、Controlとしての本体となるMenuButton、具体的な表示を行うMenuButtonSkinの組み合わせです。
意外とはまるのがLayout系に配置した時に上手くノードのサイズが計算されずにはみ出したりする事です。このような場合は、奨励サイズを取得する関数をオーバライドするとうまくいきます。

    override function getPrefWidth(width:Number): Number {
        node.layoutBounds.width;
    }
    override function getPrefHeight(height:Number): Number {
        node.layoutBounds.height;
    }

表示と非表示の時のエフェクト

単に表示するだけでは味気ないですので、少しだけアニメーションをさせてフェードイン/フェードアウトさせています。これだけ凄くリッチなUIに思えるのですから面白いです。

    function showMenu(x:Number, y:Number) {
        if (showing) return;
        context.layoutX = x + 30;
        context.layoutY = y + 15;
        Timeline {
            keyFrames: [
                at(0.0s) { context.opacity => 0.0 }
                at(0.2s) { context.opacity => 1.0 }
            ]
        }.play();
        showing = true;
        insert context into node.scene.content;
    }
    function hideMenu() {
        if (not showing) return;
        Timeline {
            keyFrames: KeyFrame {
                time: 0.2s
                values: context.opacity => 0.0
                action: function() {
                    showing = false;
                    delete context from node.scene.content;
                }
            }
        }.play();
    }

シーングラフからノード(context)を取り除くタイミングには注意します。hideMenuで取り除いてからアニメーションをかけても先に消えてしまいますので、アニメーションの最後のタイミングで消すようにしましょう。

アドオン可能なコンテキストメニュー

先日のエントリーと同様にコンテキストメニューは任意のノードに対して後から機能を追加できるようにしました。追加を行っているコードはこんな感じです。

    var msg = "このエリア内で右クリックすればコンテキストメニュー表示";
    var node:Group = Group {
        content: [
            Rectangle {
                layoutX: 20, layoutY: 20
                width: 500, height: 400
                fill: Color.TRANSPARENT
                stroke: Color.GRAY
            }
            Text {
                layoutX: 50, layoutY: 50
                content: bind msg
            }
        ]
    }
    ContextMenu.mount {
        node: node
        context: ContextMenu {
            layoutX: 20, layoutY:20
            items: [
                ContextMenuItem {
                    text: "更新"
                    action: function() {
                        msg = "更新しました。";
                    }
                }// ContextMenuItem
                ContextMenuItem.SEPARATOR,
                ContextMenuItem {
                    text: "コピー"
                    action: function() {
                        msg = "コピーしました。";
                    }
                }// ContextMenuItem
                ContextMenuItem {
                    text: "貼り付け"
                    action: function() {
                        msg = "貼り付けしました。";
                    }
                }// ContextMenuItem
            ]
        }// ContextMenu
    };
    return node;

node自体は普通のノードですが、ContextMenu.mount(クラス)を使用してコンテキストメニューを追加しています。内容はContextMenuItem にラベルと処理を記述して列挙するだけと直感的で解りやすいインターフェイスを実現しています。

一見、かなり面倒そうなコンポーネントですが、JavaFXの仕組みを理解してくると簡単に実現できるところに注目しましょう。もはや、Swingなどで実装したくはないですw