Drag and Drop on JavaFX Part 1
前回やっと最終回を迎えた 『Transition in JavaFX』 シリーズでは、"script.aculo.us の 16 個の Effect を JavaFX で実現してみよう!!" をテーマにしてきましたが、その続きとして、今回から しばらくの間 "script.aculo.us の Drag and Drop を JavaFX で実現してみよう!!" がテーマです。
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 を実装してみました。もちろん、この Draggable は script.aculo.us を参考に実装したものです。指定できるオプションも script.aculo.us とほぼ同じになっています。(いくつかのオプションはまだ実装していませんが...) 指定できるオプションとその効果については、次のサンプルと下記の Draggable.fx のソースを参照してみてください。
サンプル
Draggable の各オプションをそれぞれ1ずつ指定した Rectangle を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; } }