4

I am currently working on a JavaFX based GUI Program for video editing. Here I have run into a problem with ScrollPanes. I cannot figure out how to make a ScrollPane (truly) pickOnBounds=„false“ . Meaning that I can actually click through the ScrollPane onto the Nodes below. I think that this may be an issue with the Viewport of the ScrollPane. I have tried to give a massively simplified version of our program below:

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);
    }
}

Now you can clearly see the scrollbars in the top left. When you press on the red outside the scrollbars, onMouseInteraction() gets called but when you press within the scrollbars nothing happens.

Thanks in advance
Philipp

philipp404
  • 41
  • 3

2 Answers2

1

Assuming the Pane element is the viewport you mentioned; You could add the onMouseInteraction handler as an EventFilter to the stackpane so it is in the correct eventchain. E.g.

myStackPane.addEventFilter(Controller::onMouseInteraction) ;

That way the Pane is not needed as an extra eventlayer, although the downside is that you cannot achieve adding the EventFilter from only the fxml file. You can however add the statement in the init method of the controller as an alternative.

If you want a visual overlay in the Pane you can set its mouseTransparent property to true. This way it won't interfere with the event (as if it weren't there)

If you cannot put the EventFilter in the eventchain there is not really a good solution. In javafx8 I 'hacked' the system by propagating the event with a modified PickResult. But in the later versions the used (depricated) methods has been made private (i.e. Not usable without heavy reflection).


EDIT

After carefully rereading your comments, I think I understand the problem a bit more. It is quit tricky to pull something like that off, so I created a small test on how I would solve it. I am not sure if it is usable for you though, as I have no idea how the substructere is layout/managed. So here we go:

The idea is to make a Parent Node which has no size itself, but only which only layout its children. That way it won't block an eventPick, but is still able to render components on top of others (as clipping is not on by default in JavaFX).

The downside of this is that you need to take care of a lot yourself, as the parent has to have a size of '0'. So things like scrolling/children layout you need to do by yourself.

Below I created an example which only uses an overridden #layoutChildren method. But in some cases (if you need scrolling for example) it can be handy to implement your own skin specialized for this task. Whatever suits your needs.

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;
    }
}

Again, I have no clue if this is usable in your case (as I miss implementation details). But whichever way you turn it, I suspect you won't be able to achieve this witout either overriding default mechanics or creating your own Parent-type /skin.

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
n247s
  • 1,898
  • 1
  • 12
  • 30
  • Thanks for the fast answer! However I do not think this is exactly what I am looking for. The Pane is more or less symbolical here. In reality there is a much more complicated underlying fxml structure and I need to ensure that the user can Interact with panes in this structure via MouseEvents. Just making the StackPane catch all MouseEvents would not work. – philipp404 Jan 29 '20 at 20:32
  • When you don't call `event#consume` it wont 'catch' the event. But the EventFilter will be called before the StackPane's Children has been notified. That way you can listen to events of childElements without messing with the eventChain. Adding an EventHandler might in some cases also work, but it will only get fired after the StackPane's children are notified. When a child than consumes the event, the StackPane will never get notified. Thats why EventFilters are a thing. – n247s Jan 29 '20 at 20:46
  • Okay, but my problem is, that I do not know what method will be called by clicking on the structure below the ScrollPane and in what class that method may be. I also need some Panes in the ScrollPane to be intractable. The ScrollPane (or the viewport of the ScrollPane) seems to consume the Event, as I have not set anything for `onMousePressed` for the ScrollPane but the event does not get handed onto the underlying Pane. – philipp404 Jan 29 '20 at 21:14
  • I think the problem is that when the Event Dispath Chain gets created, only the path to the target node is considered. However the Pane as it is another child of the StackPane is therefore not tested. The Event simply travels to the ScrollPane and back. – philipp404 Jan 29 '20 at 21:18
  • I think I get what you mean. I updated my question with another alternative. If neither of the above methods will do, I suspect you are out of luck if you want to keep the current structure alive. In that case I would advise you to reconsider the layout to make it more managable/workable with JavaFX. – n247s Feb 04 '20 at 22:38
0

I don't know if this is a suitable solution for you, but you can change the sample.fxml file and add onMousePressed="#onMouseInteraction" to the ScrollPane.

Here is the file sample.fxml with my proposed change.

<?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>

Then when you click inside the ScrollPane, method onMouseInteraction() is invoked just as it is when you click inside the Pane.

Abra
  • 19,142
  • 7
  • 29
  • 41