3

I have TableView which is used for displaying data made from CostDataRow objects. The most of cells in table view are editable and holds integer data so here is excerpt how I initialize the table and one of its editable numeric columns:

// `data` is `ObservableList<CostDataRow>` made by 
// using `FXCollections.<CostDataRow>observableArrayList(...)`.
FilteredList<CostDataRow> filteredData = new FilteredList<>(data, n -> true);
table.setEditable(true);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.getSelectionModel().setCellSelectionEnabled(true);
table.setItems(filteredData);
// ...
// Below is "wireWeight" column (Integer data)
wireWeightTCol.setCellValueFactory(new PropertyValueFactory<>("wireWeight"));
wireWeightTCol.setCellFactory(TextFieldTableCell.<CostDataRow, Integer>forTableColumn(new StringConverter<Integer>() {
    @Override
    public String toString(final Integer value) {
        return value.toString() + " kg";
    }
    @Override
    public Integer fromString(final String s) {
        return Integer.parseInt(s.replace(" kg", ""));
    }
}));
wireWeightTCol.setOnEditCommit(
    new EventHandler<CellEditEvent<CostDataRow, Integer>>() {
        @Override
        public void handle(CellEditEvent<CostDataRow, Integer> t) {
            CostDataRow row = (CostDataRow) t.getTableView().getItems().get(t.getTablePosition().getRow());
            int weight = t.getNewValue();
            row.setWireWeight(t.getNewValue());
            // Calculate wire price
            row.setWirePrice((int)(weight * getWirePriceConstant()));
            // Refresh table view
            t.getTableView().refresh();
        }
    }
);

Now I need to set background color on selected cells after user clicked on one toolbar's button - here is the action's handler:

@FXML
private void handleYellowButtonAction(ActionEvent event) {
    table.getSelectionModel().getSelectedCells().forEach(pos -> {
        int row = pos.getRow();
        int col = pos.getColumn();
        // TODO Now I need to set background color of cell with 
        //      given row and column index.
        // ...
    });
}

Colors should be saved/loaded from same data model as data self.

Thanks for help!

Ondřej Doněk
  • 529
  • 1
  • 10
  • 17
  • I know one way - I can store row/column indexes and update cell renderers to work with it so after table.refresh() I have colored cells but it seems to mě as a wrong way how to do it... – Ondřej Doněk Dec 28 '16 at 13:44
  • 1
    If I understand your question correctly, it is indeed the wrong way to do it. From what I understand, you want the user to be able to select a collection of cells in the table, then press a button, and those cells will then change their background color. Subsequently you might change the selection, but those cells will still have the new background color set. Is that correct? If so, then what should be happening is that the user changes the *state* of the model for the selected items. The table cell should observe the model and set its background color accordingly. – James_D Dec 28 '16 at 14:35
  • Yes, you understand corretly - I will store background colors in model. – Ondřej Doněk Dec 28 '16 at 14:53
  • I would not store colors in the model: that violates separation of data and view. I would store some data type in the model on which the color depends: probably a `BooleanProperty` representing whatever state it is that determines whether they should be highlighted, and then observe that in the cell factory. – James_D Dec 28 '16 at 15:24
  • Please, se my updated question - there is more code... but in fact with that `BooleanProperty` - I should have three states on each cell - red background, yellow background, no background. So I think I will use IntegerProperty where 0 is no color, 1 is yellow and so on... BUT STILL - I need to find best way how to render that background. – Ondřej Doněk Dec 28 '16 at 15:27
  • I can't really answer that in any more detail without more understanding of what you are doing. What is the purpose of this functionality from the perspective of your data model? – James_D Dec 28 '16 at 15:31
  • Data model holds costs of some building projects - colors are attached directly to values because of some evaluating process in the company which ordered this work from me. So in one row I can have same data as in second but with different colors - that is why colors are part of the data model. But I think it is not important - what I need is know how to set these color in table cells. – Ondřej Doněk Dec 28 '16 at 15:36
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/131725/discussion-between-ondrej-donk-and-james-d). – Ondřej Doněk Dec 28 '16 at 15:42
  • @James_D please, look at the chat again. – Ondřej Doněk Dec 28 '16 at 17:30
  • For all who are courious how is it finished see the discussion in chat (link is few comments above) ... I really thanks to @James_D because he shows me how to do it and was really patient with me - THANKS James_D!!! – Ondřej Doněk Dec 28 '16 at 17:57
  • I summarized the techniques discussed in the chat in an answer below. – James_D Dec 28 '16 at 17:58

4 Answers4

6

In my experience/opinion, the trick to anything like this is properly representing the data you need in the model. In this case, it looks like you need some (or perhaps all) of the properties in the table model to carry an associated attribute. I would create a specific class for those properties that carry the attribute. For example, if you are wanting the user to be able to verify values in the table, you basically want each cell in the table to show both the value and the current "verification state" of that value. Hence you need a class that encapsulates a value and a verification state, which should be observable. So you would do something like:

public class VerifiableValue<T> {

    public enum VerificationState { UNVERIFIED, VERIFIED, ERROR }

    private final T value ;
    private final ObjectProperty<VerificationState> verificationState = new SimpleObjectProperty<>(VerificationState.UNVERIFIED) ;

    public VerifiableValue(T value) {
        this.value = value ;
    }

    public VerifiableValue(T value, VerificationState verificationState) {
        this(value);
        setVerificationState(verificationState);
    }



    public T getValue() {
        return value ;
    }

    public final ObjectProperty<VerificationState> verificationStateProperty() {
        return this.verificationState;
    }


    public final VerifiableValue.VerificationState getVerificationState() {
        return this.verificationStateProperty().get();
    }


    public final void setVerificationState(
            final VerifiableValue.VerificationState verificationState) {
        this.verificationStateProperty().set(verificationState);
    }


}

Now create table cells that observe the verification state of the current item in the table. So for example, given a TableColumn<Product, VerifiableValue<Double>> weightColumn, you might do:

weightColumn.setCellFactory(tc -> {
    TextFieldTableCell<Product, VerifiableValue<Double>> cell = new TextFieldTableCell<>();

    cell.setConverter(new StringConverter<VerifiableValue<Double>>() {

        @Override
        public String toString(VerifiableValue<Double> item) {
            return item == null ? "" : String.format("%.3f Kg", item.getValue());
        }

        @Override
        public VerifiableValue<T> fromString(String string) {
            T value = new Double(string.replace("Kg","").trim());
            VerifiableValue.VerificationState verificationState = cell.getItem() == null ? VerifiableValue.VerificationState.UNVERIFIED : cell.getItem().getVerificationState() ;
            return new VerifiableValue<>(value, verificationState);
        }

    });

    ChangeListener<VerifiableValue.VerificationState> verifiedListener = (obs, oldState, newState) -> {
        if (newState == null || newState == VerifiableValue.VerificationState.UNVERIFIED) {
            cell.setStyle("");
        } else if (newState == VerifiableValue.VerificationState.VERIFIED) {
            cell.setStyle("-fx-background-color: yellow ;");
        } else if (newState == VerifiableValue.VerificationState.ERROR) {
            cell.setStyle("-fx-background-color: red ;");
        }
    };


    cell.itemProperty().addListener((obs, oldItem, newItem) -> {
        if (oldItem != null) {
            oldItem.verificationStateProperty().removeListener(verifiedListener);
        }
        if (newItem == null) {
            cell.setStyle("");
        } else {
            if (newItem.getVerificationState() == null || newItem.getVerificationState() == VerifiableValue.VerificationState.UNVERIFIED) {
                cell.setStyle("");
            } else if (newItem.getVerificationState() == VerifiableValue.VerificationState.VERIFIED) {
                cell.setStyle("-fx-background-color: yellow ;");
            } else if (newItem.getVerificationState() == VerifiableValue.VerificationState.ERROR) {
                cell.setStyle("-fx-background-color: red ;");
            }               
            newItem.verificationStateProperty().addListener(verifiedListener);
        }
    });

    return cell ;
});

Here's a SSCCE. I moved the most important parts to the top of the code (so things are in an unusual order), and move the creation of the table cell to a method to reduce repetitive code. In real life I would probably roll my own table cell for this, so the labels display "Kg" but they don't appear in the text field, and use a text formatter on the text field to prevent invalid input. I would also move the style out of the cell implementation code, and use CSS PseudoClasses to represent the "view state" of the cell, and an external style sheet to actually map those states to colors.

import java.util.Random;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
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.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class CellHighlightingTable extends Application {

    public static class VerifiableValue<T> {

        public enum VerificationState { UNVERIFIED, VERIFIED, ERROR }

        private final T value ;
        private final ObjectProperty<VerificationState> verificationState = new SimpleObjectProperty<>(VerificationState.UNVERIFIED) ;

        public VerifiableValue(T value) {
            this.value = value ;
        }

        public VerifiableValue(T value, VerificationState verificationState) {
            this(value);
            setVerificationState(verificationState);
        }



        public T getValue() {
            return value ;
        }

        public final ObjectProperty<VerificationState> verificationStateProperty() {
            return this.verificationState;
        }


        public final CellHighlightingTable.VerifiableValue.VerificationState getVerificationState() {
            return this.verificationStateProperty().get();
        }


        public final void setVerificationState(
                final CellHighlightingTable.VerifiableValue.VerificationState verificationState) {
            this.verificationStateProperty().set(verificationState);
        }


    }

    private <T> TableCell<Product, VerifiableValue<T>> createTableCell(String format, Function<String, T> supplier) {
        TextFieldTableCell<Product, VerifiableValue<T>> cell = new TextFieldTableCell<>();

        cell.setConverter(new StringConverter<VerifiableValue<T>>() {

            @Override
            public String toString(VerifiableValue<T> item) {
                return item == null ? "" : String.format(format, item.getValue());
            }

            @Override
            public VerifiableValue<T> fromString(String string) {
                T value = supplier.apply(string);
                VerifiableValue.VerificationState verificationState = cell.getItem() == null ? VerifiableValue.VerificationState.UNVERIFIED : cell.getItem().getVerificationState() ;
                return new VerifiableValue<>(value, verificationState);
            }

        });

        ChangeListener<VerifiableValue.VerificationState> verifiedListener = (obs, oldState, newState) -> {
            if (newState == null || newState == VerifiableValue.VerificationState.UNVERIFIED) {
                cell.setStyle("");
            } else if (newState == VerifiableValue.VerificationState.VERIFIED) {
                cell.setStyle("-fx-background-color: yellow ;");
            } else if (newState == VerifiableValue.VerificationState.ERROR) {
                cell.setStyle("-fx-background-color: red ;");
            }
        };


        cell.itemProperty().addListener((obs, oldItem, newItem) -> {
            if (oldItem != null) {
                oldItem.verificationStateProperty().removeListener(verifiedListener);
            }
            if (newItem == null) {
                cell.setStyle("");
            } else {
                if (newItem.getVerificationState() == null || newItem.getVerificationState() == VerifiableValue.VerificationState.UNVERIFIED) {
                    cell.setStyle("");
                } else if (newItem.getVerificationState() == VerifiableValue.VerificationState.VERIFIED) {
                    cell.setStyle("-fx-background-color: yellow ;");
                } else if (newItem.getVerificationState() == VerifiableValue.VerificationState.ERROR) {
                    cell.setStyle("-fx-background-color: red ;");
                }               
                newItem.verificationStateProperty().addListener(verifiedListener);
            }
        });

        return cell ;
    }

    @Override
    public void start(Stage primaryStage) {
        TableView<Product> table = new TableView<>();
        table.getSelectionModel().setCellSelectionEnabled(true);
        table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        table.setEditable(true);

        TableColumn<Product, String> productCol = new TableColumn<>("Product");
        productCol.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getName()));

        TableColumn<Product, VerifiableValue<Integer>> quantityColumn = new TableColumn<>("Quantity");
        quantityColumn.setCellValueFactory(cellData -> cellData.getValue().quantityProperty());

        quantityColumn.setCellFactory(tc -> createTableCell("%,d", Integer::new));

        TableColumn<Product, VerifiableValue<Double>> weightColumn = new TableColumn<>("Weight");
        weightColumn.setCellValueFactory(cellData -> cellData.getValue().weightProperty());

        weightColumn.setCellFactory(tc -> createTableCell("%.3f Kg", Double::new));

        table.getColumns().add(productCol);
        table.getColumns().add(quantityColumn);
        table.getColumns().add(weightColumn);

        Button verifySelected = new Button("Verify Selected");
        verifySelected.setOnAction(e -> {
            for (TablePosition<?, ?> pos : table.getSelectionModel().getSelectedCells()) {
                if (pos.getTableColumn() == quantityColumn) {
                    Product product = table.getItems().get(pos.getRow());
                    product.getQuantity().setVerificationState(VerifiableValue.VerificationState.VERIFIED);
                } else if (pos.getTableColumn() == weightColumn) {
                    Product product = table.getItems().get(pos.getRow());
                    product.getWeight().setVerificationState(VerifiableValue.VerificationState.VERIFIED);
                }
            }
        });
        verifySelected.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedCells()));

        Button errorSelected = new Button("Mark all selected as error");
        errorSelected.setOnAction(e -> {
            for (TablePosition<?, ?> pos : table.getSelectionModel().getSelectedCells()) {
                if (pos.getTableColumn() == quantityColumn) {
                    Product product = table.getItems().get(pos.getRow());
                    product.getQuantity().setVerificationState(VerifiableValue.VerificationState.ERROR);
                } else if (pos.getTableColumn() == weightColumn) {
                    Product product = table.getItems().get(pos.getRow());
                    product.getWeight().setVerificationState(VerifiableValue.VerificationState.ERROR);
                }
            }
        });
        errorSelected.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedCells()));


        Button unverifyAll = new Button("Remove all verification");
        unverifyAll.setOnAction(e -> {
            for (Product product : table.getItems()) {
                product.getQuantity().setVerificationState(VerifiableValue.VerificationState.UNVERIFIED);
                product.getWeight().setVerificationState(VerifiableValue.VerificationState.UNVERIFIED);
            }
        });

        HBox buttons = new HBox(5, verifySelected, errorSelected, unverifyAll);
        buttons.setAlignment(Pos.CENTER);
        buttons.setPadding(new Insets(5));

        // random data:
        Random rng = new Random();
        for (int i = 0 ;  i < 100; i++) {
            Product product = new Product("Item "+(i+1));
            product.setQuantity(new VerifiableValue<>(rng.nextInt(1200)));
            product.setWeight(new VerifiableValue<>(rng.nextDouble() * 1000));
            table.getItems().add(product);
        }

        BorderPane root = new BorderPane(table);
        root.setBottom(buttons);

        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }




    public static class Product {

        private ObjectProperty<VerifiableValue<Double>> weight = new SimpleObjectProperty<>();
        private ObjectProperty<VerifiableValue<Integer>> quantity = new SimpleObjectProperty<>();

        private final String name ;

        public Product(String name) {
            this.name = name ;
        }

        public String getName() {
            return name ;
        }

        public final ObjectProperty<VerifiableValue<Double>> weightProperty() {
            return this.weight;
        }

        public final CellHighlightingTable.VerifiableValue<java.lang.Double> getWeight() {
            return this.weightProperty().get();
        }

        public final void setWeight(final CellHighlightingTable.VerifiableValue<java.lang.Double> weight) {
            this.weightProperty().set(weight);
        }

        public final ObjectProperty<VerifiableValue<Integer>> quantityProperty() {
            return this.quantity;
        }

        public final CellHighlightingTable.VerifiableValue<java.lang.Integer> getQuantity() {
            return this.quantityProperty().get();
        }

        public final void setQuantity(final CellHighlightingTable.VerifiableValue<java.lang.Integer> quantity) {
            this.quantityProperty().set(quantity);
        }


    }

    public static void main(String[] args) {
        launch(args);
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
1

If you have a css, you can use this code:

.table-row-cell:selected {
   -fx-background-color: yellow;
}
user7291698
  • 1,972
  • 2
  • 15
  • 30
1

There is no way to retrieve a cell instance from a TableView. A solution to the problem though is using a custom TableCell class, and program it to change based on the conditions you specify.

Note howeever that a Cell instance may be used for multiple Items (e.g. they are reused to enhance performance).

When extending the TableCell class, the most important method to override is the updateItem() method. The following is an example of how to change style based on a condition.

   @Override
    protected void updateItem(LocalDate item, boolean empty) {
        super.updateItem(item, empty);

        if (item == null || empty) {
            setText(null);
            setStyle(""); // set cell style
        } else {
            if (checkCondition) {
                setTextFill(Color.CHOCOLATE);
                setStyle("-fx-background-color: red;");// set your css style here if condition is true
            } else {
                setTextFill(Color.BLACK);
                setStyle(""); // reser style if condition is false.
            }
        }
    }
Lesleyvdp
  • 313
  • 2
  • 14
n247s
  • 1,898
  • 1
  • 12
  • 30
  • 1
    Thanks, I was worry that this is the only way. Thanks. – Ondřej Doněk Dec 28 '16 at 14:15
  • This assumes that you know that `updateItem` will be invoked any time the data referenced by the condition changes, which may or may not be the case depending on the exact use case. – James_D Dec 28 '16 at 14:36
  • And what I should use instead? I'm already using `tcol.setCellFactory` for printing numbers as I want (so I have there `fromString`/`toString` methods)... I'm also using `TextFieldTableCell` for editing data in table view... – Ondřej Doněk Dec 28 '16 at 14:57
  • Well it seems I can not use `updateItem` because of using `TextFieldTableCell`... – Ondřej Doněk Dec 28 '16 at 15:11
  • if you want to use `TextFieldTableCell`, extends that class instead of `TableCell`. – n247s Dec 28 '16 at 15:43
0

I browsed for a bit and found:

setStyle("-fx-background-color: x");

Is this what you are trying to do?

Lesleyvdp
  • 313
  • 2
  • 14
  • Yes, but I have [TablePosition](https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/TablePosition.html) on which I need to set that style - and I don't know how to get cell object from that table position. – Ondřej Doněk Dec 28 '16 at 13:24
  • I can not just write setStyle - I need to have cell object to apply style on it... I hope you understand me. – Ondřej Doněk Dec 28 '16 at 13:25
  • Can you store the selected items, and then set the Style of all these selected items such as: selectedItems = taview.getSelectionModel().getSelectedItems(); – Lesleyvdp Dec 28 '16 at 13:32
  • Are you able to store it in a variable like the example above, and apply the setStyle() to that? – Lesleyvdp Dec 28 '16 at 13:43
  • Any example? And please watch my new comment on original question. – Ondřej Doněk Dec 28 '16 at 13:45
  • Instead of using the table.getSelectionModel.getSelectedCells() etc. Store it in an object such as ObservableList list = table.getSelectionModel.getSelectedCells(). Then try applying the setStyle() to the object. – Lesleyvdp Dec 28 '16 at 13:50
  • But I will try it within an hour - I just go to the forest with my dog on walk to clear my head. – Ondřej Doněk Dec 28 '16 at 13:58