TilePane with automatically stretching tiles in JavaFX

前端 未结 2 570
半阙折子戏
半阙折子戏 2021-01-19 05:18

Is there any way in JavaFX to take best from both TilePane or FlowPane and GridPane?
Here\'s what I\'d like to achieve:

First, I like the idea

相关标签:
2条回答
  • 2021-01-19 06:05

    OK here's my own attempt at the solution:

    Since the GridPane works almost the way I need, just doesn't automatically flow its contents, I decided to subclass GridPane and add custom code for automatic flowing its child controls. (Thanks to @jns for the hint that JavaFX library control classes can be subclassed.)

    So I subclassed the GridPane and added a listener for its internal list of children, so that whenever a child is added or removed from that list, or the list changes in some way (e.g. we reorder the children), a private method is called to reflow the children in the grid by assigning them to correct columns and rows by their position in the list. I also added two private helper functions to convert the position in the list into row & column numbers and back.

    I know that this isn't perfect nor elegant, but hey, it works, and it works well enough for my needs :P so I publish the code here in case someone else had the same problem:

    package flowgrid;
    
    import javafx.collections.ObservableList;
    import javafx.collections.ListChangeListener;
    
    import javafx.scene.Node;
    import javafx.scene.layout.GridPane;
    import javafx.scene.layout.ColumnConstraints;
    import javafx.scene.layout.RowConstraints;
    import javafx.scene.layout.Priority;
    import javafx.geometry.HPos;
    import javafx.geometry.VPos;
    
    import javafx.beans.property.IntegerProperty;
    import javafx.beans.property.SimpleIntegerProperty;
    import javafx.beans.NamedArg;
    
    /**
     * This class subclasses the GridPane layout class.
     * It manages its child nodes by arranging them in rows of equal number of tiles.
     * Their order in the grid corresponds to their indexes in the list of children
     * in the following fashion (similarly to how FlowPane works):
     * 
     *      +---+---+---+---+
     *      | 0 | 1 | 2 | 3 |
     *      +---+---+---+---+
     *      | 4 | 5 | 6 | 7 |
     *      +---+---+---+---+
     *      | 8 | 9 | … |   |
     *      +---+---+---+---+
     *  
     * It observes its internal list of children and it automatically reflows them
     * if the number of columns changes or if you add/remove some children from the list.
     * All the tiles of the grid are of the same size and stretch accordingly with the control.
     */
    public class FlowGridPane extends GridPane
    {
       // Properties for managing the number of rows & columns.
       private IntegerProperty rowsCount;
       private IntegerProperty colsCount;
    
       public final IntegerProperty colsCountProperty() { return colsCount; }
       public final Integer getColsCount() { return colsCountProperty().get(); }
       public final void setColsCount(final Integer cols) {
          // Recreate column constraints so that they will resize properly.
          ObservableList<ColumnConstraints> constraints = getColumnConstraints();
          constraints.clear();
          for (int i=0; i < cols; ++i) {
             ColumnConstraints c = new ColumnConstraints();
             c.setHalignment(HPos.CENTER);
             c.setHgrow(Priority.ALWAYS);
             c.setMinWidth(60);
             constraints.add(c);
          }
          colsCountProperty().set(cols);
          reflowAll();
       }
    
       public final IntegerProperty rowsCountProperty() { return rowsCount; }
       public final Integer getRowsCount() { return rowsCountProperty().get(); }
       public final void setRowsCount(final Integer rows) {
          // Recreate column constraints so that they will resize properly.
          ObservableList<RowConstraints> constraints = getRowConstraints();
          constraints.clear();
          for (int i=0; i < rows; ++i) {
             RowConstraints r = new RowConstraints();
             r.setValignment(VPos.CENTER);
             r.setVgrow(Priority.ALWAYS);
             r.setMinHeight(20);
             constraints.add(r);
          }
          rowsCountProperty().set(rows);
          reflowAll();
       }
    
       /// Constructor. Takes the number of columns and rows of the grid (can be changed later).
       public FlowGridPane(@NamedArg("cols")int cols, @NamedArg("rows")int rows) {
          super();
          colsCount = new SimpleIntegerProperty();  setColsCount(cols);
          rowsCount = new SimpleIntegerProperty();  setRowsCount(rows);
          getChildren().addListener(new ListChangeListener<Node>() {
             public void onChanged(ListChangeListener.Change<? extends Node> change) {
                reflowAll();
             }
          } );
       }
    
       // Helper functions for coordinate conversions.
       private int coordsToOffset(int col, int row) { return row*colsCount.get() + col; }
       private int offsetToCol(int offset) { return offset%colsCount.get(); }
       private int offsetToRow(int offset) { return offset/colsCount.get(); }
    
       private void reflowAll() {
          ObservableList<Node> children = getChildren();
          for (Node child : children ) {
             int offs = children.indexOf(child);
             GridPane.setConstraints(child, offsetToCol(offs), offsetToRow(offs) );
          }
       }
    }
    

    As you can see, I also used properties for the number of rows and columns, with getters and setters. When one uses a setter to change the number of rows or columns, the internal lists of column/row constraints inside GridPane are being recreated so that they resized properly, and the content is being reflown when the number of columns changes.

    There are also @NamedArg annotations in the constructor, so that one could create this control with an initial number of rows & columns from the layout described in an FXML file.

    Since it pretty much "solved" my problem in a way, I'm marking my answer as accepted. But if anyone will post a better solution, I'll gladly accept his answer then.

    Also feel free to tell me if you have any suggestions about improvements of this code, or if you think something could be done more elegantly, since – as I said – it is hardly perfect yet.

    0 讨论(0)
  • 2021-01-19 06:22

    Exactly for the purpose you are describing, I created the ButtonGridPane . It's a first draft, and there is still room for improvement, but maybe it can give you a basic idea.

    public class ButtonGrid extends Pane {
    
        private static final double DEFAULT_RATIO = 0.618033987;
    
        private int                 columnCount;
        private double              ratio;
    
        public ButtonGrid(int columnCount) {
            this(columnCount, DEFAULT_RATIO);
        }
    
        public ButtonGrid(int columnCount, double heightToWidthRatio) {
            getStyleClass().add("button-grid");
            this.columnCount = columnCount;
            ratio = heightToWidthRatio;
        }
    
        public void setColumnCount(int columnCount) {
            this.columnCount = columnCount;
        }
    
        public void setHeightToWidthRatio(double ratio) {
            this.ratio = ratio;
        }
    
        @Override
        public Orientation getContentBias() {
            return Orientation.HORIZONTAL;
        }
    
        @Override
        protected void layoutChildren() {
            double left = getInsets().getLeft();
            double top = getInsets().getTop();
    
            double tileWidth = calculateTileWidth(getWidth());
            double tileHeight = calculateTileHeight(getWidth());
    
            ObservableList<Node> children = getChildren();
            double currentX = left;
            double currentY = top;
            for (int idx = 0; idx < children.size(); idx++) {
                if (idx > 0 && idx % columnCount == 0) {
                    currentX = left;
                    currentY = currentY + tileHeight;
                }
                children.get(idx).resize(tileWidth, tileHeight);
                children.get(idx).relocate(currentX, currentY);
                currentX = currentX + tileWidth;
            }
        }
    
        @Override
        protected double computePrefWidth(double height) {
            double w = 0;
            for (int idx = 0; idx < columnCount; idx++) {
                Node node = getChildren().get(idx);
                w += node.prefWidth(-1);
            }
            return getInsets().getLeft() + w + getInsets().getRight();
        }
    
        @Override
        protected double computePrefHeight(double width) {
            double h = calculateTileHeight(width) * getRowCount();
            return getInsets().getTop() + h + getInsets().getBottom();
        }
    
        private double calculateTileHeight(double width) {
            return calculateTileWidth(width) * ratio;
        }
    
        private double calculateTileWidth(double width) {
            return (-getInsets().getLeft() + width - getInsets().getRight()) / columnCount;
        }
    
        private int getRowCount() {
            return getChildren().size() / columnCount;
        }
    }
    
    0 讨论(0)
提交回复
热议问题