DOC-07-08 PaperDoll拖放程序
本章用纸娃娃(PaperDoll)程序进一步说明拖放特性。
在这个更加高级的程序中使用了第7章中介绍的基本原则,该程序让用户拖放表示衣服的图片并把它们放到一个表示纸娃娃的图片上。
PaperDoll程序的布局
PaperDoll程序显示了4张表示衣服的图片和一个纸娃娃来参与拖放操作。程序窗口显示如图8-1所示:
图8-1 纸娃娃程序
该程序的图形化场景由两部分组成:
● 窗口上部是一个VBox对象,它包含一个图像和“Paper Doll”文本,只是用来装饰用的。
● 窗口下部是一个GridPane对象。
○ 第一列是一个FlowPane对象,里面是衣服图像。
○ 第二列是一个Pane对象,里面是纸娃娃的图像。
衣服的图像可以拖放到纸娃娃的图像上面,也可以拖回原位置。纸娃娃程序提供了一个拖放操作的示例,在该程序中,同一个对象既可以是拖放源也可以是拖放目标。
纸娃娃程序的结构
纸娃娃程序包含如下几个包和类:
● PaperDoll.java是主类,布局UI元素并实现了程序逻辑。
● body包含定义了“身体”容器的类,“身体”可以接收拖放数据。
● clothes包含定义了可拖放的衣服的类。
● images包含程序的图形资源文件。
注意:本章中并未提供构建纸娃娃程序的一步步的过程。你可以下载PaperDoll.zip来查看完整的NetBeans工程。
纸娃娃程序的UI创建过程如例8-1所示。
例8-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
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(); }); } } |
注意在该例子中lambda表达式的用法。通过调用startDragAndDrop(TranferMode.MOVE)方法,setOnDragDetected方法中启动了仅支持MOVE传输模式的拖放手势。
处理数据的放下(drop)
拖放手势的目标可以是itemPane也可以是bodyPane对象,取决于拖放手势在哪儿开始。因此两个对象均需要实现setOnDragOver和setOnDragDropped方法。
前面已经提到,itemPane对象是在PaperDoll.java中创建的。例8-3补充了例8-1中的代码,并提供了创建itemPane容器的完整代码。
例8-3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
/** * 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.setOnDragOver方法只有当拖放手势源不是itemPane对象自己并且拖放板包含一个字符串时才接受该传输模式(译者注:传输模式是MOVE)。
当在接收了前面的DRAG_OVER事件的itemPane对象上松开鼠标按键时会调用itemPane.setOnDragDropped方法。在这里是可拖放的衣服添加到itemPane中并从bodyPane中移除。通过调用事件的setDropCompleted(Boolean)方法来使拖放手势完成。
相似地,bodyPane容器的setOnDragOver和setOnDragDropped方法实现如例8-4所示。
例8-4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
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; } } |
应用程序文件
源码
NetBeans工程

