JavaFX 使节点部分鼠标透明

JavaFX make Node partially mouseTransparent

如果我有两个节点堆叠在彼此之上并重叠,我怎样才能使顶部节点成为 mouseTransparent(以便底部节点可以对 MouseEvents 做出反应)同时让顶部节点对某些 MouseEvents 做出反应比如 onMouseEntered?

例如,考虑 StackPane 中的两个(假设为矩形)窗格,底部的窗格较小且完全位于顶部的下方:

<StackPane>
<Pane onMouseEntered="#printA" onMouseClicked="#printB" />
<Pane onMouseEntered="#printC" />
</StackPane>

如果用户将鼠标移到顶部窗格上,则应该在控制台中打印 C。如果他还将鼠标移到底部窗格上,那么也应该打印 A。如果他在底部窗格上单击鼠标,则应打印 B。单击顶部窗格而不是底部窗格应该不会执行任何操作。

我为什么要做这样的事情?我想检测鼠标何时移动到窗格的中心附近,以便我可以更改窗格的中心内容(基本上从显示模式到编辑模式)并让用户与新内容进行交互。我希望检测区域大于中心本身,因此它会与窗格内的其他一些东西重叠。所以 Pane 中心不能是检测器,它必须是透明的东西叠加在上面。检测器也必须保持在那里,以便它可以检测到鼠标何时再次移开。

lots of questions on Whosebug个看起来很相似,但几乎都被setMouseTransparent(true)setPickOnBounds(true)解决了。 setMouseTransparent 在这里不起作用,因为顶部窗格将不会打印 C。setPickOnBounds 使窗格 mouseTransparent 在窗格 alpha/visually 透明的任何地方,但透明部分将不会打印C 和不透明部分会阻止下窗格打印 A 或 B。因此,即使顶部窗格完全透明或完全不透明,也无法解决我的问题。将顶部窗格的可见性设置为 false 也将不起作用,因为顶部窗格无法打印 C。

我能想到的一种方法,就是观察鼠标在父节点上的移动,看鼠标指针是否落在目标节点的检测区域上。这样您就不需要虚拟(透明)节点进行检测。

于是思路如下:

<StackPane id="parent">
     <Pane onMouseEntered="#printA" onMouseClicked="#printB" />
      // Other nodes in the parent
</StackPane>
  • 像往常一样,您将在中心节点上有处理程序。
  • 在父节点上添加一个 mouseMoved 处理程序来检测鼠标是否进入检测区域。

请检查下面的工作演示:

import javafx.application.Application;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class MouseEventsDemo extends Application {
    double detectionSize = 30;

    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane center = getBlock("red");
        center.setOnMouseEntered(e -> System.out.println("Entered on center pane..."));
        center.setOnMouseClicked(e -> System.out.println("Clicked on center pane..."));

        // Simulating that the 'center' is surrounded by other nodes
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.addRow(0, getBlock("yellow"), getBlock("pink"), getBlock("orange"));
        grid.addRow(1, getBlock("orange"), center, getBlock("yellow"));
        grid.addRow(2, getBlock("yellow"), getBlock("pink"), getBlock("orange"));


        // Adding rectangle only for zone visual purpose
        Rectangle zone = new Rectangle(center.getPrefWidth() + 2 * detectionSize, center.getPrefHeight() + 2 * detectionSize);
        zone.setStyle("-fx-stroke:blue;-fx-stroke-width:1px;-fx-fill:transparent;-fx-opacity:.4");
        zone.setMouseTransparent(true);

        StackPane parent = new StackPane(grid, zone);

        VBox root = new VBox(parent);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(20));
        root.setSpacing(20);
        Scene scene = new Scene(root, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();

        addDetectionHandler(parent, center);
    }

    private Pane getBlock(String color) {
        Pane block = new Pane();
        block.setStyle("-fx-background-color:" + color);
        block.setMaxSize(100, 100);
        block.setPrefSize(100, 100);
        return block;
    }

    private void addDetectionHandler(StackPane parent, Pane node) {
        final String key = "onDetectionZone";
        parent.setOnMouseMoved(e -> {
            boolean mouseEntered = (boolean) node.getProperties().computeIfAbsent(key, p -> false);
            if (!mouseEntered && isOnDetectionZone(e, node)) {
                node.getProperties().put(key, true);
                // Perform your mouse enter operations on detection zone,.. like changing to edit mode.. or what ever
                System.out.println("Entered on center pane detection zone...");
                node.setStyle("-fx-background-color:green");

            } else if (mouseEntered && !isOnDetectionZone(e, node)) {
                node.getProperties().put(key, false);
                // Perform your mouse exit operations from detection zone,.. like change back to default state from edit mode
                System.out.println("Exiting from center pane detection zone...");
                node.setStyle("-fx-background-color:red");
            }
        });
    }

    private boolean isOnDetectionZone(MouseEvent e, Pane node) {
        Bounds b = node.localToScene(node.getBoundsInLocal());
        double d = detectionSize;
        Bounds detectionBounds = new BoundingBox(b.getMinX() - d, b.getMinY() - d, b.getWidth() + 2 * d, b.getHeight() + 2 * d);
        return detectionBounds.contains(e.getSceneX(), e.getSceneY());
    }
}

注意:您可以使用更好的方法来检查鼠标指针是否落在所需节点的检测区域中:)

更新:方法#2

看起来您正在寻找的是一个节点,它类似于一个有孔的相框 :)。如果是这种情况,我能想到的方法是构建一个形状(如框架)并将其放置在所需的节点上。

然后您可以简单地将鼠标处理程序添加到检测区域和中心节点。

请查看下面的工作演示:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Polyline;
import javafx.stage.Stage;

public class MouseEventsDemo2 extends Application {
    double detectionSize = 30;

    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane center = getBlock("red");
        center.getChildren().add(new Label("Hello"));
        center.setOnMouseEntered(e -> {
            System.out.println("Entered on center pane...");
            center.setStyle("-fx-background-color:green");
        });
        center.setOnMouseClicked(e -> System.out.println("Clicked on center pane..."));

        // Simulating that the 'center' is surrounded by other nodes
        GridPane grid = new GridPane();
        grid.setAlignment(Pos.CENTER);
        grid.addRow(0, getBlock("yellow"), getBlock("pink"), getBlock("orange"));
        grid.addRow(1, getBlock("orange"), center, getBlock("yellow"));
        grid.addRow(2, getBlock("yellow"), getBlock("pink"), getBlock("orange"));

        StackPane parent = new StackPane(grid);

        VBox root = new VBox(parent);
        root.setAlignment(Pos.CENTER);
        root.setPadding(new Insets(20));
        root.setSpacing(20);
        Scene scene = new Scene(root, 500, 500);
        primaryStage.setScene(scene);
        primaryStage.show();

        // Building the frame using Polyline node
        Polyline zone = new Polyline();
        zone.setStyle("-fx-fill:grey;-fx-opacity:.5;-fx-stroke-width:0px;");
        zone.setOnMouseEntered(e -> {
            System.out.println("Entered on detection zone...");
            center.setStyle("-fx-background-color:green");
        });
        zone.setOnMouseExited(e -> {
            System.out.println("Exited on detection zone...");
            center.setStyle("-fx-background-color:red");
        });
        zone.setOnMouseClicked(e -> System.out.println("Clicked on detection zone..."));
        parent.getChildren().add(zone);
        parent.layoutBoundsProperty().addListener(p -> updatePolylineZone(center, zone));
        center.layoutBoundsProperty().addListener(p -> updatePolylineZone(center, zone));
        updatePolylineZone(center, zone);
    }

    /**
     * Update the poly line shape to build a frame around the center node if the parent or center bounds changed.
     */
    private void updatePolylineZone(Pane center, Polyline zone) {
        zone.getPoints().clear();

        Bounds b = center.localToParent(center.getBoundsInLocal());
        double s = detectionSize;
        ObservableList<Double> pts = FXCollections.observableArrayList();

//             A-------------------------B
//             |                         |
//             |    a---------------b    |
//             |    |               |    |
//             |    |               |    |
//             |    |               |    |
//             |    d---------------c    |
//             |                         |
//             D-------------------------C

        // Outer square
        pts.addAll(b.getMinX() - s, b.getMinY() - s);  // A
        pts.addAll(b.getMaxX() + s, b.getMinY() - s);  // B
        pts.addAll(b.getMaxX() + s, b.getMaxY() + s);  // C
        pts.addAll(b.getMinX() - s, b.getMaxY() + s);  // D

        // Inner Square
        pts.addAll(b.getMinX() + 1, b.getMaxY() - 1);  // d
        pts.addAll(b.getMaxX() - 1, b.getMaxY() - 1);  // c
        pts.addAll(b.getMaxX() - 1, b.getMinY() + 1);  // b
        pts.addAll(b.getMinX() + 1, b.getMinY() + 1);  // a

        // Closing the loop
        pts.addAll(b.getMinX() - s, b.getMinY() - s);  // A
        pts.addAll(b.getMinX() - s, b.getMaxY() + s);  // D
        pts.addAll(b.getMinX() + 1, b.getMaxY() - 1);  // d
        pts.addAll(b.getMinX() + 1, b.getMinY() + 1);  // a
        pts.addAll(b.getMinX() - s, b.getMinY() - s);  // A

        zone.getPoints().addAll(pts);
    }

    private Pane getBlock(String color) {
        Pane block = new Pane();
        block.setStyle("-fx-background-color:" + color);
        block.setMaxSize(100, 100);
        block.setPrefSize(100, 100);
        return block;
    }
}

这是一个可以使用的潜在策略。

  1. 创建一个节点,该节点是您要检测与之交互的区域的形状。
    • 在下面的例子中它是一个圆圈,在这个描述中我称之为“检测节点”。
  2. 使检测节点鼠标透明,这样它就不会消耗或与任何标准鼠标事件交互。
  3. 向场景添加事件侦听器(根据需要使用事件处理程序或事件过滤器)。
  4. 当场景事件侦听器识别到一个也应该路由到检测节点的事件时,复制该事件并专门在检测节点触发它。
  5. 然后检测节点可以响应重复的事件。
  6. 原始事件的处理也不知道检测节点,因此它可以与场景中的其他节点交互,就好像检测节点从未存在过一样。

效果会是两个事件发生,目标节点和检测节点可以分别处理。

import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

import java.util.concurrent.atomic.AtomicBoolean;

public class LayersFX extends Application {
    private final ListView<String> logViewer = new ListView<>();

    // set to true if you with to see move events in the log.
    private final boolean LOG_MOVES = false;

    @Override
    public void start(Stage stage) {
        Rectangle square = new Rectangle(200, 200, Color.LIGHTSKYBLUE);
        Circle circle = new Circle(80, Color.LEMONCHIFFON);
        StackPane stack = new StackPane(square, circle);

        addEventHandlers(square);
        addEventHandlers(circle);

        VBox layout = new VBox(10, stack, logViewer);
        layout.setPadding(new Insets(10));
        logViewer.setPrefSize(200, 200);

        Scene scene = new Scene(layout);

        routeMouseEventsToNode(scene, circle);

        stage.setScene(scene);
        stage.show();
    }

    /**
     * Intercepts mouse events from the scene and created duplicate events which
     * are routed to the node when appropriate.
     */
    private void routeMouseEventsToNode(Scene scene, Node node) {
        // make the node transparent to standard mouse events.
        // it will only receive mouse events we specifically create and send to it.
        node.setMouseTransparent(true);

        // consume all events for the target node so that we don't 
        // accidentally let a duplicated event bubble back up to the scene
        // and inadvertently cause an infinite loop.
        node.addEventHandler(EventType.ROOT, Event::consume);

        // Atomic isn't used here for concurrency, it is just
        // a trick to make the boolean value effectively final
        // so that it can be used in the lambda.
        AtomicBoolean inNode = new AtomicBoolean(false);
        scene.setOnMouseMoved(
                event -> {
                    boolean wasInNode = inNode.get();
                    boolean nowInNode = node.contains(
                            node.sceneToLocal(
                                    event.getSceneX(),
                                    event.getSceneY()
                            )
                    );
                    inNode.set(nowInNode);

                    if (nowInNode) {
                        node.fireEvent(
                                event.copyFor(
                                        node,
                                        node,
                                        MouseEvent.MOUSE_MOVED
                                )
                        );
                    }

                    if (!wasInNode && nowInNode) {
                        node.fireEvent(
                                event.copyFor(
                                        node,
                                        node,
                                        MouseEvent.MOUSE_ENTERED_TARGET
                                )
                        );
                    }

                    if (wasInNode && !nowInNode) {
                        node.fireEvent(
                                event.copyFor(
                                        node,
                                        node,
                                        MouseEvent.MOUSE_EXITED_TARGET
                                )
                        );
                    }
                }
        );
    }

    private void addEventHandlers(Node node) {
        String nodeName = node.getClass().getSimpleName();

        node.setOnMouseEntered(
                event -> log("Entered " + nodeName)
        );

        node.setOnMouseExited(
                event -> log("Exited " + nodeName)
        );

        node.setOnMouseClicked(
                event -> log("Clicked " + nodeName)
        );

        node.setOnMouseMoved(event -> {
            if (LOG_MOVES) {
                log(
                        "Moved in " + nodeName +
                                " (" + Math.floor(event.getX()) + "," + Math.floor(event.getY()) + ")"
                );
            }
        });
    }

    private void log(String msg) {
        logViewer.getItems().add(msg);
        logViewer.scrollTo(logViewer.getItems().size() - 1);
    }

    public static void main(String[] args) {
        launch(args);
    }

}

使用自定义事件分派链可能有一些更好的方法来执行此操作,但上面的逻辑是我想出的。该逻辑似乎可以满足您在问题中提出的要求,但它可能不具备您实际应用程序所需的全部功能。