8 PaperDoll 拖放应用程序
本章进一步使用 PaperDoll
应用程序来说明拖放功能。
在拖放操作中解释的基本原理在这里被应用于一个更高级的应用程序,该应用程序允许用户拖动衣服的图像并将其放置在纸娃娃的图像上,以及从纸娃娃的图像上拖动衣服的图像。
PaperDoll 应用程序的布局
PaperDoll
应用程序显示了四个代表衣服(服装部件)的图像和一个参与拖放操作的纸娃娃图像。应用程序窗口如图8-1所示。
应用程序的图形场景由两部分组成:
-
在窗口的上部显示一个
VBox
对象。它包含一个图像和“纸娃娃”文本,仅用于装饰。 -
在窗口的下部显示一个
GridPane
对象。-
第一列包含一个带有衣服图像的
FlowPane
对象。 -
第二列包含一个带有纸娃娃图像的
Pane
对象。
-
衣服的图像可以被拖放到纸娃娃的图像上,然后再拖回到它们的原始位置。 PaperDoll
应用程序提供了一个拖放操作的示例,其中同一个对象既可以是操作的源也可以是目标。
PaperDoll应用程序的组织结构
PaperDoll
应用程序包含以下包和类:
-
PaperDoll.java
是主要的应用程序类,它布局用户界面(UI)元素并实现应用程序逻辑。 -
paperdoll.body
包含定义接受数据拖放的身体容器的类。 -
paperdoll.clothes
包含定义可拖动的衣物的类。 -
paperdoll.images
包含应用程序的图形资源。
如示例8-1所示,PaperDoll
应用程序的用户界面被创建。
示例 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); } /** * 所有的布局都在这里进行。 * @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; /** * 在这里创建一个未装备物品的容器。 * @param bodyPane 需要身体容器,以便将物品从中移除 * @return */ private FlowPane createItemPane(final Pane bodyPane) { // 创建itemPane容器的代码 } private void populateClothes() { // 向itemPane容器添加物品的代码 } }
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(); // 存储节点ID以了解正在拖动的内容。 content.putString(currentImage.getId()); db.setContent(content); event.consume(); }); } }
请注意,此示例中使用了lambda表达式。 setOnDragDetected
方法通过调用startDragAndDrop(TransferMode.MOVE)
方法开始支持仅MOVE
传输模式的拖放手势。
处理数据的拖放
拖放手势的目标可以是itemPane
或bodyPane
对象,取决于拖放手势的起始位置。这意味着必须为itemPane
和bodyPane
对象实现setOnDragOver
和setOnDragDropped
方法。
如前所述,itemPane
对象在PaperDoll.java
类中创建。示例8-3补充了示例8-1中的代码,并提供了创建itemPane
容器的完整代码。
示例8-3
/** * 在此处创建一个未装备物品的容器。 * @param bodyPane 需要body容器,以便在itemPane上放下物品时从bodyPane中移除物品。 * @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(); // 在此处获取物品id,该id在拖动开始时存储。 boolean success = false; // 如果这是一个有意义的放下... if (db.hasString()) { String nodeId = db.getString(); // ...在body上搜索物品。如果存在... ImageView cloth = (ImageView) bodyPane.lookup("#" + nodeId); if (cloth != null) { // ...从body中移除物品 // 并添加到未装备容器中。 bodyPane.getChildren().remove(cloth); itemPane.getChildren().add(cloth); success = true; } // ...无论如何,该物品不再是活动的或已装备的。 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; } /** * 在此处将物品添加到未装备物品容器中。 */ private void populateClothes() { ClothListBuilder clothBuilder = new ClothListBuilder(); if (itemPane == null) throw new IllegalStateException("在填充之前应该调用getItems()!"); 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
对象本身且拖动板包含字符串时才能接受传输模式。
当鼠标按钮在itemPane
对象上释放时,会调用itemPane.setOnDragDropped
方法,该方法接受了之前接受的DRAG_OVER
事件。在这里,可拖动的衣物被添加到itemPane
容器中,并从bodyPane
对象中移除,通过在事件上调用setDropCompleted (Boolean)
方法来完成拖放手势。
类似地,bodyPane
容器的setOnDragOver
和setOnDragDropped
方法的实现如Example 8-4所示。
Example 8-4
bodyPane.setOnDragDropped((DragEvent event) -> { Dragboard db = event.getDragboard(); boolean success = false; // 如果这是一个有意义的放置... if (db.hasString()) { // 在这里获取一个项目ID,该ID在拖动开始时存储。 String nodeId = db.getString(); // ...在未装备的物品中搜索该物品。如果存在... ImageView cloth = (ImageView) itemPane.lookup("#" + nodeId); if (cloth != null) { // ...该物品从未装备列表中移除 // 并附加到身体上。 itemPane.getChildren().remove(cloth); bodyPane.getChildren().add(cloth); cloth.relocate(0, 0); success = true; } // ...无论如何,该物品现在已装备。 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(); });
Example 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; /** * 接受拖放的身体容器。拖放到这里的可拖动细节将被装备。 * */ 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 (db.hasString()) { // 在这里获取一个项目ID,该ID在拖动开始时存储。 String nodeId = db.getString(); // ...在未装备的项目中搜索该项目。如果存在... ImageView cloth = (ImageView) itemPane.lookup("#" + nodeId); if (cloth != null) { // ...该项目将从未装备的列表中移除 // 并附加到身体上。 itemPane.getChildren().remove(cloth); bodyPane.getChildren().add(cloth); cloth.relocate(0, 0); success = true; } // ...无论如何,该项目现在已经装备。 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; } }