0
votes

I want to set Scrollpane H and V values but after inner content was resized.

Some context - I got following structure:

enter image description here

I attached .setOnScroll(...) (mouse) event to StackPane and when it happens, the inner content is resized (image) to some values (aka zoom). I want to add that StackPane min size is binded to scrollpane viewport size and Pane size binded to image size to keep everything working properly.

Up to this point everything works fine, when content changes size, scrollbars are updated and everything adjust as it supossed to be but I want to keep or rather set scrollbars positions to some values to keep zooming to previous middle point (something like zoom in photoshop). In this case I added this line in my callback to test it:

scrollPane.setHvalue(0.5);

The function itself works but it's overridden later (probably when layout recalculate) and this way it's always wrong. I tested values via event

scrollPane.hvalueProperty().addListener((observable, oldvalue, newvalue) -> {
     System.out.println(newvalue);
});

and the output is kind of

0.5
0.45172283226050736
0.5
0.45196805476326296
0.5
0.4522703818369453

// or this when scaled down 

0.5
0.5591296121097445
0.5
0.5608324439701174

That's why my conclusion is that I succeed setting 0.5 but later layout probably set different value for resized content becuase i changed size restrictions to Pane and StackPane.

My question then how should I do this?

Is there any onResize event to corrent or force my own value? or other way to schedule scroll event or my updates to be done when layout is recalculated?

// EDIT

I cleaned the code and here is example:

Main.java

package application;

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


public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {
            BorderPane root = (BorderPane)FXMLLoader.load(getClass().getResource("Sample.fxml"));
            Scene scene = new Scene(root,800,600);
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

application.css is empty

/* JavaFX CSS - Leave this comment until you have at least create one rule which uses -fx-Property */

Sample.fxml (replace image url)

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.StackPane?>

<BorderPane xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.SampleController">
   <center>
      <ScrollPane fx:id="scrollPane" prefHeight="600.0" prefWidth="800.0" BorderPane.alignment="CENTER">
         <content>
            <StackPane fx:id="stackPane">
               <children>
                  <Pane fx:id="clipContainer">
                     <children>
                        <ImageView fx:id="img" pickOnBounds="true" preserveRatio="true">
                           <image>
                              <Image url="@../../../../Desktop/sample.jpg" />
                           </image>
                        </ImageView>
                     </children>
                  </Pane>
               </children>
            </StackPane>
         </content>
      </ScrollPane>
   </center>
</BorderPane>

and SampleController.java

package application;

import java.net.URL;
import java.util.ResourceBundle;
import javafx.beans.binding.Bindings;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ScrollPane;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;


public class SampleController implements Initializable {


    @FXML
    private Pane clipContainer;

    @FXML
    private StackPane stackPane;

    @FXML
    private ImageView img;

    @FXML
    private ScrollPane scrollPane;

    final double SCALE_DELTA = 1.1;

    private double imgW;
    private double imgH;
    private double scale = 1;


    @Override
    public void initialize(URL location, ResourceBundle resources) {


        clipContainer.setStyle("-fx-background-color: #999999");
        stackPane.setStyle("-fx-background-color: #CCCCCC");

        imgW = img.getImage().getWidth();
        imgH = img.getImage().getHeight();

        // bind max and min to adjust to new image bounds

        stackPane.minWidthProperty().bind(Bindings.createDoubleBinding(() -> 
            scrollPane.getViewportBounds().getWidth(), scrollPane.viewportBoundsProperty()
        ));
        stackPane.minHeightProperty().bind(Bindings.createDoubleBinding(() -> 
            scrollPane.getViewportBounds().getHeight(), scrollPane.viewportBoundsProperty()
        ));

        clipContainer.maxWidthProperty().bind(Bindings.createDoubleBinding(() -> 
            img.getFitWidth(), img.fitWidthProperty()
        ));
        clipContainer.maxHeightProperty().bind(Bindings.createDoubleBinding(() -> 
            img.getFitHeight(), img.fitHeightProperty()
        ));

        // initial scale
        img.setFitWidth(imgW * 1);
        img.setFitHeight(imgH * 1);

        // on mouse scroll
        stackPane.setOnScroll(event -> {
            event.consume();

            if (event.getDeltaY() == 0) {
              return;
            }

            double scaleFactor =
              (event.getDeltaY() > 0)
                ? SCALE_DELTA
                : 1/SCALE_DELTA;


            scale *= scaleFactor;

            img.setFitWidth(imgW * scale);
            img.setFitHeight(imgH * scale);

            // HERE i want to do something to keep my image where it was
            scrollPane.setHvalue(0.5);

        });

        scrollPane.hvalueProperty().addListener((observable, oldvalue, newvalue) -> {
            System.out.println(newvalue);
        });

    }
}
2
Why did you even use a ScrollPane? Using ScrollPane to resize instead of scrolling up/down/left/right sounds like bad usage to me. setOnScroll() can be used on any node.Jai
@Jai I don't use ScrollPane to resize but mouse scroll event to resize inner image. Scrollpane is used to navigate zoomed image - it's exactly the same situation like zoomed canvas in photoshop and you navigate by scrollbars. The problem I got is that I want to keep image in position before zooming that's why i try to set scrollbar to some position but whatever I set it will be changed later, probably after layout realize that inner content changed size and scrollbar max, min and value have to be updated I belive.Griva
Please provide a minimal reproducible example that demonstrates the problem.kleopatra
@kleopatra Sorry I did not do it before, code was total mess and I had to clear it much. I thought it's rather easy problem and will be easy task to fix it - my bad. I edited question and added code with example so check it if you can, thank you.Griva

2 Answers

1
votes

While I am unfamiliar with JavaFX, one would think that you could "force your own value" by resizing the scrollbar bounds whenever it changes. So, execute scrollPane.setHvalue(0.5) within the ChangeListener callback instead:

scrollPane.hvalueProperty().addListener((DoubleProperty observable, double oldvalue, double newvalue) -> {

    if (newvalue != 0.5) {
        scrollPane.setHvalue(0.5);
    }

});
0
votes

This was asked about a year and a half ago, but for anyone still looking for an answer to this -- the layout overrides the hvalue and vvalue properties exactly once. That means, instead of setting the values directly, you can attach a listener that disposes of itself after veto'ing an attempt to change the value.

That is, given the wrapping class that automagically unregisters a listener..

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;

public class ChangeOnceListener<T> implements ChangeListener<T> {

    private ChangeListener<T> wrapped;

    public ChangeOnceListener(ChangeListener<T> wrapped) {
        this.wrapped = wrapped;
    }

    @Override
    public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
        observable.removeListener(this);
        wrapped.changed(observable, oldValue, newValue);
    }

}

.. OP's scenario could be fixed with something like the following.

        pane.setOnScroll(e -> {
            pane.hvalueProperty().addListener(new ChangeOnceListener<>((obv, old, neww) -> {
                pane.setHvalue(0.5);
            }));

            pane.vvalueProperty().addListener(new ChangeOnceListener<>((obv, old, neww) -> {
                pane.setVvalue(0.5);
            }));
        });