JavaFX: Fit Columns of TableView to content to avoid cut the content - javafx

I have following problem: When i create a TableView and put data in the columns, the columns fit to content automatically. But if there many rows (more than 30) JavaFX optimize the columnwidth to the average length of all content.
In the first example i put the long strings first in the table and everything is fine.
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
// Init table
TableView<Person> table = new TableView<Person>();
table.setMaxWidth(Double.MAX_VALUE);
table.setMaxHeight(Double.MAX_VALUE);
table.setItems(persons);
// Init columns
TableColumn<Person, String> firstname = new TableColumn<Person, String>("Firstname");
firstname.setCellValueFactory(new PropertyValueFactory<Person, String>("firstname"));
table.getColumns().add(firstname);
TableColumn<Person, String> lastname = new TableColumn<Person, String>("Lastname");
lastname.setCellValueFactory(new PropertyValueFactory<Person, String>("lastname"));
table.getColumns().add(lastname);
TableColumn<Person, String> email = new TableColumn<Person, String>("E-Mail");
email.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
table.getColumns().add(email);
// Init Stage
Scene scene = new Scene(table, 400, 150);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public class Person {
private String firstname;
private String lastname;
private String email;
public Person(String firstname, String lastname, String email) {
this.firstname = firstname;
this.lastname = lastname;
this.email = email;
}
// Getters and Setters
}
}
Looks good...
In the second example i put short strings first in the table and at least the long strings. In this example JavaFX choose columnwidth to small and the content gets cut.
public class Example extends Application {
#Override
public void start(Stage stage) {
// Init data
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 0; i < 100; i++) {
persons.add(new Person("Max", "Power", "Max.Power#test.com"));
}
persons.add(new Person("Maximus-Superman", "Power", "Maximus-Superman.Power#test.com"));
persons.add(new Person("Max", "Powerstrongsupercool", "Max.Powerstrongsupercool#test.com"));
persons.add(new Person("Maximus-Superman", "Powerstrongsupercool", "Maximus-Superman.Powerstrongsupercool#test.com"));
[...]
Looks bad...
How can i avoid this?​
EDIT 19.05.2018
The links in the comments didn`t work.
So, i have found the problem in the source of JavaFx:
In the updateScene() method the maxRows are set to 30. There is no way to change the value because every methods that are involved are protected or private.
The comment is right. It can be take much time to create a table if there many rows in the table. But sometimes a developer knows the much possible rows of a table or it´s okay to risk higher loadingtime.
One solution is to contact Oracle to create a setter() for the value of max. rows. So the developer can choose the max. rows for each column individually.
public class TableColumnHeader extends Region {
[...]
private void updateScene() {
// RT-17684: If the TableColumn widths are all currently the default,
// we attempt to 'auto-size' based on the preferred width of the first
// n rows (we can't do all rows, as that could conceivably be an unlimited
// number of rows retrieved from a very slow (e.g. remote) data source.
// Obviously, the bigger the value of n, the more likely the default
// width will be suitable for most values in the column
final int n = 30; // ------------------------------------------> This is the problem!
if (! autoSizeComplete) {
if (getTableColumn() == null || getTableColumn().getWidth() != DEFAULT_COLUMN_WIDTH || getScene() == null) {
return;
}
doColumnAutoSize(getTableColumn(), n);
autoSizeComplete = true;
}
}
[...]
private void doColumnAutoSize(TableColumnBase<?,?> column, int cellsToMeasure) {
double prefWidth = column.getPrefWidth();
// if the prefWidth has been set, we do _not_ autosize columns
if (prefWidth == DEFAULT_COLUMN_WIDTH) {
getTableViewSkin().resizeColumnToFitContent(column, cellsToMeasure);
}
}
[...]
}
public class TableViewSkin<T> extends TableViewSkinBase<T, T, TableView<T>, TableViewBehavior<T>, TableRow<T>, TableColumn<T, ?>> {
[...]
#Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
if (!tc.isResizable()) return;
// final TableColumn<T, ?> col = tc;
List<?> items = itemsProperty().get();
if (items == null || items.isEmpty()) return;
Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
if (cellFactory == null) return;
TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumnBase
cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.snappedLeftInset() + r.snappedRightInset();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows); // ------------------> if maxRows equals -1 every item will be checked
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(tc);
cell.updateTableView(tableView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
// dispose of the cell to prevent it retaining listeners (see RT-31015)
cell.updateIndex(-1);
// RT-36855 - take into account the column header text / graphic widths.
// Magic 10 is to allow for sort arrow to appear without text truncation.
TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
Node graphic = header.label.getGraphic();
double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
maxWidth = Math.max(maxWidth, headerWidth);
// RT-23486
maxWidth += padding;
if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
maxWidth = Math.max(maxWidth, tc.getWidth());
}
tc.impl_setWidth(maxWidth);
}
[...]
}
Another solution is to fire a MouseEvent on the header of the rect of the TableColumn.
If there a MouseEvent with a ClickCount equals 2 and the PrimaryButton is down the resizeColumnToFitContent() method is called with a value for maxRows of -1.
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
-1 means all rows that are in the TableView.
public class NestedTableColumnHeader extends TableColumnHeader {
[...]
private static final EventHandler<MouseEvent> rectMousePressed = new EventHandler<MouseEvent>() {
#Override public void handle(MouseEvent me) {
Rectangle rect = (Rectangle) me.getSource();
TableColumnBase column = (TableColumnBase) rect.getProperties().get(TABLE_COLUMN_KEY);
NestedTableColumnHeader header = (NestedTableColumnHeader) rect.getProperties().get(TABLE_COLUMN_HEADER_KEY);
if (! header.isColumnResizingEnabled()) return;
if (me.getClickCount() == 2 && me.isPrimaryButtonDown()) {
// the user wants to resize the column such that its
// width is equal to the widest element in the column
header.getTableViewSkin().resizeColumnToFitContent(column, -1); // -----------------------> this method should be call and everything is fine
} else {
// rather than refer to the rect variable, we just grab
// it from the source to prevent a small memory leak.
Rectangle innerRect = (Rectangle) me.getSource();
double startX = header.getTableHeaderRow().sceneToLocal(innerRect.localToScene(innerRect.getBoundsInLocal())).getMinX() + 2;
header.dragAnchorX = me.getSceneX();
header.columnResizingStarted(startX);
}
me.consume();
}
};
[...]
}
So, is it possible to create a new MouseEvent with a ClickCount of 2 and the PrimaryButtonDown-boolean is true and fire this to the TableColumn?
And: How can i contact Oracle to please them to create a setter() for the maxRows in the next release?

Related

JavaFX8: The last TableColumn's header text get clipped off if there's more than 45 TableColumns

Important disclaimer: I'm using fx that's packaged with JDK 8u202
I'm creating a TableView with about 100 TableColumns but when I scroll horizontally to the last TableColumn, that last TableColumn is clipped in half. (for reference, the 99th column is the last column)
Here's a SSCCE of this:
import javafx.application.Application;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Main extends Application {
private static final int N_COLS = 100;
private static final int N_ROWS = 1_000;
public void start(Stage stage) throws Exception {
TableView<ObservableList<String>> tableView = new TableView<>();
// add columns
for (int i = 0; i < N_COLS; i++) {
final int finalIdx = i;
TableColumn<ObservableList<String>, String> column = new TableColumn<>(String.valueOf(i));
column.setCellValueFactory(param -> new ReadOnlyObjectWrapper<>(param.getValue().get(finalIdx)));
tableView.getColumns().add(column);
// column.setMinWidth(100);
}
// create rowData
String[] rowData = new String[N_COLS];
for (int i = 0; i < N_COLS; i++)
rowData[i] = "Longggggg string";
// add data to TableView
for (int i = 0; i < N_ROWS; i++)
tableView.getItems().add(FXCollections.observableArrayList(rowData));
// Explicitly set PrefWidth for each TableColumn based on each column's header text, and every cell's width in that column
// autoResizeColumns(tableView);
Scene scene = new Scene(tableView, 800, 800);
stage.setScene(scene);
stage.show();
}
public static void autoResizeColumns( TableView<?> tableView )
{
//Set the right policy
tableView.getColumns().forEach( (column) ->
{
Text t = new Text( column.getText() );
double max = 0.0f;
max = t.getLayoutBounds().getWidth();
for ( int i = 0; i < tableView.getItems().size(); i++ )
{
//cell must not be empty
if ( column.getCellData( i ) != null )
{
t = new Text( column.getCellData( i ).toString() );
double calcwidth = t.getLayoutBounds().getWidth();
//remember new max-width
if ( calcwidth > max )
max = calcwidth;
}
}
// set the new max-widht with some extra space
column.setPrefWidth( max + 15.0d );
} );
}
public static void main(String[] args) {
launch(args);
}
}
I've tried explicitly setting the PrefWidth for each TableColumn based on the header's width, and the cell content's width and it appears that the width is indeed being updated, but the last TableColumn is still clipped in half. I did this by calling this method:
autoResizeColumns(tableView);
So far I've seen that setting each TableColumn's MinWidth to 100 fixes this bug. But I do not wish to use this method since I still prefer having each column's width being computed automatically given that some of the fields will be much shorter than these long string values I have here.
This is the method I'm referring to:
column.setMinWidth(100);

Various Colours In The Same Table Cell, Possible?

I have a request to display a string in various colours in a table cell, that is one portion of a string in one colour and the rest in another colour (either the background or the text). I have found an article on changing the cell background colour, but not a portion of a cell. That is close to the requirement, but don't meet the requirement.
The only possible solution, I can think of, is to use the Text type which can be set with various colours after splitting a string into two parts. But, how to use the Text type data with the TableView setup as the following?
aColumn.setCellValueFactory(p -> new SimpleStringProperty(...) );
...
aTalbeView.setItems(FXcollections.observableArrayList(...));
I am still new to JavaFX. Is it doable? If so, how shall I approach a solution?
A mock up table is attached below.
The cellValueFactory is used to tell the cell what data to display. To tell the cell how to display its data, use a cellFactory. The two are more or less independent.
So you can do
aColumn.setCellValueFactory(p -> new SimpleStringProperty(...));
and then something like:
aColumn.setCellFactory(tc -> new TableCell<>() {
private final String[] palette = new String[] {
"#1B9E77", "#D95F02", "#7570B3", "#E7298A",
"#66A61E", "#E6AB02", "#A6761D", "#666666" };
private TextFlow flow = new TextFlow();
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setGraphic(null);
} else {
flow.getChildren().clear();
int i = 0 ;
for (String word : item.split("\\s")) {
Text text = new Text(word);
text.setFill(Color.web(palette[i++ % palette.length]);
flow.getChildren().add(text);
flow.getChildren().add(new Text(" "));
}
setGraphic(flow);
}
}
});
This assumes each cell has multiple words (separated by whitespace) and colors each word a different color. You can implement the different colors any way you like; this shows the basic idea.
The approach used in this answer
An additional range parameter is added to the backing model to indicate the highlight range for text in the cell.
The cellValueFactory uses a binding statement to allow the cell to respond to updates to either the text in the cell or the highlight range.
Labels within an HBox are used for the cell graphic rather than a
TextFlow as labels have more styling options (e.g. for text
background) than text nodes in TextFlow.
Using multiple labels within the cells does change some of the eliding behavior of the cell when not enough room is available in the column to include all text, this could be customized by setting properties on the HBOX or label to configure this behavior how you want.
CSS stylesheet for styling is included in the code but could be
extracted to a separate stylesheet if desired.
I didn't thoroughly test the solution, so there may be logic errors around some of the boundary conditions.
Screenshots
Highlighted a subset of text within a cell in a non-selected row:
Highlighted a subset of text within a cell in a selected row:
Example code
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class HighlightedTextTableViewer extends Application {
private static final String CSS_DATA_URL = "data:text/css,";
private static final String HIGHLIGHTABLE_LABEL_CSS = CSS_DATA_URL + // language=CSS
"""
.highlightable {
-fx-font-family: monospace;
-fx-font-weight: bold;
}
.highlight {
-fx-background-color: cornflowerblue;
-fx-text-fill: white;
}
""";
private static final String HIGHLIGHTABLE_STYLE_CLASS = "highlightable";
private static final String HIGHLIGHTED_STYLE_CLASS = "highlight";
#Override
public void start(Stage stage) {
TableView<Field> table = createTable();
populateTable(table);
VBox layout = new VBox(
10,
table
);
layout.setPadding(new Insets(10));
layout.setPrefHeight(100);
stage.setScene(new Scene(layout));
stage.show();
}
private TableView<Field> createTable() {
TableView<Field> table = new TableView<>();
TableColumn<Field, String> nameColumn = new TableColumn<>("Name");
nameColumn.setCellValueFactory(
p -> p.getValue().nameProperty()
);
TableColumn<Field, Field> valueColumn = new TableColumn<>("Value");
valueColumn.setCellValueFactory(
p -> Bindings.createObjectBinding(
p::getValue,
p.getValue().valueProperty(), p.getValue().highlightRangeProperty()
)
);
valueColumn.setCellFactory(param -> new HighlightableTextCell());
//noinspection unchecked
table.getColumns().setAll(nameColumn, valueColumn);
return table;
}
public static class HighlightableTextCell extends TableCell<Field, Field> {
protected void updateItem(Field item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty || item.getValue() == null) {
setGraphic(null);
} else {
setGraphic(constructTextBox(item));
}
}
private Node constructTextBox(Field item) {
HBox textBox = new HBox();
textBox.getStylesheets().setAll(HIGHLIGHTABLE_LABEL_CSS);
textBox.getStyleClass().add(HIGHLIGHTABLE_STYLE_CLASS);
int from = item.getHighlightRange() != null ? item.getHighlightRange().from() : -1;
int valueLen = item.getValue() != null ? item.getValue().length() : -1;
int to = item.getHighlightRange() != null ? item.getHighlightRange().to() : -1;
if (item.highlightRangeProperty() == null
|| from >= to
|| from > valueLen
) { // no highlight specified or no highlight in range.
textBox.getChildren().add(
createStyledLabel(
item.getValue()
)
);
} else {
textBox.getChildren().add(
createStyledLabel(
item.getValue().substring(
0,
from
)
)
);
if (from >= valueLen) {
return textBox;
}
textBox.getChildren().add(
createStyledLabel(
item.getValue().substring(
from,
Math.min(valueLen, to)
), HIGHLIGHTED_STYLE_CLASS
)
);
if (to >= valueLen) {
return textBox;
}
textBox.getChildren().add(
createStyledLabel(
item.getValue().substring(
to
)
)
);
}
return textBox;
}
private Label createStyledLabel(String value, String... styleClasses) {
Label label = new Label(value);
label.getStyleClass().setAll(styleClasses);
return label;
}
}
private void populateTable(TableView<Field> table) {
table.getItems().addAll(
new Field("Dragon", "93 6d 6d da", null),
new Field("Rainbow", "0c fb ff 1c", new Range(3, 8))
);
}
}
class Field {
private final StringProperty name;
private final StringProperty value;
private final ObjectProperty<Range> highlightRange;
public Field(String name, String value, Range highlightRange) {
this.name = new SimpleStringProperty(name);
this.value = new SimpleStringProperty(value);
this.highlightRange = new SimpleObjectProperty<>(highlightRange);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getValue() {
return value.get();
}
public StringProperty valueProperty() {
return value;
}
public void setValue(String value) {
this.value.set(value);
}
public Range getHighlightRange() {
return highlightRange.get();
}
public ObjectProperty<Range> highlightRangeProperty() {
return highlightRange;
}
public void setHighlightRange(Range highlightRange) {
this.highlightRange.set(highlightRange);
}
}
record Range(int from, int to) {}
Alternative using TextField
An alternative to the HBox for displaying highlighted text would be to use a TextField (non-editable), which allows a selection to be set (via APIs on the text field), however, I did not attempt a solution with a TextField approach. A TextField may allow a user to drag the mouse to select text (perhaps could be disabled if desired by making the field mouse transparent).
Related Questions (uses TextFlow)
Highlight text in TableView with TextFlow
JavaFX TableView with highlighted text
JavaFX: setting background color for Text controls

Modifying cellfactory of TreeView

I would like to change the disclosure node of expanding/unexpanding in Tree View without using -fx-background-image of CSS .arrow, because eventhough the image is 9*9 pixel, it shows so bad. I want to use the setCellFactory, but I don't know how.
I have several questions :
in setCellFactory, what is the purpose of overriding call or updateItem method? which one to override in this case?
what is the different in item == null or boolean empty = true in updateItem? what case does the boolean empty = true handles?
I want my view to be like this
since TreeView can only contain one type of Item and Group A label is not a Person, other than putting Group A as a name of a Person object I guess I can only display the tree using a loop and put the TreeItems under a Box according to which Group they belong to. However, I don't know how to make expandable Box (use VBox?Hbox?StackPane?) please give me a hint about this
package DummyGUI;
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.*;
import javafx.event.ActionEvent;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
public class App extends Application
{
private final Image male = new Image(getClass().getResourceAsStream("male.png"), 16, 16, true, true);
private final Node maleIcon = new ImageView(male);
private final Image female = new Image(getClass().getResourceAsStream("female.png"), 16, 16, true, true);
private final Node femaleIcon = new ImageView(female);
private final Image plus = new Image(getClass().getResourceAsStream("plus-button.png"), 16, 16, true, true);
private final Node plusIcon = new ImageView(plus);
private final Image minus = new Image(getClass().getResourceAsStream("minus-button.png"), 16, 16, true, true);
private final Node minusIcon = new ImageView(minus);
private TreeView<Person> tree;
public static void main(String[] args)
{
launch();
}
public void start(Stage topView)
{
createGUI(topView);
}
private void createGUI(Stage topView)
{
topView.setTitle("Dummy App");
initTree();
VBox vb = new VBox(tree);
topView.setScene(new Scene(vb));
topView.show();
}
private void initTree()
{
Person person1 = new Person("Charles", 'M', '0');
Person person2 = new Person("John", 'M', 'A');
Person person3 = new Person("Pearl", 'M', 'A');
TreeItem<Person> root = new TreeItem<>(person1);
TreeItem<Person> child1 = new TreeItem<>(person2);
TreeItem<Person> child2 = new TreeItem<>(person3);
tree = new TreeView<>(root);
root.setExpanded(true);
root.getChildren().addAll(child1, child2);
tree.setCellFactory(tv ->
{
HBox hb = new HBox();
TreeCell<Person> cell = new TreeCell<Person>()
{
#Override
public void updateItem(Person item, boolean empty)
{
super.updateItem(item, empty);
if(empty)
{
setGraphic(null);
setText(null);
}
else
{
Node icon = (item.getGender() == 'M' ? maleIcon : femaleIcon);
setGraphic(icon);
setText(item.toString());
}
}
};
cell.setDisclosureNode(plusIcon);
return cell;
});
}
}
package DummyGUI;
public class Person
{
String name;
char gender;
char group;
public Person(String name, char gender, char group)
{
this.name = name;
this.gender = gender;
this.group = group;
}
public String getName()
{
return name;
}
public char getGender()
{
return gender;
}
public char getGroup()
{
return group;
}
public String toString()
{
return name;
}
}
I tried to change the arrow icon to a plus icon but it has bug because I don't understand how to override updateItem.
I don't know why the gender icon for John and Charles does not show when the tree is expanded and I dont know how to add minus icon.
Thank you
Minimalistic answer:
The basic problem is the re-use of the same instance of the nodes in all cells - nodes must have exactly one parent. Using the same, silently removes it from its former parent (or throws an exception)
The adapted cell
tree.setCellFactory(tv -> {
// per-cell icons
Node maleIcon = new Button("m");
Node femaleIcon = new Button("f");
Node expanded = new Label("-");
Node collapsed = new Label("+");
TreeCell<Person> cell = new TreeCell<Person>() {
#Override
public void updateItem(Person item, boolean empty) {
super.updateItem(item, empty);
// check if the cell is empty (no data) or if the data is null
if (item == null || empty) {
setGraphic(null);
setText(null);
} else {
Node icon = (item.getGender() == 'M' ? maleIcon
: femaleIcon);
setGraphic(icon);
// never-ever override toString for application reasons
// instead set the text from data properties
setText(item.getName());
}
if (getTreeItem() != null) {
// update disclosureNode depending on expanded state
setDisclosureNode(getTreeItem().isExpanded() ? expanded : collapsed);
}
}
};
return cell;
});
if (getTreeItem() != null)
{
setDisclosureNode(getTreeItem().isExpanded() ? expanded : collapsed);
setOnMouseClicked(event -> {
setDisclosureNode(getTreeItem().isExpanded() ? expanded : collapsed);
});
}
its ok, i think with setOnMouseClicked it becomes good x0 thanks!

JavaFX retrieve TableCells of selected row

In my JavaFX TableView, I am trying to retrieve TableCells from a selected row to mark them
with custom colors.
Simply changing the colors of the entire row does not work in this case, as I use different color shadings in each cell depending
on the value of each cell
The example below shows two approaches I tried I to solve the problem
1) Use a listener to retrieve cells in the selected row. Printing the row index and content already works
However, I could not find how to retrieve a TableCell from table.getSelectionModel().
2) Try a dirty workaround to add the TableCells to a global data structure in the columnCellFactory.
However, the TableCells do not get added to the tableCells ArrayList for some reason.
To obtain a short example, the imports and the Classes defining the EditingCell (custom TableCell) and CellEditEvent were omitted.
package TableViewColExample;
public class TableViewExample extends Application {
private Callback<TableColumn, TableCell> columnCellFactory ;
final TableView<String[]> table = new TableView<String[]>();
ObservableSet<Integer> selectedRowIndexes = FXCollections.observableSet();
ObservableSet<String> selectedRows = FXCollections.observableSet();
ArrayList<ArrayList<EditingCell>> tableColumns = new ArrayList<ArrayList<EditingCell>>();
#Override
public void start(Stage stage) {
String[][] dat = new String[][]{
{"C1","C2","C3"},{"a","b","c"},{"d","e","f"},{"g","i","h"}};
ObservableList<String []> data = FXCollections.observableArrayList();
data.addAll(Arrays.asList(dat));
data.remove(0);
table.setItems(data);
for (int i = 0; i < dat[0].length; i++) {
TableColumn tc = new TableColumn(dat[0][i]);
final int colNo = i;
tc.setCellValueFactory(new Callback<CellDataFeatures<String[], String>, ObservableValue<String>>() {
public ObservableValue<String> call(CellDataFeatures<String[], String> p) {
return new SimpleStringProperty((p.getValue()[colNo]));
}
});
ArrayList<EditingCell> tableCells = new ArrayList<EditingCell>();
columnCellFactory =
new Callback<TableColumn, TableCell>() {
public TableCell call(TableColumn p) {
EditingCell tcell = new EditingCell();
//For some reason, the EditingCell is never added to the list
tableCells.add(tcell);
return tcell;
}
};
tc.setCellFactory(columnCellFactory);
tableColumns.add(tableCells);
//The printed value here is 0, which means that the Factory does not add the Editing Cell to the List
System.out.println(" Column rows "+tableCells.size());
table.getColumns().add(tc);
}
//Output: TableColumns 3, TableRows 0
System.out.println("TableColumns "+ tableColumns.size() + " Table rows "+tableColumns.get(0).size());
table.setItems(data);
table.getSelectionModel().getSelectedCells().addListener((Change<? extends TablePosition> change) -> {
selectedRows.clear();
table.getSelectionModel().getSelectedCells().stream().map(TablePosition::getRow).f orEach(row -> {
selectedRowIndexes.add(row);
System.out.println(selectedRowIndexes.toString());
});
table.getSelectionModel().getSelectedItems().forEach(row -> {
selectedRows.add(Arrays.toString(row));
System.out.println(selectedRows.toString());
});
});
stage.setScene(new Scene(table));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}

JavaFX TableColumn resize to fit cell content

I'm looking for a way to resize a TableColumn in a TableView so that all of the content is visible in each cell (i.e. no truncation).
I noticed that double clicking on the column divider's does auto fit the column to the contents of its cells. Is there a way to trigger this programmatically?
Digging through the javafx source, I found that the actual method called when you click TableView columns divider is
/*
* FIXME: Naive implementation ahead
* Attempts to resize column based on the pref width of all items contained
* in this column. This can be potentially very expensive if the number of
* rows is large.
*/
#Override protected void resizeColumnToFitContent(TableColumn<T, ?> tc, int maxRows) {
if (!tc.isResizable()) return;
// final TableColumn<T, ?> col = tc;
List<?> items = itemsProperty().get();
if (items == null || items.isEmpty()) return;
Callback/*<TableColumn<T, ?>, TableCell<T,?>>*/ cellFactory = tc.getCellFactory();
if (cellFactory == null) return;
TableCell<T,?> cell = (TableCell<T, ?>) cellFactory.call(tc);
if (cell == null) return;
// set this property to tell the TableCell we want to know its actual
// preferred width, not the width of the associated TableColumnBase
cell.getProperties().put(TableCellSkin.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE);
// determine cell padding
double padding = 10;
Node n = cell.getSkin() == null ? null : cell.getSkin().getNode();
if (n instanceof Region) {
Region r = (Region) n;
padding = r.snappedLeftInset() + r.snappedRightInset();
}
int rows = maxRows == -1 ? items.size() : Math.min(items.size(), maxRows);
double maxWidth = 0;
for (int row = 0; row < rows; row++) {
cell.updateTableColumn(tc);
cell.updateTableView(tableView);
cell.updateIndex(row);
if ((cell.getText() != null && !cell.getText().isEmpty()) || cell.getGraphic() != null) {
getChildren().add(cell);
cell.applyCss();
maxWidth = Math.max(maxWidth, cell.prefWidth(-1));
getChildren().remove(cell);
}
}
// dispose of the cell to prevent it retaining listeners (see RT-31015)
cell.updateIndex(-1);
// RT-36855 - take into account the column header text / graphic widths.
// Magic 10 is to allow for sort arrow to appear without text truncation.
TableColumnHeader header = getTableHeaderRow().getColumnHeaderFor(tc);
double headerTextWidth = Utils.computeTextWidth(header.label.getFont(), tc.getText(), -1);
Node graphic = header.label.getGraphic();
double headerGraphicWidth = graphic == null ? 0 : graphic.prefWidth(-1) + header.label.getGraphicTextGap();
double headerWidth = headerTextWidth + headerGraphicWidth + 10 + header.snappedLeftInset() + header.snappedRightInset();
maxWidth = Math.max(maxWidth, headerWidth);
// RT-23486
maxWidth += padding;
if(tableView.getColumnResizePolicy() == TableView.CONSTRAINED_RESIZE_POLICY) {
maxWidth = Math.max(maxWidth, tc.getWidth());
}
tc.impl_setWidth(maxWidth);
}
It's declared in
com.sun.javafx.scene.control.skin.TableViewSkinBase
method signature
protected abstract void resizeColumnToFitContent(TC tc, int maxRows)
Since it's protected, You cannot call it from e.g. tableView.getSkin(), but You can always extend the TableViewSkin overriding only resizeColumnToFitContent method and make it public.
As #Tomasz suggestion, I resolve by reflection:
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.Skin;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class GUIUtils {
private static Method columnToFitMethod;
static {
try {
columnToFitMethod = TableViewSkin.class.getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
columnToFitMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public static void autoFitTable(TableView tableView) {
tableView.getItems().addListener(new ListChangeListener<Object>() {
#Override
public void onChanged(Change<?> c) {
for (Object column : tableView.getColumns()) {
try {
columnToFitMethod.invoke(tableView.getSkin(), column, -1);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
});
}
}
The current versions of JavaFX (e.g. 15-ea+1) resize table columns, if the prefWidth was never set (or is set to 80.0F, see TableColumnHeader enter link description here).
In Java 16 we can extend TableView to use a custom TableViewSkin which in turn uses a custom TableColumnHeader
class FitWidthTableView<T> extends TableView<T> {
public FitWidthTableView() {
setSkin(new FitWidthTableViewSkin<>(this));
}
public void resizeColumnsToFitContent() {
Skin<?> skin = getSkin();
if (skin instanceof FitWidthTableViewSkin<?> tvs) tvs.resizeColumnsToFitContent();
}
}
class FitWidthTableViewSkin<T> extends TableViewSkin<T> {
public FitWidthTableViewSkin(TableView<T> tableView) {
super(tableView);
}
#Override
protected TableHeaderRow createTableHeaderRow() {
return new TableHeaderRow(this) {
#Override
protected NestedTableColumnHeader createRootHeader() {
return new NestedTableColumnHeader(null) {
#Override
protected TableColumnHeader createTableColumnHeader(TableColumnBase col) {
return new FitWidthTableColumnHeader(col);
}
};
}
};
}
public void resizeColumnsToFitContent() {
for (TableColumnHeader columnHeader : getTableHeaderRow().getRootHeader().getColumnHeaders()) {
if (columnHeader instanceof FitWidthTableColumnHeader colHead) colHead.resizeColumnToFitContent(-1);
}
}
}
class FitWidthTableColumnHeader extends TableColumnHeader {
public FitWidthTableColumnHeader(TableColumnBase col) {
super(col);
}
#Override
public void resizeColumnToFitContent(int rows) {
super.resizeColumnToFitContent(-1);
}
}
You can use tableView.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
You can also try switching between the two policies TableView.CONSTRAINED_RESIZE_POLICY
and TableView.UNCONSTRAINED_RESIZE_POLICY in case TableView.UNCONSTRAINED_RESIZE_POLICY alone doesn't fit your need.
Here's a useful link.

Resources