JavaFX滚动面板PickOnBounds(部分鼠标透明)

4
我目前正在为视频编辑编写基于JavaFX的GUI程序。在这里,我遇到了一个关于ScrollPane的问题。我无法想出如何使ScrollPane(真正)pickOnBounds=”false”。也就是说,我实际上可以通过ScrollPane点击下面的Node。我认为这可能与ScrollPane的视口有关。以下是我们程序的大大简化版本: Styles.css
.scroll-pane > .viewport {
    -fx-background-color: transparent;
    -fx-background-radius: 4;
}

sample.fxml

<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.ScrollPane?>

<StackPane
  xmlns:fx="http://javafx.com/fxml"
  xmlns="http://javafx.com/javafx"
  fx:controller="sample.Controller" fx:id="myStackPane">
    <Pane onMousePressed="#onMouseInteraction" style="-fx-background-color: red"/>
    <AnchorPane fx:id="markerIconPane" pickOnBounds="false">
        <ScrollPane style="-fx-background-color: transparent" pickOnBounds="false" vbarPolicy="ALWAYS" hbarPolicy="ALWAYS"/>
    </AnchorPane>
</StackPane>

Controller.java

package sample;

import javafx.scene.input.MouseEvent;
import javafx.fxml.FXML;

public class Controller implements Initializable {
    //...
    public void onMouseInteraction(MouseEvent mouseEvent) {
        System.out.println(mouseEvent.getX());
    }
}

Main.java

package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        root.getStylesheets().add("./Styles.css");
        primaryStage.setTitle("Hello World");
        primaryStage.setScene(new Scene(root, 300, 275));
        primaryStage.show();
    }

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

现在你可以清楚地看到左上角的滚动条。当你按下滚动条外面的红色区域时,会调用onMouseInteraction(),但当你在滚动条内部按下时,什么也不会发生。

提前感谢
菲利普


请编辑您的帖子并提供一个 MCVE。 - Abra
我希望现在更容易复现。 - philipp404
2个回答

1
假设您提到的视口是 Pane 元素;您可以将 onMouseInteraction 处理程序作为事件过滤器添加到堆栈面板中,使其位于正确的事件链中。例如:
myStackPane.addEventFilter(Controller::onMouseInteraction) ;

那样,Pane 就不需要作为额外的事件层,尽管缺点是你不能仅从 fxml 文件中添加 EventFilter。但是,你可以将该语句作为替代方案添加到控制器的 init 方法中。
如果你想在 Pane 中设置视觉叠加,可以将其 mouseTransparent 属性设置为 true。这样它就不会干扰事件(好像它不存在一样)。
如果你无法将 EventFilter 放入事件链中,则没有一个真正好的解决方案。在 javafx8 中,我通过使用修改过的 PickResult 传播事件来“黑客”系统。但是,在后续版本中,使用的(已弃用)方法已被设置为私有(即无法在没有大量反射的情况下使用)。

编辑

仔细阅读了您的评论后,我认为更加理解问题了。这个问题有点棘手,因此我创建了一个小测试来解决它。但是我不确定它是否适用于您,因为我不知道子结构是如何布局/管理的。所以我们开始吧:

想法是创建一个父节点,它本身没有大小,而只布置它的子节点。这样它就不会阻碍事件选择,但仍然能够在其他组件上渲染组件(因为JavaFX默认情况下未开启剪辑)。

缺点是您需要自己处理很多事情,因为父级必须具有“ 0”的大小。因此,您需要自己处理滚动/子节点布局等事项。

下面我创建了一个示例,仅使用重写的#layoutChildren方法。但在某些情况下(例如,如果您需要滚动),实现专门针对此任务的自定义皮肤可能很方便。任何适合您的需求都可以。

public class Test extends Application
{

    public static void main(String[] args)
    {
        Application.launch(args);
    }
    
    
    
    @Override
    public void start(Stage primaryStage) throws Exception
    {
        Scene s = new Scene(this.createContent());
        primaryStage.setScene(s);
        primaryStage.show();
    }
    
    
    
    protected Parent createContent()
    {
        Pane forGround= this.createForGround();
        StackPane root = new StackPane(this.createBackground(), forGround)
        {
            @Override
            protected void layoutChildren()
            {
                // since the foreGround is not managed, you need to trigger the 'child-layout' manually by calling #requestLayout
                super.layoutChildren();
                forGround.requestLayout();
            }
        };
        
        return root;
    }
    
    protected Node createBackground()
    {
        GridPane pane = new GridPane();
        pane.setPadding(new Insets(5.0));
        pane.setVgap(5.0);
        pane.setHgap(5.0);
        
        for(int i = 0; i < 2; i++)
        {
            for(int j = 0; j < 2; j++)
            {
                Button b = new Button("test " + ((i * 2) + j));
                // some layout to make things testable
                GridPane.setHgrow(b, Priority.ALWAYS);
                GridPane.setVgrow(b, Priority.ALWAYS);
                GridPane.setFillHeight(b, true);
                GridPane.setFillWidth(b, true);
                GridPane.setHalignment(b, HPos.CENTER);
                GridPane.setValignment(b, VPos.CENTER);
                b.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
                // let us know when clicked
                b.setOnAction(E -> System.out.println(b.getText()));
                pane.add(b, i, j);
            }
        }
        
        return pane;
    }
    
    protected Pane createForGround()
    {
        Circle c1 = new Circle(20.0);
        c1.setFill(Color.BLUE);
        c1.setManaged(false);
        c1.setPickOnBounds(false);
        // let us know when clicked
        c1.setOnMouseClicked(E -> System.out.println("clicked blue"));
        
        Circle c2 = new Circle(20.0);
        c2.setFill(Color.RED);
        c2.setManaged(false);
        c2.setPickOnBounds(false);
        // let us know when clicked
        c2.setOnMouseClicked(E -> System.out.println("clicked red"));
        
        
        
        ////////////////////////////////////////////////////////////////
        // this is the old/current method
        ////////////////////////////////////////////////////////////////
        
        // VBox pane = new VBox();
        // pane.setAlignment(Pos.CENTER);
        // pane.setStyle("-fx-background-color:#AAAAAAAA");
        // pane.getChildren().addAll(c1, c2);
        
        
        ////////////////////////////////////////////////////////////////
        // this is the new/improved method
        ////////////////////////////////////////////////////////////////
        
        Pane pane = new Pane()
        {
            protected void layoutChildren()
            {
                // in here you need to layout the children yourself as the default method will not work due to this Pane being size '0'
                
                // get parent bounds instead of this Pane, as it has size '0'
                double pw = ((Region)this.getParent()).getWidth();
                double ph = ((Region)this.getParent()).getHeight();
                
                // resize
                c1.setRadius(ph / 4);
                c2.setRadius(ph / 4);
                
                c1.relocate(((pw - (ph / 2)) / 2), 0);
                c2.relocate(((pw - (ph / 2)) / 2), ph / 2);
            };
        };
        // make sure you set the pickOnBounds to 'false' on the pane as well, otherwise it will still create a(n event pickable)box around its children, even though it is not rendered!
        pane.setPickOnBounds(false);
        // set managed to false, so it will remain size 0
        pane.setManaged(false);
        // set style to make things visible in case of oddities
        pane.setStyle("-fx-background-color:#AAAAAAAA");
        pane.getChildren().addAll(c1, c2);
        
        return pane;
    }
}

再次强调,由于我缺少实现细节,所以不确定这是否适用于您的情况。但无论如何,我猜想您将无法在不覆盖默认机制或创建自己的父类型/皮肤的情况下实现此目标。


感谢快速回答! 然而我不认为这正是我所寻找的。在这里,Pane 更多地是象征性的。实际上,有一个更复杂的底层 fxml 结构,我需要确保用户可以通过鼠标事件与此结构中的 pane 交互。仅仅让 StackPane 捕获所有鼠标事件是行不通的。 - philipp404
当您不调用event#consume时,它不会“捕获”事件。但是,在StackPane的Children被通知之前,将调用EventFilter。这样,您可以在不干扰事件链的情况下监听childElements的事件。在某些情况下,添加EventHandler也可能起作用,但它只会在StackPane的children被通知后触发。当子项消耗事件时,StackPane将永远不会收到通知。这就是为什么EventFilters很重要的原因。 - n247s
好的,但我的问题是,我不知道在单击ScrollPane下面的结构时将调用哪种方法以及该方法可能在哪个类中。 我还需要一些Pane在ScrollPane中是可交互的。 ScrollPane(或ScrollPane的视口)似乎会消耗事件,因为我没有为ScrollPane设置任何onMousePressed,但事件不会传递到底层的Pane。 - philipp404
我认为问题在于当事件分发链创建时,仅考虑到目标节点的路径。然而,作为 StackPane 的另一个子节点的 Pane 没有被测试。事件只是简单地传播到 ScrollPane 再返回。 - philipp404
我想我明白你的意思了。我用另一种方式更新了我的问题。如果以上两种方法都不行,我怀疑如果你想保持当前的结构不变,你就没有办法了。在这种情况下,我建议你重新考虑布局,使其更易于使用JavaFX进行管理/操作。 - n247s

0

我不知道这对您是否是一个合适的解决方案,但是您可以更改sample.fxml文件并将onMousePressed="#onMouseInteraction"添加到ScrollPane

这里是我的建议更改后的sample.fxml文件。

<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.ScrollPane?>

<StackPane
  xmlns:fx="http://javafx.com/fxml"
  xmlns="http://javafx.com/javafx"
  fx:controller="guitests.jfxtests.sample.Controller" fx:id="myStackPane">
    <Pane onMousePressed="#onMouseInteraction" style="-fx-background-color: red"/>
    <AnchorPane fx:id="markerIconPane" pickOnBounds="false">
        <ScrollPane onMousePressed="#onMouseInteraction" style="-fx-background-color: transparent" pickOnBounds="false" vbarPolicy="ALWAYS" hbarPolicy="ALWAYS"/>
    </AnchorPane>
</StackPane>

当你在ScrollPane中单击时,与在Pane内单击一样会触发onMouseInteraction()方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接