Drag and Drop on JavaFX Part 1

前回やっと最終回を迎えた 『Transition in JavaFX』 シリーズでは、"script.aculo.us の 16 個の Effect を JavaFX で実現してみよう!!" をテーマにしてきましたが、その続きとして、今回から しばらくの間 "script.aculo.us の Drag and DropJavaFX で実現してみよう!!" がテーマです。


RIA の主な特徴の一つとして、『高度なユーザインターフェース』 がよくあげられます。『高度なユーザインターフェース』 の代表格と言えば、やはり Drag and Drop ではないでしょうか。現在では、script.aculo.us のような JavaScript ライブラリにより 通常の WEB ページでも簡単に Drag and Drop できてしまうので、もう 『高度なユーザインターフェース』 とは言えないかもしれませんが...

JavaScript で簡単にできることが、JavaFX でも同じように簡単に実装できなくては 全く意味がありません。もちろん、JavaFX でも標準 API で Drag and Drop がサポートされていますが、残念ながら、とても簡単に実装できるとは言いがたいです。と言うのも、標準 API では Drag and Drop したい Node に対し、毎回 onMouseDragged 等の関数を実装しなくてはならないからです。


と言うことで、今回は まず 任意の Node を簡単に Drag 可能にする Draggable を実装してみました。もちろん、この Draggablescript.aculo.us を参考に実装したものです。指定できるオプションも script.aculo.us とほぼ同じになっています。(いくつかのオプションはまだ実装していませんが...) 指定できるオプションとその効果については、次のサンプルと下記の Draggable.fx のソースを参照してみてください。

サンプル

Draggable の各オプションをそれぞれ1ずつ指定した Rectangle を5つ用意しました。ちなみに...

  1. 赤 は オプション指定なしです。自由にドラッグできます。
  2. 青 は 上部の色の濃い部分のみドラッグできます。
  3. 緑 は ドラッグ終了後 元の位置に戻ります。
  4. 黄 は 水平方向のみ移動できます。
  5. 桃 は 50 ずつ移動できます。

コーディング例

以下は 100×100 の四角形を Drag できるようにするだけの最も単純なコーディング例です。下記のコードを見てもらえればわかると思いますが、ただ単に Rectangle を Draggable でラップするだけで 簡単に Drag できるようになります。

import javafx.scene.Scene;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

Stage {
    width:  400
    height: 400
    scene: Scene {
        content: Draggable {
            node: Rectangle { width: 100 height: 100 }
        }
    }
}
Draggable.fx

以下は Draggable のソースです。今後、未実装のオプションも順次実装していく予定です。

import javafx.animation.transition.TranslateTransition;
import javafx.animation.transition.Transition;
import javafx.scene.CustomNode;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.util.Math;

public-read def VERTICAL   = "vertical";
public-read def HORIZONTAL = "horizontal";

public class Draggable extends CustomNode {

    /** Drag を可能にするノード */
    public-init var node:  Node;

    /** 
     * 実際に Drag 操作ができる部分に対応するノード
     * もしくは その スタイルクラス名 (デフォルト: node)
     */
    public-init var handle: Object on replace {
        handleNode =
            if (handle instanceof String) {
                lookup(node, handle as String);
            } else if (handle instanceof Node) {
                handle as Node;
            } else {
                node;
            }
    }

    /**
     * "horizontal" の場合 水平方向のみ移動、
     * "vertical" の場合 垂直方向 のみ移動
     */
    public-init var constraint: String;

    /**
     * Drag 後にもとの位置に戻すかどうかを表すフラグ。
     * true の場合 元の位置に戻す。(デフォルト: false)
     */
    public-init var revert = false on replace {
        if (revert) {
            revertTransition = TranslateTransition {
                node: this
                duration: 0.3s
                toX: bind basePointX
                toY: bind basePointY
            }
        }
    }

    /**
     * 水平方向に移動する際の増分値。
     * 0 より大きい値を指定した場合、この値ずつ水平方向に移動する。
     */
    public-init var snapX = 0.0;

    /**
     * 垂直方向に移動する際の増分値。
     * 0 より大きい値を指定した場合、この値ずつ垂直方向に移動する。
     */
    public-init var snapY = 0.0;

    var handleNode: Node;
    var draggable = false;

    var basePointX = 0.0;
    var basePointY = 0.0;

    var revertTransition: Transition;

    override function create(): Node {
        Group {
            content: node
            onMousePressed: function(e: MouseEvent) {

                draggable = handleNode.boundsInParent.contains(e.x, e.y);
                if (not draggable) { return; }

                basePointX = translateX;
                basePointY = translateY;

                opacity    = 0.5;
            }

            onMouseDragged: function(e: MouseEvent) {

                if (not draggable) { return; }

                if (constraint != VERTICAL) {
                    var dragX = if (snapX == 0.0) e.dragX else Math.round(e.dragX / snapX) * snapX;
                    translateX = basePointX + dragX;
                }

                if (constraint != HORIZONTAL) {
                    var dragY = if (snapY == 0.0) e.dragY else Math.round(e.dragY / snapY) * snapY;
                    translateY = basePointY + dragY;
                }
            }

            onMouseReleased: function(e: MouseEvent) {

                if (not draggable) { return; }
                draggable = false;

                if (revertTransition != null) {
                    revertTransition.playFromStart();
                }

                opacity    = 1.0;
            }
        }
    }

    function lookup(node: Node, styleClass: String): Node {
        if (node.styleClass == styleClass) {
            return node;
        }
        if (node instanceof Group) {
            for (child in (node as Group).content) {
                var _node = lookup(child, styleClass);
                if (_node != null) return _node;
            }
        }
        return null;
    }
}