javauser-interfacejavafxalignmentscrollpane

Aligning Two Scrolling Gridpanes in Javafx


I have two scrollpanes containing grids that I am trying to align. They should both have cell width of 40, so I'm not sure why they become unaligned. Even though they appear to align at first, they gradually become unaligned.

Here, they are aligned.

But as you can see, it gradually becomes unaligned.

The top grid (with the numbers) and the lower grid are both contained in individual scrollpanes. If I don't do this and instead put both grids together in a VBox and put that into a single scrollpane, everything aligns. However, I need them to be in separate scrollpanes so that I can scroll the lower grid down while keeping the numbers in place and still have the numbers scroll horizontally with the grid (I binded the hscrollvalues of the two scrollpanes bidirectionally).

private void layerDisplay() {
    BeatNumbers bn = new BeatNumbers(this.finalBeat, this.overlay, this::togglePlay,
    this.features::setBeat);

    VBox layerGrid = new VBox();

    for (String layerName : this.layers.keySet()) {
      LayerGrid lg = new LayerGrid(this.layers.get(layerName), layerName, this.finalBeat,
          this.features::addUnit, this.features::deleteUnit, this.features::playUnit,
          this.features::stopAllUnits, this.features::refresh);

      layerGrid.getChildren().add(lg);
    }

    StackPane pane = new StackPane();
    pane.getChildren().add(layerGrid);
    pane.getChildren().add(this.overlay);

    this.gridScrollPane.setContent(pane);
    this.numberPane.setContent(bn);

    VBox box = new VBox();
    box.getChildren().addAll(this.numberPane, this.gridScrollPane);

    this.setCenter(box);
}

Do you have any idea how I can keep the BeatNumbers and layerGrid aligned? The BeatNumbers have to stay fixed on top of the screen when I scroll down, but must move horizontally with the grid when I scroll horizontally.

Thank you!

EDIT: Here is a minimum reproducible example:

This is the code which runs the example

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class MinimumReproducibleExample extends Application {

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


  @Override
  public void start(Stage stage) {
    VBox notAlignedButScrolls = new NotAlignedButScrolls();
    Scene scene = new Scene(notAlignedButScrolls, 400, 300);
    
    stage.setTitle("Not Aligned But Scrolls");
    stage.setScene(scene);
    stage.show();

    VBox alignedButDoesNotScroll = new AlignedButDoesNotScroll();
    Scene scene2 = new Scene(alignedButDoesNotScroll, 400, 300);
    Stage stage2 = new Stage();
    stage2.setTitle("Aligned But Does Not Scroll");
    stage2.setScene(scene2);
    stage2.show();
  }
}

Here is the class for NotAlignedButScrolls, which creates an example where the two grids scroll how I want them to but where the numbers gradually lose alignment with the grid.

import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.input.ScrollEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;

public class NotAlignedButScrolls extends VBox {

  public NotAlignedButScrolls() {
    super();

    // Setting up the scroll panes

    // This is the top scroll pane
    ScrollPane numberPane = new ScrollPane();
    numberPane.addEventFilter(ScrollEvent.SCROLL, event -> {
      if (event.getDeltaY() != 0) {
        event.consume();
      }
    });
    numberPane.setHbarPolicy(ScrollBarPolicy.NEVER);
    numberPane.setVbarPolicy(ScrollBarPolicy.NEVER);
    numberPane.setMaxHeight(15);
    numberPane.setMinHeight(15);

    // This is the lower scroll pane
    ScrollPane gridScrollPane = new ScrollPane();
    gridScrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
    gridScrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);

    // Bind their horizontal scroll values together
    gridScrollPane.hvalueProperty().bindBidirectional(numberPane.hvalueProperty());

    // Set up the content for each scroll pane
    HBox beatNumbers = this.beatNumbers();

    VBox layerGrid = new VBox();
    for (int i = 0; i < 28; i++) {
      layerGrid.getChildren().add(this.gridPane());
    }

    gridScrollPane.setContent(layerGrid);
    numberPane.setContent(beatNumbers);

    gridScrollPane.prefHeightProperty().bind(this.heightProperty());

    // Put everything together

    VBox box = new VBox();
    box.getChildren().addAll(numberPane, gridScrollPane);

    this.getChildren().add(box);
  }

  private HBox beatNumbers() {
    HBox beatNumbers = new HBox();

    HBox.setHgrow(beatNumbers, Priority.ALWAYS);

    for (int i = 0; i < 150; i++) {
      VBox unit = new VBox();
      unit.setMinWidth(40);
      unit.setStyle("-fx-border-width: 0 1 0 0;-fx-border-color: green;");

      Label measureNumber = new Label(String.valueOf(i + 1));
      measureNumber.setStyle("-fx-text-fill: black");

      unit.getChildren().add(measureNumber);
      beatNumbers.getChildren().add(unit);
    }

    return beatNumbers;
  }

  private HBox gridPane() {
    HBox gridPane = new HBox();
    gridPane.setMinHeight(40);
    HBox.setHgrow(gridPane, Priority.ALWAYS);
    gridPane.setStyle("-fx-background-color: gray;-fx-border-style: solid;"
        + "-fx-border-color: black;-fx-border-width: 0 0 1 0");

    for (int i = 0; i < 150; i++) {
      VBox cell = new VBox();
      cell.setMinWidth(40);
      setCellStyle(cell);

      gridPane.getChildren().add(cell);
    }
    return gridPane;
  }

  private void setCellStyle(VBox cell) {
    String style = "-fx-border-style: solid;-fx-border-color: black;-fx-border-width: 0 1 0 0;";

    cell.setStyle("-fx-background-color: null;" + style);
    cell.setOnMouseEntered(e -> cell.setStyle("-fx-background-color: lightgray;" + style));
    cell.setOnMouseExited(e -> cell.setStyle("-fx-background-color: null;" + style));
  }
}

Finally, here is the class for AlignedButDoesNotScroll, which creates an example where the two grids are aligned properly but when you scroll down, the top grid is not fixed at the top of the screen.

import javafx.scene.control.ScrollPane;
import javafx.scene.control.ScrollPane.ScrollBarPolicy;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;

public class AlignedButDoesNotScroll extends VBox {

  public AlignedButDoesNotScroll() {
    super();

    // Setting up the only scroll pane

    ScrollPane gridScrollPane = new ScrollPane();
    gridScrollPane.setHbarPolicy(ScrollBarPolicy.AS_NEEDED);
    gridScrollPane.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);

    // Set up the content for the scroll pane
    HBox beatNumbers = this.beatNumbers();

    VBox layerGrid = new VBox();
    for (int i = 0; i < 28; i++) {
      layerGrid.getChildren().add(this.gridPane());
    }

    // Put everything together

    VBox box = new VBox();
    box.getChildren().addAll(beatNumbers, layerGrid);

    gridScrollPane.setContent(box);
    gridScrollPane.prefHeightProperty().bind(this.heightProperty());

    this.getChildren().add(gridScrollPane);
  }

  private HBox beatNumbers() {
    HBox beatNumbers = new HBox();

    HBox.setHgrow(beatNumbers, Priority.ALWAYS);

    for (int i = 0; i < 150; i++) {
      VBox unit = new VBox();
      unit.setMinWidth(40);
      unit.setStyle("-fx-border-width: 0 1 0 0;-fx-border-color: green;");

      Label measureNumber = new Label(String.valueOf(i + 1));
      measureNumber.setStyle("-fx-text-fill: black");

      unit.getChildren().add(measureNumber);
      beatNumbers.getChildren().add(unit);
    }

    return beatNumbers;
  }

  private HBox gridPane() {
    HBox gridPane = new HBox();
    gridPane.setMinHeight(40);
    HBox.setHgrow(gridPane, Priority.ALWAYS);
    gridPane.setStyle("-fx-background-color: gray;-fx-border-style: solid;"
        + "-fx-border-color: black;-fx-border-width: 0 0 1 0");

    for (int i = 0; i < 150; i++) {
      VBox cell = new VBox();
      cell.setMinWidth(40);
      setCellStyle(cell);

      gridPane.getChildren().add(cell);
    }
    return gridPane;
  }

  private void setCellStyle(VBox cell) {
    String style = "-fx-border-style: solid;-fx-border-color: black;-fx-border-width: 0 1 0 0;";

    cell.setStyle("-fx-background-color: null;" + style);
    cell.setOnMouseEntered(e -> cell.setStyle("-fx-background-color: lightgray;" + style));
    cell.setOnMouseExited(e -> cell.setStyle("-fx-background-color: null;" + style));
  }
}

NotAlignedButScrolls

AlignedButDoesNotScroll

I hope this helps to explain the problem. Thanks again!


Solution

  • The problem is that the viewports of the two ScrollPanes are not the same size. The lower one is smaller to accommodate the scrollbars.

    Something like this can help...

    gridScrollPane.viewportBoundsProperty().addListener((obs,oldVal, newVal) -> {
        double w = newVal.getWidth() + 2; // this 2 accounts for 1 pixel borders
        numberPane.setPrefWidth(w);
        numberPane.setMaxWidth(w);
    });
    

    Another option would be to force the VBar policy for both ScrollPanes to ALWAYS. That doesn't look as nice though.