Programmatically change the TableView row appearance

How about creating a row factory which exposes an observable list of the indexes of table rows which are to be highlighted? That way you can simply update the list with the indexes you need to highlight: for example by calling the getSelectedIndices() on the selection model and passing it to the list's setAll(...) method.

This could look something like:

import java.util.Collections;

import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.util.Callback;


public class StyleChangingRowFactory<T> implements
        Callback<TableView<T>, TableRow<T>> {

    private final String styleClass ;
    private final ObservableList<Integer> styledRowIndices ;
    private final Callback<TableView<T>, TableRow<T>> baseFactory ;

    public StyleChangingRowFactory(String styleClass, Callback<TableView<T>, TableRow<T>> baseFactory) {
        this.styleClass = styleClass ;
        this.baseFactory = baseFactory ;
        this.styledRowIndices = FXCollections.observableArrayList();
    }

    public StyleChangingRowFactory(String styleClass) {
        this(styleClass, null);
    }

    @Override
    public TableRow<T> call(TableView<T> tableView) {

        final TableRow<T> row ;
        if (baseFactory == null) {
            row = new TableRow<>();
        } else {
            row = baseFactory.call(tableView);
        }

        row.indexProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> obs,
                    Number oldValue, Number newValue) {
                updateStyleClass(row);
            }
        });

        styledRowIndices.addListener(new ListChangeListener<Integer>() {

            @Override
            public void onChanged(Change<? extends Integer> change) {
                updateStyleClass(row);
            }
        });

        return row;
    }

    public ObservableList<Integer> getStyledRowIndices() {
        return styledRowIndices ;
    }

    private void updateStyleClass(TableRow<T> row) {
        final ObservableList<String> rowStyleClasses = row.getStyleClass();
        if (styledRowIndices.contains(row.getIndex()) ) {
            if (! rowStyleClasses.contains(styleClass)) {
                rowStyleClasses.add(styleClass);
            }
        } else {
            // remove all occurrences of styleClass:
            rowStyleClasses.removeAll(Collections.singleton(styleClass));
        }
    }

}

Now you can do

final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<>("highlightedRow");
table.setRowFactory(rowFactory);

And in your button's action handler do

    rowFactory.getStyledRowIndices().setAll(table.getSelectionModel().getSelectedIndices());

Because StyleChangingRowFactory wraps another row factory, you can still use it if you already have a custom row factory implementation you want to use. For example:

final StyleChangingRowFactory<Person> rowFactory = new StyleChangingRowFactory<Person>(
        "highlightedRow",
        new Callback<TableView<Person>, TableRow<Person>>() {

            @Override
            public TableRow<Person> call(TableView<Person> tableView) {
                final TableRow<Person> row = new TableRow<Person>();
                ContextMenu menu = new ContextMenu();
                MenuItem removeMenuItem = new MenuItem("Remove");
                removeMenuItem.setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent event) {
                        table.getItems().remove(row.getItem());
                    }
                });
                menu.getItems().add(removeMenuItem);
                row.contextMenuProperty().bind(
                        Bindings.when(row.emptyProperty())
                                .then((ContextMenu) null)
                                .otherwise(menu));
                return row;
            }

        });
table.setRowFactory(rowFactory);

Here is a complete code example.


Edit: Updated version of this (old) post is at https://stackoverflow.com/a/73764770/2189127

If you don't want the reusability of the solution I posted here, this is really the same thing but using an anonymous inner class for the row factory instead of a standalone class. Perhaps the code is easier to follow as it's all in one place. It's kind of a hybrid between Jonathan's solution and mine, but will automatically update the highlights without forcing it with a sort.

I used a list of integers so it supports multiple selection, but if you don't need that you could obviously just use an IntegerProperty instead.

Here's the row factory:

    final ObservableList<Integer> highlightRows = FXCollections.observableArrayList();
    
    table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
        @Override
        public TableRow<Person> call(TableView<Person> tableView) {
            final TableRow<Person> row = new TableRow<Person>() {
                @Override
                protected void updateItem(Person person, boolean empty){
                    super.updateItem(person, empty);
                    if (highlightRows.contains(getIndex())) {
                        if (! getStyleClass().contains("highlightedRow")) {
                            getStyleClass().add("highlightedRow");
                        }
                    } else {
                        getStyleClass().removeAll(Collections.singleton("highlightedRow"));
                    }
                }
            };
            highlightRows.addListener(new ListChangeListener<Integer>() {
                @Override
                public void onChanged(Change<? extends Integer> change) {
                    if (highlightRows.contains(row.getIndex())) {
                        if (! row.getStyleClass().contains("highlightedRow")) {
                            row.getStyleClass().add("highlightedRow");
                        }
                    } else {
                        row.getStyleClass().removeAll(Collections.singleton("highlightedRow"));
                    }
                }
            });
            return row;
        }
    });

And here are what some buttons might look like:

    final Button btnHighlight = new Button("Highlight");
    btnHighlight.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedIndices()));
    btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            highlightRows.setAll(table.getSelectionModel().getSelectedIndices());
        }
    });
    
    final Button btnClearHighlight = new Button("Clear Highlights");
    btnClearHighlight.disableProperty().bind(Bindings.isEmpty(highlightRows));
    btnClearHighlight.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            highlightRows.clear();
        }
    });