8 PaperDoll Drag-and-Drop Application
This chapter further illustrates the drag-and-drop feature using the PaperDoll
application.
The basic principles explained in the Drag-and-Drop Operation are used here in a more advanced application that enables the user to drag the images of dresses and drop them on the image of a paper doll, and drag the images of dresses from the image of a paper doll.
Layout of the PaperDoll Application
The PaperDoll
application displays four images that represent dresses (pieces of clothing) and a paper doll that participate in a drag-and-drop operation. The application window is shown in Figure 8-1.
The graphical scene of the application consists of two parts:
-
A
VBox
object is displayed in the upper part of the window. It contains an image and the Paper Doll text, and is used for decoration only. -
A
GridPane
object is displayed in the bottom part of the window.-
The first column contains a
FlowPane
object with the images of clothing. -
The second column contains a
Pane
object with an image of a paper doll.
-
The images of the clothing can be dragged and dropped on an image of a paper doll and back to their original locations. The PaperDoll
application provides an example of a drag-and-drop operation in which the same object can be both the source and the target of the operation.
Organization of the PaperDoll Application
The PaperDoll
application contains the following packages and classes:
-
PaperDoll.java
is the main application class, which lays out the user interface (UI) elements and implements the application logic. -
paperdoll.body
contains classes that define a container for the body that accepts drops of the data. -
paperdoll.clothes
contains classes that define a draggable piece of clothing. -
paperdoll.images
contains the graphical resources for the application.
Note:
This chapter does not provide a step-by-step procedure to build the PaperDoll
application.
You can download the PaperDoll.zip
to see the completed NetBeans project.
The UI of the PaperDoll
application is created as shown in Example 8-1.
Example 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 } }
The itemPane
object represents separate pieces of clothing and the bodyPane
object represents the body of the doll with pieces of clothing that can be put on.
Starting the Drag-And-Drop Operation
The source for the drag-and-drop operation is one of the ImageView
objects that represent a Cloth
item. At any moment, each currentImage
is either a node in the itemPane
or in the bodyPane
. The setOnDragDetected
method is implemented as shown in bold in Example 8-2.
Example 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(); }); } }
Note the usage of a lambda expression in this example. The setOnDragDetected
method starts the drag-and-drop gesture that supports only the MOVE
transfer mode by calling the startDragAndDrop(TransferMode.MOVE)
method.
Handling the Drop of the Data
The target of the drag-and-drop gesture can be either the itemPane
or the bodyPane
object depending on where the drag-and-drop gesture was started, which means that the setOnDragOver
and setOnDragDropped
methods must be implemented for the both objects, itemPane
and bodyPane
.
As described earlier, the itemPane
object is created in the PaperDoll.java
class. Example 8-3 complements the code in Example 8-1, and provides the complete code to create the itemPane
container.
Example 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); }); }
Note that the itemPane.setOnDrageOver
method must accept the transfer mode only if the source of the drag-and-drop gesture was not the itemPane
object itself and the dragboard contains a string.
The itemPane.setOnDragDropped
method is called when the mouse button is released over the itemPane
object, which accepted the previous DRAG_OVER
event. It is here that the draggable piece of clothing is added to the itemPane
container and removed from the bodyPane
object, and the drag-and-drop gesture is completed by calling the setDropCompleted (Boolean)
method on the event.
Similarly, the setOnDragOver
and setOnDragDropped
methods for the bodyPane
container are implemented as shown in Example 8-4.
Example 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(); });
Example 8-5 shows the complete code for the BodyElement
class.
Example 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; } }