JavaFX TableView: copy text as rendered in cell - javafx

I want to implement copy functionality in a TableView. The text to be copied should be the actual text that is rendered in the cell, not the .toString version of the data model to be rendered, that is, it should be the .getText of the cell.
There are several ways of getting the data from a cell. However to get the rendered cell text contents, the procedure seems to be like this:
Get the cell data.
Get the cell factory.
Use the factory to create a cell.
Use the cell's updateItem method to render the data, then getText to get the rendered text.
The last step is not possible due to updateItem being protected.
How can I access the rendered text of any given cell in a TableView?

The process you outline involves getting the text (i.e. data) from the view (the cell), which violates the principles behind the MVC/MVP design. From a practical perspective, it involves creating UI elements (which are expensive to create) to essentially manipulate data (which is typically much less expensive to create and process). Additionally, depending on exactly what you're doing, the UI elements may impose additional threading constraints on your code (as they are essentially single-threaded).
If you need to use the "formatting text" functionality outside of the cell, you should factor it out elsewhere and reuse it in both the "copy" functionality you need and in the cell. At a minimum, this could be done by making the "format text" functionality part of the cell factory:
import java.util.function.Function;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
public class FormattingTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private final Function<T, String> formatter ;
public FormattingTableCellFactory(Function<T, String> formatter) {
this.formatter = formatter ;
}
public FormattingTableCellFactory() {
this(T::toString);
}
public final Function<T, String> getFormatter() {
return formatter ;
}
#Override
public TableCell<S,T> call(TableColumn<S,T> col) {
return new TableCell<S,T>() {
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
setText(item == null ? null : formatter.apply(item));
}
};
}
}
(Obviously you could extend this to produce more sophisticated cells with graphical content, etc.)
And now your copy functionality can simply apply the formatter to the data, without reference to any actual cells. Here's a SSCCE:
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
private String copy(TableView<Product> table) {
StringBuilder sb = new StringBuilder();
for (Product p : table.getSelectionModel().getSelectedItems()) {
List<String> data = new ArrayList<>();
for (TableColumn<Product, ?> column : table.getColumns()) {
Function<Object, String> formatter = ((FormattingTableCellFactory) column.getCellFactory()).getFormatter();
data.add(formatter.apply(column.getCellObservableValue(p).getValue()));
}
sb.append(String.join("\t", data)).append("\n");
}
return sb.toString() ;
}
#Override
public void start(Stage primaryStage) {
TableView<Product> table = new TableView<>();
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.getColumns().add(column("Product", Product::nameProperty, String::toString));
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
table.getColumns().add(column("Price", Product::priceProperty, currencyFormat::format));
Random rng = new Random();
for (int i = 1; i <= 100; i++) {
table.getItems().add(new Product("Product "+i, rng.nextDouble()*100));
}
Button copy = new Button("Copy");
copy.setOnAction(e -> System.out.println(copy(table)));
copy.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedItems()));
BorderPane root = new BorderPane(table);
BorderPane.setAlignment(copy, Pos.CENTER);
BorderPane.setMargin(copy, new Insets(10));
root.setBottom(copy);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S,T> TableColumn<S,T> column(String title, Function<S,ObservableValue<T>> property, Function<T,String> formatter) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(new FormattingTableCellFactory<>(formatter));
return col ;
}
public static class Product {
private final StringProperty name = new SimpleStringProperty();
private final DoubleProperty price = new SimpleDoubleProperty() ;
public Product(String name, double price) {
setName(name);
setPrice(price);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final DoubleProperty priceProperty() {
return this.price;
}
public final double getPrice() {
return this.priceProperty().get();
}
public final void setPrice(final double price) {
this.priceProperty().set(price);
}
}
public static void main(String[] args) {
launch(args);
}
}
You can get rid of the less typesafe code at the expense of less flexibility:
private final Function<String, String> defaultFormatter = Function.identity() ;
private final Function<Number, String> priceFormatter = DecimalFormat.getCurrencyInstance()::format ;
private String copy(TableView<Product> table) {
return table.getSelectionModel().getSelectedItems().stream().map(product ->
String.format("%s\t%s",
defaultFormatter.apply(product.getName()),
priceFormatter.apply(product.getPrice()))
).collect(Collectors.joining("\n"));
}
and
table.getColumns().add(column("Product", Product::nameProperty, defaultFormatter));
table.getColumns().add(column("Price", Product::priceProperty, priceFormatter));

Related

JavaFX TableView: open detail information between rows on click

I'm trying to display travel connections in a TableView. So far that works like a charm. Now I'm kinda stuck trying to get details of a connection to be displayed in between table rows. This should happen on selecting a table item.
The problem is, that the details are in a different format than the connections I'm displaying. So I would need to put a panel between two table rows. Is this at all possible?
The "proper" way to do this would be to create a custom skin for TableRow and use a rowFactory on the table that returned a TableRow with the custom skin installed. However, since skin classes are not public API at the time of writing (note though that they will be in Java 9), this would mean implementing the skin class entirely from scratch (laying out the table cells, etc), which would be pretty difficult.
A less "official" approach, but one that's a little easier, is just to override the various layout methods in the TableRow directly, and hook into the superclass implementation.
This works, but feels a little fragile:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableWithCustomRow extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setRowFactory(tv -> new TableRow<Item>() {
Node detailsPane ;
{
selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
getChildren().add(detailsPane);
} else {
getChildren().remove(detailsPane);
}
this.requestLayout();
});
detailsPane = createDetailsPane(itemProperty());
}
#Override
protected double computePrefHeight(double width) {
if (isSelected()) {
return super.computePrefHeight(width)+detailsPane.prefHeight(getWidth());
} else {
return super.computePrefHeight(width);
}
}
#Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelected()) {
double width = getWidth();
double paneHeight = detailsPane.prefHeight(width);
detailsPane.resizeRelocate(0, getHeight()-paneHeight, width, paneHeight);
}
}
});
Random random = new Random();
for (int i = 1 ; i <= 100 ; i++) {
table.getItems().add(new Item("Item "+i, random.nextInt(100)));
}
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
Scene scene = new Scene(table, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Node createDetailsPane(ObjectProperty<Item> item) {
BorderPane detailsPane = new BorderPane();
Label detailsLabel = new Label();
VBox labels = new VBox(5, new Label("These are the"), detailsLabel);
labels.setAlignment(Pos.CENTER_LEFT);
labels.setPadding(new Insets(2, 2, 2, 16));
detailsPane.setCenter(labels);
Label icon = new Label("Icon");
icon.setStyle("-fx-background-color: aqua; -fx-text-fill: darkgreen; -fx-font-size:18;");
BorderPane.setMargin(icon, new Insets(6));
icon.setMinSize(40, 40);
detailsPane.setLeft(icon);
detailsPane.setStyle("-fx-background-color: -fx-background; -fx-background: skyblue;");
item.addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
detailsLabel.setText("");
} else {
detailsLabel.setText("details for "+newItem.getName());
}
});
return detailsPane ;
}
private static <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setPrefWidth(150);
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty() ;
private final IntegerProperty value = new SimpleIntegerProperty() ;
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
This gives the following:

Understanding CheckBoxTableCell changelistener using setSelectedStateCallback

I'm trying to follow: CheckBoxTableCell changelistener not working
The given code answer to that question is below and dependent on the model 'Trainee'
final CheckBoxTableCell<Trainee, Boolean> ctCell = new CheckBoxTableCell<>();
ctCell.setSelectedStateCallback(new Callback<Integer, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(Integer index) {
return table.getItems().get(index).selectedProperty();
}
});
I would like to obtain that selected property value and add a listener to it, but I don't think I'm doing it right. I attempted to add all kind of listeners to it so that I know when the checkbox in each row is changed and I can add logic to each. I presume the code above allow ctCell to now observe changes and I can just call a change listener to it and detect selection per given row.
I tried some change properties here just to detect the changes:
ctCell.selectedStateCallbackProperty().addListener(change -> {
System.out.println("1Change happened in selected state property");
});
ctCell.selectedProperty().addListener(change -> {
System.out.println("2Change happened in selected property");
});
ctCell.itemProperty().addListener(change -> {
System.out.println("3Change happened in item property");
});
ctCell.indexProperty().addListener(change -> {
System.out.println("4Change happened in index property");
});
...but none seemed to be called.
This is the shorten set up that I have:
requestedFaxCol.setCellValueFactory(new PropertyValueFactory("clientHasRequestedFax"));
requestedFaxCol.setCellFactory(CheckBoxTableCell.forTableColumn(requestedFaxCol));
final CheckBoxTableCell<ClinicClientInfo, Boolean> ctCell = new CheckBoxTableCell<>();
ctCell.setSelectedStateCallback(new Callback<Integer, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(Integer index) {
return clinicLinkTable.getItems().get(index).clientHasRequestedFaxProperty();}
});
Let me know if I need to provide a more information! What am I not understanding in terms of why I cannot bridge a change listener to my table cell check boxes? Or if someone can point out the a direction for me to try. Thanks!
UPDATE to depict the ultimate goal of this question
package testapp;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TestApp extends Application {
private TableView<ClinicClientInfo> clientTable = new TableView<>();
private TableColumn<ClinicClientInfo, String> faxCol = new TableColumn<>("Fax");
private TableColumn<ClinicClientInfo, Boolean> requestedFaxCol = new TableColumn<>("Requested Fax");
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
ObservableList<ClinicClientInfo> list = FXCollections.observableArrayList(
new ClinicClientInfo("", false),
new ClinicClientInfo("945-342-4324", true));
root.getChildren().add(clientTable);
clientTable.getColumns().addAll(faxCol, requestedFaxCol);
clientTable.setItems(list);
clientTable.setEditable(true);
clientTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
faxCol.setCellValueFactory(new PropertyValueFactory<>("clinicFax"));
faxCol.setVisible(true);
requestedFaxCol.setCellValueFactory(new PropertyValueFactory("clientHasRequestedFax"));
requestedFaxCol.setCellFactory(CheckBoxTableCell.forTableColumn(requestedFaxCol));
requestedFaxCol.setVisible(true);
requestedFaxCol.setEditable(true);
//My attempt to connect the listener
//If user selects checkbox and the fax value is empty, the alert should prompt
CheckBoxTableCell<ClinicClientInfo, Boolean> ctCell = new CheckBoxTableCell<>();
ctCell.setSelectedStateCallback(new Callback<Integer, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(Integer index) {
ObservableValue<Boolean> itemBoolean = clientTable.getItems().get(index).clientHasRequestedFaxProperty();
itemBoolean.addListener(change -> {
ClinicClientInfo item = clientTable.getItems().get(index);
if(item.getClinicFax().isEmpty() && item.getClientHasRequestedFax()){
Alert alert = new Alert(Alert.AlertType.WARNING);
alert.setTitle("Warning");
alert.show();
}
});
return itemBoolean;
}
});
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public class ClinicClientInfo {
private final StringProperty clinicFax;
private final BooleanProperty clientHasRequestedFax;
public ClinicClientInfo(String fax, boolean clientHasRequestedFax){
this.clinicFax = new SimpleStringProperty(fax);
this.clientHasRequestedFax = new SimpleBooleanProperty(clientHasRequestedFax);
}
public String getClinicFax(){
return clinicFax.get();
}
public void setClinicFax(String clinicFax){
this.clinicFax.set(clinicFax);
}
public StringProperty clinicFaxProperty(){
return clinicFax;
}
public boolean getClientHasRequestedFax(){
return clientHasRequestedFax.get();
}
public void setClientHasRequestedFax(boolean clientHasRequestedFax){
this.clientHasRequestedFax.set(clientHasRequestedFax);
}
public BooleanProperty clientHasRequestedFaxProperty(){
return clientHasRequestedFax;
}
}
}
The goal is to get a prompt when the user tries to select fax request when the fax string is empty.
This is already fully explained in the question you already linked, so I don't know what more I can add here other than just to restate it.
The check boxes in the cell are bidirectionally bound to the property that is returned by the selectedStateCallback. If no selectedStateCallback is set, and the cell is attached to a column whose cellValueFactory returns a BooleanProperty (which covers almost all use cases), then the check box's state is bidirectionally bound to that property.
In your code sample, I don't understand what ctCell is for. You just create it, set a selectedStateCallBack on it, and then don't do anything with it. It has nothing to do with your table and nothing to do with the cell factory you set.
So in your case, no selected state callback is set on the cells produced by your cell factory, and the cell value factory returns a boolean property, so the default applies, and the check box state is bidirectionally bound to the property returned by the cell value factory. All you have to do is register a listener with those properties.
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
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.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class CheckBoxTableCellTestApp extends Application {
private TableView<ClinicClientInfo> clientTable = new TableView<>();
private TableColumn<ClinicClientInfo, String> faxCol = new TableColumn<>("Fax");
private TableColumn<ClinicClientInfo, Boolean> requestedFaxCol = new TableColumn<>("Requested Fax");
#Override
public void start(Stage primaryStage) {
StackPane root = new StackPane();
ObservableList<ClinicClientInfo> list = FXCollections.observableArrayList(
new ClinicClientInfo("", false),
new ClinicClientInfo("945-342-4324", true));
// add listeners to boolean properties:
for (ClinicClientInfo clinic : list) {
clinic.clientHasRequestedFaxProperty().addListener((obs, faxWasRequested, faxIsNowRequested) ->{
System.out.printf("%s changed fax request from %s to %s %n",
clinic.getClinicFax(), faxWasRequested, faxIsNowRequested);
});
}
root.getChildren().add(clientTable);
clientTable.getColumns().addAll(faxCol, requestedFaxCol);
clientTable.setItems(list);
clientTable.setEditable(true);
clientTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
faxCol.setCellValueFactory(new PropertyValueFactory<>("clinicFax"));
faxCol.setVisible(true);
requestedFaxCol.setCellValueFactory(new PropertyValueFactory<>("clientHasRequestedFax"));
requestedFaxCol.setCellFactory(CheckBoxTableCell.forTableColumn(requestedFaxCol));
requestedFaxCol.setVisible(true);
requestedFaxCol.setEditable(true);
Scene scene = new Scene(root, 300, 250);
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
public class ClinicClientInfo {
private final StringProperty clinicFax;
private final BooleanProperty clientHasRequestedFax;
public ClinicClientInfo(String fax, boolean clientHasRequestedFax){
this.clinicFax = new SimpleStringProperty(fax);
this.clientHasRequestedFax = new SimpleBooleanProperty(clientHasRequestedFax);
}
public String getClinicFax(){
return clinicFax.get();
}
public void setClinicFax(String clinicFax){
this.clinicFax.set(clinicFax);
}
public StringProperty clinicFaxProperty(){
return clinicFax;
}
public boolean getClientHasRequestedFax(){
return clientHasRequestedFax.get();
}
public void setClientHasRequestedFax(boolean clientHasRequestedFax){
this.clientHasRequestedFax.set(clientHasRequestedFax);
}
public BooleanProperty clientHasRequestedFaxProperty(){
return clientHasRequestedFax;
}
}
}

JavaFx Bindings : is there any way to bind value to observable List?

i have table view it's contents is observable list that contains numbers and i have a text field that should display the sum of these values in the table is there any way to bind this text fields to sum of the number properties .
note : the user may edit the values in this list , may add more elements , may delete some element how can i bind the sum of these numbers correctly using javafx binding instead of doing this by the old fashion way iterate over the list and sum the numbers manually and every change reiterate over it again .
An ObservableList will fire update events if (and only if) you create the list with an extractor. The extractor is a function that maps each element of the list to an array of Observables; if any of those Observables change their value, the list fires the appropriate update events and becomes invalid.
So the two steps here are:
Create the list with an extractor
Create a binding that computes the total whenever the list is invalidated.
So if you have a model class for your table such as:
public class Item {
private final IntegerProperty value = new SimpleIntegerProperty();
public IntegerProperty valueProperty() {
return value ;
}
public final int getValue() {
return valueProperty().get();
}
public final void setValue(int value) {
valueProperty().set(value);
}
// other properties, etc...
}
Then you create the table with:
TableView<Item> table = new TableView<>();
table.setItems(FXCollections.observableArrayList(item ->
new Observable[] { item.valueProperty() }));
Now you can create the binding with
IntegerBinding total = Bindings.createIntegerBinding(() ->
table.getItems().stream().collect(Collectors.summingInt(Item::getValue)),
table.getItems());
Implementation note: the two arguments to createIntegerBinding above are a function that computes the int value, and any values to observe. If any of the observed values (here there is just one, table.getItems()) is invalidated, then the value is recomputed. Remember we created table.getItems() so it would be invalidated if any of the item's valueProperty()s changed. The function that is the first argument uses a lambda expression and the Java 8 Streams API, it is roughly equivalent to
() -> {
int totalValue = 0 ;
for (Item item : table.getItems()) {
totalValue = totalValue + item.getValue();
}
return totalValue ;
}
Finally, if you want a label to display the total, you can do something like
Label totalLabel = new Label();
totalLabel.textProperty().bind(Bindings.format("Total: %d", total));
Here is an SSCCE:
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
import javafx.util.converter.NumberStringConverter;
public class TotallingTableView extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setEditable(true);
table.getColumns().add(
column("Item", Item::nameProperty, new DefaultStringConverter()));
table.getColumns().add(
column("Value", Item::valueProperty, new NumberStringConverter()));
table.setItems(FXCollections.observableArrayList(
item -> new Observable[] {item.valueProperty() }));
IntStream.rangeClosed(1, 20)
.mapToObj(i -> new Item("Item "+i, i))
.forEach(table.getItems()::add);
IntegerBinding total = Bindings.createIntegerBinding(() ->
table.getItems().stream().collect(Collectors.summingInt(Item::getValue)),
table.getItems());
Label totalLabel = new Label();
totalLabel.textProperty().bind(Bindings.format("Total: %d", total));
Button add = new Button("Add item");
add.setOnAction(e ->
table.getItems().add(new Item("New Item", table.getItems().size() + 1)));
Button remove = new Button("Remove");
remove.disableProperty().bind(
Bindings.isEmpty(table.getSelectionModel().getSelectedItems()));
remove.setOnAction(e ->
table.getItems().remove(table.getSelectionModel().getSelectedItem()));
HBox buttons = new HBox(5, add, remove);
buttons.setAlignment(Pos.CENTER);
VBox controls = new VBox(5, totalLabel, buttons);
VBox.setVgrow(totalLabel, Priority.ALWAYS);
totalLabel.setMaxWidth(Double.MAX_VALUE);
totalLabel.setAlignment(Pos.CENTER_RIGHT);
BorderPane root = new BorderPane(table, null, null, controls, null);
Scene scene = new Scene(root, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> column(String title,
Function<S, ObservableValue<T>> property, StringConverter<T> converter) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(TextFieldTableCell.forTableColumn(converter));
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Note that this is not the most efficient possible implementation, but the one that (IMHO) keeps the code the cleanest. If you have a very large number of items in the table, recomputing the total from scratch by iterating through and summing them all might be prohibitively expensive. The alternative approach is to listen for add/remove changes to the list. When an item is added, add its value to the total, and register a listener with the value property that updates the total if the value changes. When an item is removed from the list, remove the listener from the value property and subtract the value from the total. This avoids continually recomputing from scratch, but the code is harder to decipher.
I would just add a listener to the ObservableList and have it update the label any time there is a change.
list.addListener((obs, oldValue, newValue) -> {
textfield.setText(list.stream().mapToInt(x -> x.intValue()).sum();
});
Something like that should work

Make individual cell editable in JavaFX tableview

I am writing a program that displays a JavaFX table. I understand how to make all the data in a specific column editable via "Column.setCellFactory(TextFieldTableCell.forTableColumn());"
However I would like to make some of the cells editable and others immutable. Is this possible? Moreover, I would like editable cells to either have a border or have a unique font color.
Yes, this is possible: the TableCell has an editable property inherited from the Cell class. You need to arrange that the cell sets its editable property accordingly when the item changes (and possibly if the condition governing when it should be editable changes).
In the example below, I create a default cell factory using TextFieldTableCell.forTableColumn(), and then create another cell factory. The custom cell factory invokes the default cell factory (to get the standard TextField behavior), then observes the itemProperty of the cell and updates the editableProperty accordingly (in this simple example, only cells with an even value are editable).
To add the border, you need to update the style somehow. The best way to do this is to define a pseudoclass for "editable" and use an external style sheet to manage the style for editable cells.
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ConditionallyEditableTableCell extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setEditable(true);
TableColumn<Item, String> nameCol = createCol("Name", Item::nameProperty);
TableColumn<Item, Number> canEditCol = createCol("Value", Item::valueProperty);
PseudoClass editableCssClass = PseudoClass.getPseudoClass("editable");
Callback<TableColumn<Item, String>, TableCell<Item, String>> defaultTextFieldCellFactory
= TextFieldTableCell.<Item>forTableColumn();
nameCol.setCellFactory(col -> {
TableCell<Item, String> cell = defaultTextFieldCellFactory.call(col);
cell.itemProperty().addListener((obs, oldValue, newValue) -> {
TableRow row = cell.getTableRow();
if (row == null) {
cell.setEditable(false);
} else {
Item item = (Item) cell.getTableRow().getItem();
if (item == null) {
cell.setEditable(false);
} else {
cell.setEditable(item.getValue() % 2 == 0);
}
}
cell.pseudoClassStateChanged(editableCssClass, cell.isEditable());
});
return cell ;
});
table.getColumns().addAll(canEditCol, nameCol);
for (int i=1; i<=20; i++) {
table.getItems().add(new Item("Item "+i, i));
}
Scene scene = new Scene(new BorderPane(table), 600, 400);
scene.getStylesheets().add("conditionally-editable-table-cell.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> createCol(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Item {
private final IntegerProperty value = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
And the stylesheet, conditionally-editable-table-cell.css:
.table-cell:editable {
-fx-border-color: red ;
}

Populate TableView with ObservableMap JavaFX

I wanted to know if it is possible to use a ObservableMap to populate a TableView ?
I use ObservableMap instead of ObservableList because I need to add and delete often, so I need to minimize the cost.
My hashMap use an BigInteger as key field and a type with many properties as value field.
In my tableView I just want to display the values with a column per properties. I hope that is clear
Thanks
I've been trying to do this. I guess the post is old but I don't see any answers anywhere on the net. The examples use the map key for columns and then a list of maps for every row. I'd like to see the rows as keys and their associated values. It's a long example.
package tablemap;
import static java.lang.Math.random;
import java.util.Map;
import java.util.TreeMap;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableMap extends Application {
#Override
public void start(Stage primaryStage) {
VBox root = new VBox();
Map<String,LineItem> mapData = new TreeMap<>();
for (int i = 0; i < 3; i++)
mapData.put(String.valueOf(random()), new LineItem(String.valueOf(i),"i"));
ObservableList<Map.Entry<String,LineItem>> listData =
FXCollections.observableArrayList(mapData.entrySet());
TableView<Map.Entry<String,LineItem>> tv = new TableView(listData);
TableColumn<Map.Entry<String,LineItem>,String> keyCol = new TableColumn("Key");
keyCol.setCellValueFactory(
(TableColumn.CellDataFeatures<Map.Entry<String,LineItem>, String> p) ->
new SimpleStringProperty(p.getValue().getKey()));
TableColumn<Map.Entry<String,LineItem>,String> lineNoCol = new TableColumn("Line No");
lineNoCol.setCellValueFactory(
(TableColumn.CellDataFeatures<Map.Entry<String,LineItem>, String> p) ->
new SimpleStringProperty(p.getValue().getValue().getLineNo()));
TableColumn<Map.Entry<String,LineItem>,String> descCol = new TableColumn("Desc");
descCol.setCellValueFactory(
(TableColumn.CellDataFeatures<Map.Entry<String,LineItem>, String> p) ->
new SimpleStringProperty(p.getValue().getValue().getDesc()));
descCol.setCellFactory(TextFieldTableCell.forTableColumn());
descCol.setOnEditCommit((CellEditEvent<Map.Entry<String,LineItem>, String> t) -> {
t.getTableView().getItems().get(t.getTablePosition().getRow())
.getValue().setDesc(t.getNewValue());
});
tv.getColumns().addAll(keyCol,lineNoCol, descCol);
tv.setEditable(true);
tv.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
Button btnOut = new Button("out");
btnOut.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent t) {
for (Map.Entry<String,LineItem> me : mapData.entrySet()){
System.out.println("key "+me.getKey()+" entry "+me.getValue().toCSVString());
}
for (Map.Entry<String,LineItem> me : listData){
System.out.println("key "+me.getKey()+" entry "+me.getValue().toCSVString());
}
}
});
root.getChildren().addAll(tv,btnOut);
Scene scene = new Scene(root, 300, 200);
primaryStage.setTitle("Map Table Test");
primaryStage.setScene(scene);
primaryStage.show();
}
}
And the LineItem Class Code
package tablemap;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
/* LineItem class */
public class LineItem {
private final StringProperty lineNo = new SimpleStringProperty();
private final StringProperty desc = new SimpleStringProperty();
public LineItem(String ln, String dsc) {
lineNo.set(ln); desc.set(dsc);
}
public String getLineNo() {return (lineNo.getValue() != null) ?lineNo.get():"";}
public void setLineNo(String lineNo) {this.lineNo.set(lineNo);}
public StringProperty lineNoProperty() {return lineNo;}
public String getDesc() {return (desc.getValue() != null) ?desc.get():"";}
public void setDesc(String desc) {this.desc.set(desc);}
public StringProperty descProperty() {return desc;}
public String toCSVString(){
return lineNo.getValueSafe()+","+
desc.getValueSafe()+"\n";
}
}
You can see after editing data and clicking out that changes in the list are reflected in the map. I still have to check the other way and handle insertions and deletions but that shouldn't be to hard.
I packaged up my Map Table listeners in a subclass of TableView.
package tablemap;
import java.util.AbstractMap;
import java.util.Map;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.MapChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.scene.control.TableView;
public class MapTableView<K,V> extends TableView<Map.Entry<K,V>>{
private final ObservableList<Map.Entry<K,V>> obsList;
private final ObservableMap<K,V> map;
private final MapChangeListener<K,V> mapChange;
private final ListChangeListener<Map.Entry<K,V>> listChange;
public MapTableView(ObservableMap<K,V> map) {
this.map = map;
obsList = FXCollections.observableArrayList(map.entrySet());
setItems(obsList);
mapChange = new MapChangeListener<K, V>() {
#Override
public void onChanged(MapChangeListener.Change<? extends K, ? extends V> change) {
obsList.removeListener(listChange);
if (change.wasAdded())
obsList.add(new AbstractMap.SimpleEntry(change.getKey(),change.getValueAdded()));
if (change.wasRemoved()){
//obsList.remove(new AbstractMap.SimpleEntry(change.getKey(),change.getValueRemoved()));
// ^ doesn't work always, use loop instead
for (Map.Entry<K,V> me : obsList){
if (me.getKey().equals(change.getKey())){
obsList.remove(me);
break;
}
}
}
obsList.addListener(listChange);
}
};
listChange = (ListChangeListener.Change<? extends Map.Entry<K, V>> change) -> {
map.removeListener(mapChange);
while (change.next()){
//maybe check for uniqueness here
if (change.wasAdded()) for (Map.Entry<K, V> me: change.getAddedSubList())
map.put(me.getKey(),me.getValue());
if (change.wasRemoved()) for (Map.Entry<K, V> me: change.getRemoved())
map.remove(me.getKey());
}
map.addListener(mapChange);
};
map.addListener(mapChange);
obsList.addListener(listChange);
}
//adding to list should be unique
public void addUnique(K key, V value){
boolean isFound = false;
//if a duplicate key just change the value
for (Map.Entry<K,V> me : getItems()){
if (me.getKey().equals(key)){
isFound = true;
me.setValue(value);
break;//only first match
}
}
if (!isFound) // add new entry
getItems().add(new AbstractMap.SimpleEntry<>(key,value));
}
//for doing lenghty map operations
public void removeMapListener(){
map.removeListener(mapChange);
}
//for resyncing list to map after many changes
public void resetMapListener(){
obsList.removeListener(listChange);
obsList.clear();
obsList.addAll(map.entrySet());
obsList.addListener(listChange);
map.addListener(mapChange);
}
}
It seems to work so far. I create with the following code :
final ObservableMap<String, LineItem> obsMap = FXCollections.observableHashMap();
final MapTableView<String,LineItem> mtv = new MapTableView(obsMap);
You can even edit the keys.
final TableColumn<Map.Entry<String,LineItem>,String> keyCol = new TableColumn("Key");
keyCol.setCellValueFactory(
(TableColumn.CellDataFeatures<Map.Entry<String,LineItem>, String> p) ->
new SimpleStringProperty(p.getValue().getKey()));
keyCol.setCellFactory(TextFieldTableCell.forTableColumn());
keyCol.setOnEditCommit((CellEditEvent<Map.Entry<String,LineItem>, String> t) -> {
final String oldKey = t.getOldValue();
final LineItem oldLineItem = obsMap.get(oldKey);
obsMap.remove(oldKey);//should remove from list but maybe doesn't always
obsMap.put(t.getNewValue(),oldLineItem);
});
You can see I added a method to remove and re add the map listeners. To add and remove 100k entries takes .65 secs w/out listeners and 5.2 secs with them.
Here's the whole thing in one file on pastebin. http://pastebin.com/NmdTURFt

Resources