Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Programmatically change the TableView row appearance

After doing a Oracle tutorial about the TableView, I was wondering if there's a way to programmatically apply different CSS style to the selected TableView row. For example, user selects a certain row, clicks the "Highlight" button and the selected row gets brown background, white text fill, etc. I've read the JavaFX tableview colors, Updating TableView row appearance and Background with 2 colors in JavaFX?, but to no avail =/

Here's the source:

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.geometry.Insets;
import javafx.scene.Group;
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.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class TableViewSample extends Application {

    private TableView<Person> table = new TableView<Person>();
    private final ObservableList<Person> data =
        FXCollections.observableArrayList(
            new Person("Jacob", "Smith", "[email protected]"),
            new Person("Isabella", "Johnson", "[email protected]"),
            new Person("Ethan", "Williams", "[email protected]"),
            new Person("Emma", "Jones", "[email protected]"),
            new Person("Michael", "Brown", "[email protected]")
        );

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(new Group());
        stage.setTitle("Table View Sample");
        stage.setWidth(450);
        stage.setHeight(600);

        final Label label = new Label("Address Book");
        label.setFont(new Font("Arial", 20));

        TableColumn firstNameCol = new TableColumn("First Name");
        firstNameCol.setMinWidth(100);
        firstNameCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("firstName"));

        TableColumn lastNameCol = new TableColumn("Last Name");
        lastNameCol.setMinWidth(100);
        lastNameCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("lastName"));

        TableColumn emailCol = new TableColumn("Email");
        emailCol.setMinWidth(200);
        emailCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("email"));

        table.setItems(data);
        table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);

        final Button btnHighlight = new Button("Highlight selected row");
        btnHighlight.setMaxWidth(Double.MAX_VALUE);
        btnHighlight.setPrefHeight(30);

        btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
            public void handle(ActionEvent e){
                // this is where the CSS should be applied
            }
        });

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(10, 0, 0, 10));
        vbox.getChildren().addAll(label, table, btnHighlight);

        ((Group) scene.getRoot()).getChildren().addAll(vbox);

        stage.setScene(scene);
        stage.show();
    }

    public static class Person {

        private final SimpleStringProperty firstName;
        private final SimpleStringProperty lastName;
        private final SimpleStringProperty email;

        private Person(String fName, String lName, String email) {
            this.firstName = new SimpleStringProperty(fName);
            this.lastName = new SimpleStringProperty(lName);
            this.email = new SimpleStringProperty(email);
        }

        public String getFirstName() {
            return firstName.get();
        }

        public void setFirstName(String fName) {
            firstName.set(fName);
        }

        public String getLastName() {
            return lastName.get();
        }

        public void setLastName(String fName) {
            lastName.set(fName);
        }

        public String getEmail() {
            return email.get();
        }

        public void setEmail(String fName) {
            email.set(fName);
        }
    }
} 

And the application.css from which the "Highlight selected row" button applies the highlightedRow class to the selected table row:

.highlightedRow {
    -fx-background-color: brown;
    -fx-background-insets: 0, 1, 2;
    -fx-background: -fx-accent;
    -fx-text-fill: -fx-selection-bar-text;
}

Edit:

After several hours of trying, the best thing I could come up is this using the code below:

firstNameCol.setCellFactory(new Callback<TableColumn<Person, String>, TableCell<Person, String>>() {
    @Override
    public TableCell<Person, String> call(TableColumn<Person, String> personStringTableColumn) {
        return new TableCell<Person, String>() {
            @Override
            protected void updateItem(String name, boolean empty) {
                super.updateItem(name, empty);
                if (!empty) {
                    if (name.toLowerCase().startsWith("e") || name.toLowerCase().startsWith("i")) {
                        getStyleClass().add("highlightedRow");
                    }
                    setText(name);
                } else {
                    setText("empty");  // for debugging purposes
                }
            }
        };
    }
});

The part I don't really understand is why I can't do that from inside the setOnAction method of the btnHighlight? I also tried refreshing the table afterwards (described here), but it didn't seem to work. Also, my "solution" only works for the firstNameCol column, so does one have to set new cell factory for each column in order to apply a certain style, or is there a smarter solution?

like image 298
E. Normous Avatar asked Dec 03 '13 11:12

E. Normous


2 Answers

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.

like image 106
James_D Avatar answered Oct 12 '22 21:10

James_D


Here's an ugly hack solution. Firstly, define an int field called highlightedRow. Then set a row factory on the TableView:

table.setRowFactory(new Callback<TableView<Person>, TableRow<Person>>() {
    @Override public TableRow<Person> call(TableView<Person> param) {
        return new TableRow<Person>() {
            @Override protected void updateItem(Person item, boolean empty) {
                super.updateItem(item, empty);

                if (getIndex() == highlightedRow) {
                    getStyleClass().add("highlightedRow");
                } else {
                    getStyleClass().remove("highlightedRow");
                }
            }
        };
    }
});

Then add the following code in your button on action (and this is where the ugly hack comes into play):

btnHighlight.setOnAction(new EventHandler<ActionEvent>(){
    public void handle(ActionEvent e){
        // set the highlightedRow integer to the selection index
        highlightedRow = table.getSelectionModel().getSelectedIndex();

        // force a tableview refresh - HACK
        List<Person> items = new ArrayList<>(table.getItems());
        table.getItems().setAll(items);
    }
});

Once that is done, you get the brown highlight on the selected row. You could of course easily support multiple brown highlights by replacing the int with a list of itns.

like image 7
Jonathan Giles Avatar answered Oct 12 '22 22:10

Jonathan Giles