I need to calculate the exact position of a control in relation to the height of the content of a scrollpane to be able to set the vValue of the scrollpane, but my formula seems to be not exactly right yet.
I've created a scene with a scrollpane which consists of a table of content and a lot of sections, whereas labels function as section headers:
Table of content
Section 1:
Lorem Ipsum...
Section 2:
Lorem Ipsum...
I use an onMouseReleased event to handle the mouse click on a section label in the table of contents. Now I want to scroll down automatically to the related section label (variable 'control') by setting vValue of the scrollpane.
I calculate the scroll position for the control by the following logic:
scrollPane.setVvalue(0.0); // Reset previous vValue to start from the top
Bounds uberPaneBounds = uberPane.localToScene(uberPane.getBoundsInLocal());
Bounds controlBounds = control.localToScene(control.getBoundsInLocal());
double vValue = controlBounds.getMaxY() / uberPaneBounds.getMaxY();
uberPane is a VBox directly under scrollpane which includes different panes. So the section labels are nested within different panes on different hierarchy levels.
Unfortunately vValue isn't exactly right and becomes less and less correct the further down the related section is in the scrollpane. It seems there is an additional parameter that rises with increasing height. I did a test with 10 sections (incl. ToC):
Any help is appreciated!
The scrollpane can't scroll to a point where the bottom of the content is above the bottom of the scrollpane's viewport. Consequently, the maximum by which you can scroll is the height of the node in the scrollpane's viewport, minus the height of the viewport itself.
Note also you are assuming the scroll value ranges from zero to the maximum scroll offset in pixels. This is probably true unless you explicitly change it, but really you should linearly interpolate the value you want into the range defined by the scrollpane's vmin
and vmax
properties.
Assuming control
is a child of the node displayed in the scrollpane, you need something like:
double y = control.getBoundsInParent().getMinY();
double scrollRange = scrollPane.getVmax() - scrollPane.getVmin() ;
double scrollPixelRange = scrollPane.getContent().getBoundsInLocal().getHeight()
- scrollPane.getViewportBounds().getHeight();
double scrollOffset = y*scrollRange / scrollPixelRange ;
scrollPane.setVvalue(scrollPane.getVmin() + scrollOffset);
Here's a complete example:
import java.util.Random;
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
* JavaFX App
*/
public class App extends Application {
private final Random rng = new Random();
@Override
public void start(Stage stage) {
VBox content = new VBox(5);
VBox tableOfContents = new VBox(5);
content.getChildren().add(tableOfContents);
ScrollPane scrollPane = new ScrollPane(content);
for (int i = 1 ; i <=5 ; i++) {
createSection("Section "+i, scrollPane, content, tableOfContents);
}
Scene scene = new Scene(scrollPane);
stage.setScene(scene);
stage.show();
}
private void createSection(String title, ScrollPane scrollPane, Pane content, Pane tableOfContents) {
Label sectionLabel = new Label(title);
Hyperlink link = new Hyperlink(title);
link.setOnAction(e -> {
double y = sectionLabel.getBoundsInParent().getMinY();
double scrollRange = scrollPane.getVmax() - scrollPane.getVmin() ;
double scrollPixelRange = scrollPane.getContent().getBoundsInLocal().getHeight()
- scrollPane.getViewportBounds().getHeight();
double scrollOffset = y*scrollRange / scrollPixelRange ;
scrollPane.setVvalue(scrollPane.getVmin() + scrollOffset);
});
Hyperlink returnToTop = new Hyperlink("Return to top");
returnToTop.setOnAction(e -> scrollPane.setVvalue(scrollPane.getVmin()));
Pane section = new Pane();
section.setPrefWidth(400);
section.setPrefHeight(200+rng.nextInt(400));
section.setBackground(new Background(new BackgroundFill(Color.LIGHTBLUE, CornerRadii.EMPTY, Insets.EMPTY)));
tableOfContents.getChildren().add(link);
content.getChildren().addAll(sectionLabel, section, returnToTop);
}
public static void main(String[] args) {
launch();
}
}
If you need to scroll to a control which is not a direct child of the scollpane's content, you can modify this with
double y = scrollPane.getContent().sceneToLocal(
control.localToScene(control.getBoundsInLocal())).getMinY();
(This transforms the local coordinates of the control to scene coordinates, then transforms those coordinates to the coordinates of the scrollpane's content.)