文档



JavaFX:事件处理
8 PaperDoll拖放应用程序(发布版8)
8 PaperDoll 拖放应用程序(版本8)

8 PaperDoll 拖放应用程序

本章进一步使用 PaperDoll 应用程序来说明拖放功能。

拖放操作中解释的基本原理在这里被应用于一个更高级的应用程序,该应用程序允许用户拖动衣服的图像并将其放置在纸娃娃的图像上,以及从纸娃娃的图像上拖动衣服的图像。

PaperDoll 应用程序的布局

PaperDoll 应用程序显示了四个代表衣服(服装部件)的图像和一个参与拖放操作的纸娃娃图像。应用程序窗口如图8-1所示。

图8-1 纸娃娃应用程序

图8-1的描述如下
"图8-1 纸娃娃应用程序"的描述

应用程序的图形场景由两部分组成:

  • 在窗口的上部显示一个 VBox 对象。它包含一个图像和“纸娃娃”文本,仅用于装饰。

  • 在窗口的下部显示一个 GridPane 对象。

    • 第一列包含一个带有衣服图像的 FlowPane 对象。

    • 第二列包含一个带有纸娃娃图像的 Pane 对象。

衣服的图像可以被拖放到纸娃娃的图像上,然后再拖回到它们的原始位置。 PaperDoll 应用程序提供了一个拖放操作的示例,其中同一个对象既可以是操作的源也可以是目标。

PaperDoll应用程序的组织结构

PaperDoll应用程序包含以下包和类:

  • PaperDoll.java是主要的应用程序类,它布局用户界面(UI)元素并实现应用程序逻辑。

  • paperdoll.body包含定义接受数据拖放的身体容器的类。

  • paperdoll.clothes包含定义可拖动的衣物的类。

  • paperdoll.images包含应用程序的图形资源。

注意:

本章不提供逐步构建PaperDoll应用程序的过程。

您可以下载PaperDoll.zip查看完成的NetBeans项目。

示例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都是itemPanebodyPane中的一个节点。 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传输模式的拖放手势。

处理数据的拖放

拖放手势的目标可以是itemPanebodyPane对象,取决于拖放手势的起始位置。这意味着必须为itemPanebodyPane对象实现setOnDragOversetOnDragDropped方法。

如前所述,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容器的setOnDragOversetOnDragDropped方法的实现如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;
    }
}
关闭窗口

目录

JavaFX: 事件处理

展开 折叠