Programmatically change the TableView row appearan

2019-01-03 15:32发布

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", "jacob.smith@example.com"),
            new Person("Isabella", "Johnson", "isabella.johnson@example.com"),
            new Person("Ethan", "Williams", "ethan.williams@example.com"),
            new Person("Emma", "Jones", "emma.jones@example.com"),
            new Person("Michael", "Brown", "michael.brown@example.com")
        );

    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?

6条回答
放荡不羁爱自由
2楼-- · 2019-01-03 15:52

The best way I find to do this:

In my CSS

.table-row-cell:feederChecked{
    -fx-background-color: #06FF00;
}

In my table initialization with a SimpleBooleanProperty of an Object content in my ObservableList:

// The pseudo classes feederChecked that were defined in the css file.
PseudoClass feederChecked = PseudoClass.getPseudoClass("feederChecked");
// Set a rowFactory for the table view.
tableView.setRowFactory(tableView -> {
    TableRow<Feeder> row = new TableRow<>();
    ChangeListener<Boolean> changeListener = (obs, oldFeeder, newFeeder) -> {
        row.pseudoClassStateChanged(feederChecked, newFeeder);
    };
    row.itemProperty().addListener((obs, previousFeeder, currentFeeder) -> {
        if (previousFeeder != null) {
            previousFeeder.feederCheckedProperty().removeListener(changeListener);
        }
        if (currentFeeder != null) {
            currentFeeder.feederCheckedProperty().addListener(changeListener);
            row.pseudoClassStateChanged(feederChecked, currentFeeder.getFeederChecked());
        } else {
            row.pseudoClassStateChanged(feederChecked, false);
        }
    });
    return row;
});

Code adapting from this complete exemple

查看更多
神经病院院长
3楼-- · 2019-01-03 15:54

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.

查看更多
爷、活的狠高调
4楼-- · 2019-01-03 15:56

If you don't want the reusability of the solution I posted above, 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();
        }
    });
查看更多
姐就是有狂的资本
5楼-- · 2019-01-03 15:58

I found that the best solution would be to listen for row.itemProperty() changes because when you sort for example the rows change indexes, so rows get notified automatically.

查看更多
叛逆
6楼-- · 2019-01-03 16:01

I might have found something that works:

With this code added, if you press the button the highlighted row changes color, when you select a different row the color changes back to default, when you press the button again, it changes the color of the new row to brown.

final String css = getClass().getResource("style.css").toExternalForm();
final Scene scene = new Scene(new Group());


btnHighlight.setOnAction(new EventHandler<ActionEvent>() {
    @Override
     public void handle(ActionEvent e) {
         scene.getStylesheets().add(css);
     }
});
table.getSelectionModel().selectedIndexProperty()
            .addListener(new ChangeListener<Number>() {
    @Override
     public void changed(ObservableValue<? extends Number> ov, Number t, Number t1) {
         scene.getStylesheets().remove(css);
     }
});

css:

.table-row-cell:selected
{
     -fx-background-color: brown;
     -fx-text-inner-color: white;
}

Only problem with this solution is that if you press the button twice in a row, your next row selected is already brown. You would have to use a seperate css file for this, else at startup of the application no css rules would be applied untill you press the button.

查看更多
放我归山
7楼-- · 2019-01-03 16:04

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.

查看更多
登录 后发表回答