【问题标题】:TilePane with automatically stretching tiles in JavaFX在 JavaFX 中自动拉伸瓷砖的 TilePane
【发布时间】:2016-04-25 03:51:21
【问题描述】:

JavaFX 中是否有任何方法可以充分利用 TilePane 或 FlowPane GridPane?
这是我想要实现的目标:

首先,我喜欢 GridPane 的想法,我可以在其中设置一个 M×N 网格,该网格会在其父容器内自动调整大小,以将空间平均分为 M 列和 N 行。然后我可以放置一些完全填充每个单元格的子元素,它们将与网格一起伸展。这很酷。
但是有一个缺点:我需要通过设置每个控件的行号和列号来明确指定每个控件的位置。

然后,有一些布局容器,例如 FlowPane 或 TilePane,它们会在父容器更改其大小时自动重新排列其子元素。当我添加另一个子元素时,它会自动附加到列表的末尾,当空间太少而无法容纳另一个元素时,元素列表会在到达容器边缘后自动换行。
但这里也有一个缺点:子元素只能有固定的、预定义的大小。它们不会随其父元素一起拉伸。

这就是我需要的:
我需要这两个容器中最好的,也就是说,我想要一个 M x N 网格(比如说 4×4),其中每个单元格是 ParentWidth/M by ParentHeight/N 并与窗口一起延伸,所以它总是4×4 单元格,但它们的大小(连同其内容的大小)相应地拉伸。 但是我不想明确地告诉容器将我添加的每个新孩子放在哪一行和哪一列中。相反,我只想添加它并让容器找出要放入的第一个空单元格,从左到右填充单元格,如果当前行中没有空单元格,则从上到下填充。

这些预定义容器的属性是否有一些神奇的设置可以让我实现这一点?还是我需要自己写这样一个容器?

【问题讨论】:

  • 也许回顾一下grid based view of a color chooser 的实现,看看那里的一些逻辑是否可以帮助您实现目标。尝试运行它并调整颜色选择器的大小,以查看其在大小变化时的行为。

标签: layout javafx gridpane flowpane


【解决方案1】:

正是出于您描述的目的,我创建了 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;
    }
}

【讨论】:

  • 嗯,所以可以子类化一个内置的 JavaFX 控件类并添加我自己的功能吗?因为如果是这种情况,我想最好的方法是继承GridPane 并添加一些计数器,这些计数器将跟踪第一个空单元格并在添加/删除子元素时更新。但是我担心当用户在中间某处删除控件时可能会遇到更多麻烦,因为在这种情况下,所有后续控件都需要重飞:/
  • 我能想到的另一种方法是在每次TilePane 容器更改其大小时相应地更改prefTileWidthprefTileHeight。问题是我在TilePane 类中找不到任何有关调整大小的事件:/
  • "问题是我在 TilePane 类中找不到任何关于调整大小的事件" => 见 layoutChildren()
  • 有趣。只能对子类控件进行吗?或者这也可以作为预先存在的控件的某个事件来捕获?因为我真正需要的是在父容器因任何原因改变其大小时得到通知。不过,对于我自己的子类控件,我会检查一下这个方法,听起来很有趣......
【解决方案2】:

好的,这是我自己尝试的解决方案:

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

如您所见,我还使用了行数和列数的属性,包括 getter 和 setter。当使用 setter 更改行数或列数时,GridPane 内的列/行约束的内部列表将被重新创建,以便它们正确调整大小,并且当列数发生变化时,内容将被重排。

在构造函数中还有 @NamedArg 注释,因此可以使用 FXML 文件中描述的布局中的初始行数和列数来创建此控件。

由于它在某种程度上“解决”了我的问题,因此我将我的答案标记为已接受。但是,如果有人会发布更好的解决方案,那么我很乐意接受他的答案。

如果您对改进此代码有任何建议,或者您认为可以更优雅地完成某些事情,请随时告诉我,因为——正如我所说——它还不是完美的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-28
    • 1970-01-01
    • 1970-01-01
    • 2016-11-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多