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;
}
}


