layout - hbox - TilePane con mosaicos que se estiran automáticamente en JavaFX
layout javafx 8 (2)
¿Hay alguna forma en JavaFX de sacar lo mejor de TilePane o FlowPane y GridPane?
Esto es lo que me gustaría lograr:
En primer lugar, me gusta la idea de GridPane, donde puedo configurar una cuadrícula M × N que cambia el tamaño automáticamente dentro de su contenedor principal para dividir el espacio por igual en M columnas y N filas. Luego puedo poner algunos elementos secundarios que llenen completamente cada celda, y se extenderán junto con la cuadrícula. Esto es genial.
Pero hay una desventaja: necesito especificar explícitamente dónde debe ir cada control, estableciendo su número de fila y columna.
Luego, hay contenedores de diseño como FlowPane o TilePane que refluyen automáticamente sus elementos secundarios cuando el contenedor principal cambia su tamaño. Cuando agrego otro elemento secundario, se adjunta automáticamente al final de la lista, y la lista de elementos se ajusta automáticamente después de llegar al borde del contenedor cuando hay muy poco espacio para colocar otro elemento allí.
Pero también hay un inconveniente: los elementos secundarios solo pueden tener tamaños rígidos predefinidos. No se estirarán con su elemento padre.
Y esto es lo que necesito:
Necesito lo mejor de estos dos contenedores, es decir, quiero una cuadrícula M por N (digamos 4 × 4) donde cada celda es ParentWidth / M por ParentHeight / N y se extiende junto con la ventana, de modo que siempre esté 4 × 4 células, pero sus tamaños (junto con los tamaños de sus contenidos) se extiende en consecuencia. Pero no quiero decirle al contenedor explícitamente en qué fila y columna colocar cada nuevo hijo que agregue allí. En su lugar, quiero agregarlo y dejar que el contenedor descubra la primera celda vacía en la que colocarlo, llenando las celdas de izquierda a derecha y luego de arriba hacia abajo si no queda una celda vacía en la fila actual.
¿Hay alguna configuración mágica para cualquiera de estos atributos de contenedores predefinidos que me permita lograr esto? ¿O necesito escribir un contenedor así?
Exactamente para el propósito que está describiendo, creé el ButtonGridPane
. Es un primer borrador, y todavía hay margen de mejora, pero tal vez pueda darte una idea básica.
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;
}
}
OK, aquí está mi propio intento de solución:
Como GridPane
funciona casi de la manera que necesito, simplemente no fluye automáticamente sus contenidos, decidí GridPane
subclase de GridPane
y agregar código personalizado para que fluya automáticamente su control secundario. (Gracias a @jns por la sugerencia de que las clases de control de la biblioteca JavaFX pueden ser subclasificadas).
Así que GridPane
el GridPane
y agregué un oyente para su lista interna de elementos secundarios, de modo que cada vez que se agrega o elimina un elemento de esa lista, o la lista cambia de algún modo (por ejemplo, reordenamos los elementos secundarios), se llama a un método privado Refluya a los niños en la cuadrícula asignándolos para corregir columnas y filas según su posición en la lista. También agregué dos funciones auxiliares privadas para convertir la posición en la lista en números de fila y columna y viceversa.
Sé que esto no es perfecto ni elegante, pero bueno, funciona, y funciona lo suficientemente bien para mis necesidades: P, así que publico el código aquí en caso de que alguien más tenga el mismo problema:
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) );
}
}
}
Como puede ver, también usé propiedades para el número de filas y columnas, con getters y setters. Cuando se usa un setter para cambiar el número de filas o columnas, las listas internas de restricciones de columna / fila dentro de GridPane
se GridPane
crear para que GridPane
su tamaño correctamente, y el contenido se vuelve a generar cuando cambia el número de columnas.
También hay anotaciones @NamedArg
en el constructor, de modo que se podría crear este control con un número inicial de filas y columnas del diseño descrito en un archivo FXML.
Dado que prácticamente "resolvió" mi problema de alguna manera, estoy marcando mi respuesta como aceptada. Pero si alguien publicará una solución mejor, con gusto aceptaré su respuesta.
También siéntase libre de decirme si tiene alguna sugerencia acerca de las mejoras de este código, o si cree que se podría hacer algo más elegante, ya que, como dije, difícilmente es perfecto.