TilePane с автоматически растягивающимися плитками в JavaFX

Есть ли в JavaFX какой-либо способ получить лучшее от TilePane или FlowPane и GridPane?
Вот чего я хотел бы достичь:

Во-первых, мне нравится идея GridPane, где я могу настроить сетку M×N, которая автоматически изменяет размер в своем родительском контейнере, чтобы поровну разделить пространство на M столбцов и N строк. Затем я могу поставить несколько дочерних элементов, которые полностью заполняют каждую ячейку, и они будут растягиваться вместе с сеткой. Это круто.
Но есть один недостаток: мне нужно явно указать, куда должен идти каждый элемент управления, задав номер строки и столбца.

Кроме того, есть контейнеры компоновки, такие как FlowPane или TilePane, которые автоматически перекомпоновывают свои дочерние элементы, когда родительский контейнер меняет свой размер. Когда я добавляю еще один дочерний элемент, он автоматически прикрепляется в конец списка, и список элементов автоматически переносится после достижения края контейнера, когда места слишком мало для размещения там другого элемента.
Но вот недостаток: дочерние элементы могут иметь только жесткие, заранее определенные размеры. Они не будут растягиваться с его родительским элементом.

И вот что мне нужно:
Мне нужно лучшее из обоих этих контейнеров, то есть я хочу сетку M на N (скажем, 4×4), где каждая ячейка имеет размеры ParentWidth/M на ParentHeight/N и растягивается. вместе с окном, так что это всегда 4х4 ячейки, но их размеры (вместе с размерами их содержимого) соответственно растягиваются. Но я не хочу явно указывать контейнеру, в какую строку и столбец помещать каждый новый дочерний элемент, который я добавляю туда. Вместо этого я хочу просто добавить его и позволить контейнеру определить первую пустую ячейку, чтобы поместить его, заполняя ячейки слева направо, а затем сверху вниз, если в текущей строке не осталось пустой ячейки.

Есть ли какая-то волшебная настройка для любого из атрибутов этих предопределенных контейнеров, которая позволила бы мне добиться этого? Или мне нужно самому написать такой контейнер?


person SasQ    schedule 25.04.2016    source источник
comment
Возможно, просмотрите эту реализацию представления средства выбора цвета на основе сетки и посмотрите, там поможет вам достичь ваших целей. Попробуйте запустить его и изменить размер окна выбора цвета, чтобы увидеть его поведение при изменении размера.   -  person jewelsea    schedule 04.05.2016


Ответы (2)


Именно для той цели, которую вы описываете, я создал ButtonGridPane . Это первый набросок, и есть еще возможности для улучшения, но, возможно, он может дать вам основную идею.

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;
    }
}
person jns    schedule 25.04.2016
comment
Хм, так можно просто создать подкласс встроенного класса управления JavaFX и добавить свои собственные функции? Потому что, если это так, я думаю, лучший способ — создать подкласс GridPane и просто добавить несколько счетчиков, которые будут отслеживать первую пустую ячейку и обновляться при добавлении/удалении дочерних элементов. Но я боюсь, что может быть больше проблем, когда пользователь удаляет элемент управления где-то посередине, потому что в этом случае все последующие элементы управления должны быть перекомпонованы :/ - person SasQ; 25.04.2016
comment
Другой способ, который я могу придумать, - это изменять prefTileWidth и prefTileHeight соответственно каждый раз, когда контейнер TilePane меняет свой размер. Проблема в том, что я не могу найти никаких событий, касающихся изменения размера в классе TilePane:/ - person SasQ; 25.04.2016
comment
Проблема в том, что я не могу найти никаких событий, касающихся изменения размера в классе TilePane => см. layoutChildren() - person jewelsea; 04.05.2016
comment
Интересный. Можно ли это сделать только для подклассов элементов управления? Или это можно было бы зафиксировать как какое-то событие для ранее существовавшего элемента управления? Потому что мне действительно нужно просто получать информацию, когда родительский контейнер меняет свои размеры по каким-либо причинам. Однако для моих собственных подклассов элементов управления, конечно, я проверю этот метод, звучит интересно... - person SasQ; 05.05.2016

ОК, вот моя собственная попытка решения:

Поскольку GridPane работает почти так, как мне нужно, просто не передает свое содержимое автоматически, я решил создать подкласс GridPane и добавить пользовательский код для автоматической передачи своих дочерних элементов управления. (Спасибо @jns за подсказку о том, что классы управления библиотекой JavaFX могут быть подклассами.)

Поэтому я создал подкласс GridPane и добавил прослушиватель для его внутреннего списка дочерних элементов, так что всякий раз, когда дочерний элемент добавляется или удаляется из этого списка, или список каким-либо образом изменяется (например, мы переупорядочиваем дочерние элементы), вызывается частный метод для перекомпоновать дочерние элементы в сетке, назначив им правильные столбцы и строки в соответствии с их положением в списке. Я также добавил две частные вспомогательные функции для преобразования позиции в списке в номера строк и столбцов и обратно.

Я знаю, что это не идеально и не элегантно, но, эй, это работает, и работает достаточно хорошо для моих нужд: P, поэтому я публикую код здесь на случай, если у кого-то еще возникнет такая же проблема:

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

Как видите, я также использовал свойства для количества строк и столбцов с геттерами и сеттерами. Когда кто-то использует установщик для изменения количества строк или столбцов, внутренние списки ограничений столбца/строки внутри GridPane воссоздаются, чтобы их размер изменялся правильно, а содержимое переформатируется при изменении количества столбцов.

В конструкторе также есть аннотации @NamedArg, чтобы можно было создать этот элемент управления с начальным количеством строк и столбцов из макета, описанного в файле FXML.

Поскольку это в значительной степени «решило» мою проблему, я отмечаю свой ответ как принятый. Но если кто-нибудь опубликует лучшее решение, я с радостью приму его ответ.

Также не стесняйтесь сообщить мне, если у вас есть какие-либо предложения по улучшению этого кода, или если вы думаете, что что-то можно сделать более элегантно, поскольку, как я уже сказал, он еще далеко не совершенен.

person SasQ    schedule 04.05.2016