8 PaperDollドラッグ・アンド・ドロップ・アプリケーション
この章では、PaperDoll
アプリケーションを使用してドラッグ・アンド・ドロップ機能についてさらに詳しく解説します。
ここでは、「ドラッグ・アンド・ドロップ操作」で説明した基本原理をより高度なアプリケーションで使用しており、ユーザーは、衣装のイメージをドラッグして紙人形のイメージ上にドロップしたり、紙人形のイメージから衣装のイメージをドラッグできます。
PaperDollアプリケーションのレイアウト
PaperDoll
アプリケーションには、ドラッグ・アンド・ドロップ操作の対象となる衣装を表す4つのイメージと1つの紙人形が表示されます。図8-1に、アプリケーション・ウィンドウを示します。
アプリケーションのグラフィック領域は次の2つの部分で構成されます。
-
ウィンドウの上の部分には、
VBox
オブジェクトが表示されます。ここには、単に装飾として1つのイメージとPaper Dollというテキストが含まれています。 -
ウィンドウの下の部分には、
GridPane
オブジェクトが表示されます。-
1列目には、衣装のイメージを表す
FlowPane
オブジェクトが含まれています。 -
2列目には、紙人形のイメージを表す
Pane
オブジェクトが含まれています。
-
衣装のイメージは、紙人形のイメージ上にドラッグ・アンド・ドロップしたり、元の場所に戻すことができます。PaperDoll
アプリケーションは、同じオブジェクトがドラッグ・アンド・ドロップ操作のソースおよびターゲットの両方になることが可能であることを例示しています。
PaperDollアプリケーションの構成
PaperDoll
アプリケーションには、次のパッケージおよびクラスが含まれています。
-
PaperDoll.java
は、ユーザー・インタフェース(UI)要素のレイアウトを決定し、アプリケーション・ロジックを実装するメイン・アプリケーション・クラスです。 -
paperdoll.body
には、データをドロップ可能な本体のコンテナを定義するクラスが含まれています。 -
paperdoll.clothes
には、ドラッグ可能な衣装を定義するクラスが含まれています。 -
paperdoll.images
には、アプリケーションのグラフィック・リソースが含まれています。
注意:
この章では、PaperDoll
アプリケーションをビルドするための段階的な手順は説明しません。
完全なNetBeansプロジェクトを参照するには、PaperDoll.zip
をダウンロードしてください。
PaperDoll
アプリケーションのUIは、例8-1に示すように作成されます。
例8-1
package paperdoll; import paperdoll.clothes.Cloth; import paperdoll.clothes.ClothListBuilder; import paperdoll.body.Body; import paperdoll.images.ImageManager; import java.util.HashMap; import java.util.List; import javafx.application.Application; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.image.ImageView; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.ColumnConstraints; import javafx.scene.layout.FlowPane; import javafx.scene.layout.GridPane; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.stage.Stage; public class PaperDoll extends Application { public static void main(String[] args) { launch(args); } /** * All laying out goes here. * @param primaryStage */ @Override public void start(Stage primaryStage) { primaryStage.setTitle("Paper Doll"); ImageView header = new ImageView(ImageManager.getImage("ui/flowers.jpg")); VBox title = new VBox(); title.getChildren().addAll(header); title.setPadding(new Insets(10.0)); GridPane content = new GridPane(); content.add(Body.getBody().getNode(), 1, 1); content.add(createItemPane(Body.getBody().getBodyPane()), 0, 1); ColumnConstraints c1 = new ColumnConstraints(); c1.setHgrow(Priority.ALWAYS); ColumnConstraints c2 = new ColumnConstraints(); c2.setHgrow(Priority.NEVER); c2.setPrefWidth(Body.getBody().getBodyPane().getMinWidth() + 20); content.getColumnConstraints().addAll(c1, c2); items = new HashMap<>(); Body.getBody().setItemsInfo(itemPane, items); populateClothes(); VBox root = new VBox(); root.getChildren().addAll(title, content); primaryStage.setScene(new Scene(root, 800, 900)); primaryStage.setMinWidth(800); primaryStage.setMinHeight(900); primaryStage.show(); } private FlowPane itemPane = null; private HashMap<String, Cloth> items; /** * A container for unequipped items is created here. * @param bodyPane body container is needed so that the item is removed from * it when dropped here. * @return */ private FlowPane createItemPane(final Pane bodyPane) { // code for creating the itemPane container } private void populateClothes() { //code for adding items to the itemPane container } }
itemPane
オブジェクトは各種衣装を表し、bodyPane
オブジェクトは衣装の着せ替えが可能な人形本体を表します。
ドラッグ・アンド・ドロップ操作の開始
ドラッグ・アンド・ドロップ操作のソースは、Cloth
項目を表すImageView
オブジェクトのいずれかです。各currentImage
は常にitemPane
内またはbodyPane
内のノードになります。setOnDragDetected
メソッドの実装は、例8-2の太字箇所です。
例8-2
public class Cloth { private final Image previewImage; private final Image activeImage; private final Image equippedImage; private final ImageView currentImage; public void putOn() { currentImage.setImage(equippedImage); } public void takeOff() { currentImage.setImage(previewImage); } private void activate() { currentImage.setImage(activeImage); } public String getImageViewId() { return currentImage.getId(); } public Node getNode() { return currentImage; } public Cloth(Image[] images) { this.previewImage = images[0]; this.activeImage = images[1]; this.equippedImage = images[2]; currentImage = new ImageView(); currentImage.setImage(previewImage); currentImage.setId(this.getClass().getSimpleName() + System.currentTimeMillis()); currentImage.setOnDragDetected((MouseEvent event) -> { activate(); Dragboard db = currentImage.startDragAndDrop(TransferMode.MOVE); ClipboardContent content = new ClipboardContent(); // Store node ID in order to know what is dragged. content.putString(currentImage.getId()); db.setContent(content); event.consume(); }); } }
この例ではラムダ式が使用されていることに注目してください。setOnDragDetected
メソッドによりstartDragAndDrop(TransferMode.MOVE)
メソッドがコールされ、MOVE
転送モードのみをサポートするドラッグ・アンド・ドロップ・ジェスチャが開始されます。
データのドロップの処理
ドラッグ・アンド・ドロップ・ジェスチャのターゲットは、そのジェスチャが開始された場所に応じてitemPane
オブジェクトまたはbodyPane
オブジェクトのいずれかになります。つまり、setOnDragOver
およびsetOnDragDropped
メソッドは、itemPane
とbodyPane
の両方のオブジェクトに対して実装する必要があります。
前述のとおり、itemPane
オブジェクトはPaperDoll.java
クラスで作成されます。例8-3は、例8-1のコードを補完するものであり、itemPane
コンテナを作成するための完全なコードを示しています。
例8-3
/** * A container for unequipped items is created here. * @param bodyPane body container is needed so that an item is removed from * the bodyPane when dropped on the itemPane. * @return */ private FlowPane createItemPane(final Pane bodyPane) { if (!(itemPane == null)) return itemPane; itemPane = new FlowPane(); itemPane.setPadding(new Insets(10.0)); itemPane.setOnDragDropped((DragEvent event) -> { Dragboard db = event.getDragboard(); // Get item id here, which was stored when the drag started. boolean success = false; // If this is a meaningful drop... if (db.hasString()) { String nodeId = db.getString(); // ...search for the item on body. If it is there... ImageView cloth = (ImageView) bodyPane.lookup("#" + nodeId); if (cloth != null) { // ... the item is removed from body // and added to an unequipped container. bodyPane.getChildren().remove(cloth); itemPane.getChildren().add(cloth); success = true; } // ...anyway, the item is not active or equipped anymore. items.get(nodeId).takeOff(); } event.setDropCompleted(success); event.consume(); }); itemPane.setOnDragOver((DragEvent event) -> { if (event.getGestureSource() != itemPane && event.getDragboard().hasString()) { event.acceptTransferModes(TransferMode.MOVE); } event.consume(); }); return itemPane; } /** * Here items are added to an unequipped items container. */ private void populateClothes() { ClothListBuilder clothBuilder = new ClothListBuilder(); if (itemPane == null) throw new IllegalStateException("Should call getItems() before populating!"); List<Cloth> clothes = clothBuilder.getClothList(); clothes.stream().map((c) -> { itemPane.getChildren().add(c.getNode()); return c; }).forEach((c) -> { items.put(c.getImageViewId(), c); }); }
itemPane.setOnDrageOver
メソッドで転送モードが受け入れられる条件は、ドラッグ・アンド・ドロップ・ジェスチャのソースがitemPane
オブジェクト自体ではなく、ドラッグボードに文字列が含まれていることです。
前のDRAG_OVER
イベントが受け入れられたitemPane
オブジェクト上でマウス・ボタンを放すと、itemPane.setOnDragDropped
メソッドがコールされます。ここで、ドラッグ可能な衣装がitemPane
コンテナに追加され、bodyPane
オブジェクトから削除されます。また、ドラッグ・アンド・ドロップ・ジェスチャを完了するために、イベントに対してsetDropCompleted (Boolean)
メソッドがコールされます。
同様に、例8-4に、bodyPane
コンテナに対するsetOnDragOver
およびsetOnDragDropped
メソッドの実装を示します。
例8-4
bodyPane.setOnDragDropped((DragEvent event) -> { Dragboard db = event.getDragboard(); boolean success = false; // If this is a meaningful drop... if (db.hasString()) { // Get an item ID here, which was stored when the drag started. String nodeId = db.getString(); // ...search for the item in unequipped items. If it is there... ImageView cloth = (ImageView) itemPane.lookup("#" + nodeId); if (cloth != null) { // ... the item is removed from the unequipped list // and attached to body. itemPane.getChildren().remove(cloth); bodyPane.getChildren().add(cloth); cloth.relocate(0, 0); success = true; } // ...anyway, the item is now equipped. items.get(nodeId).putOn(); } event.setDropCompleted(success); event.consume(); }); bodyPane.setOnDragOver((DragEvent event) -> { if (event.getGestureSource() != bodyImage && event.getDragboard().hasString()) { event.acceptTransferModes(TransferMode.MOVE); } event.consume(); });
例8-5に、BodyElement
クラスの完全なコードを示します。
例8-5
package paperdoll.body; import paperdoll.clothes.Cloth; import paperdoll.images.ImageManager; import java.util.Map; import javafx.geometry.Insets; import javafx.scene.Node; import javafx.scene.image.ImageView; import javafx.scene.input.DragEvent; import javafx.scene.input.Dragboard; import javafx.scene.input.TransferMode; import javafx.scene.layout.Pane; /** * Container for body that accepts drops. Draggable details dropped here * are equipped. * */ public class BodyElement { private final Pane bodyPane; private final ImageView bodyImage; private Pane itemPane; private Map<String, Cloth> items; public void setItemsInfo(Pane p, Map<String, Cloth> m) { itemPane = p; items = m; } public Pane getBodyPane() { return bodyPane; } public BodyElement() { bodyPane = new Pane(); bodyImage = new ImageView(ImageManager.getResource("body.png")); bodyPane.setOnDragDropped((DragEvent event) -> { Dragboard db = event.getDragboard(); boolean success = false; // If this is a meaningful drop... if (db.hasString()) { // Get an item ID here, which was stored when the drag started. String nodeId = db.getString(); // ... search for the item in unequipped items. If it is there... ImageView cloth = (ImageView) itemPane.lookup("#" + nodeId); if (cloth != null) { // ... the item is removed from the unequipped list // and attached to body. itemPane.getChildren().remove(cloth); bodyPane.getChildren().add(cloth); cloth.relocate(0, 0); success = true; } // ...anyway, the item is now equipped. items.get(nodeId).putOn(); } event.setDropCompleted(success); event.consume(); }); bodyPane.setOnDragOver((DragEvent event) -> { if (event.getGestureSource() != bodyImage && event.getDragboard().hasString()) { event.acceptTransferModes(TransferMode.MOVE); } event.consume(); }); bodyPane.getChildren().add(bodyImage); bodyPane.setMinWidth(bodyImage.getImage().getWidth()); bodyPane.setPadding(new Insets(10.0)); } public Node getNode() { return bodyPane; } }