How to set a TableRow's background colour based on whether or not it's selected and/or a value in the data model, in a JavaFX8 TableView?

ぐ巨炮叔叔 提交于 2020-01-15 12:15:20

问题


A relative Java newbie question.

I'm trying set a TableRow's background colour based on whether or not it's selected and/or whether or not a boolean value in the data model is true.

I've found ways of doing each but not both together in the same setRowFactory.

What I would like to end up with is this (albeit with horrible colours for example purposes only!):

How would I go about achieving that?

This is what I found wrt changing row colour based on selection. It's adapted from user James_D's answer here https://community.oracle.com/thread/3528543.

final ObservableSet<Integer> selectedRowIndexes = FXCollections.observableSet();

table.getSelectionModel().getSelectedCells().addListener((Change<? extends TablePosition> change) -> {  
    selectedRowIndexes.clear();
    selectedRowIndexes.add( (table.getSelectionModel().getSelectedCells().get(0)).getRow() );
});

table.setRowFactory(tv -> {
    TableRow<TestModel> row = new TableRow<>();
    BooleanBinding selected = Bindings.createBooleanBinding(() ->  
        selectedRowIndexes.contains(new Integer(row.getIndex())), row.indexProperty(), selectedRowIndexes);
    row.styleProperty().bind(Bindings.when(selected)
        .then("-fx-background-color:  green;")
        .otherwise(""));
    return row;
});

And this is what I found wrt changing row colour based on a cell value. It's adapted from user kleopatra's answer here TreeTableView : setting a row not editable.

table.setRowFactory(tv -> {
    TableRow<TestModel> row = new TableRow<TestModel>() {
        @Override
        public void updateItem(TestModel testmodel, boolean empty) {
            super.updateItem(testmodel, empty);
            boolean locked = false;
            if ( getItem() != null ) {
                locked = getItem().lockedProperty().get();
                setEditable( ! locked);
            }
            if (!isEmpty() && locked ) {
                setStyle("-fx-background-color: red;");
            }else{
                setStyle(null);
            }
        }
    };
    return row;
});

However, I've ended up with two row factories and haven't been able to figure out how merge them into one.

If it helps, here is the MVCE I've been playing with. It has the two row factories. I haven't included my (many!) attempts to merge them as none worked.

I'm using JavaFX8 (JDK1.8.0_181), NetBeans 8.2 and Scene Builder 8.3.

package test31;

import java.util.Arrays;
import javafx.application.Application;
import static javafx.application.Application.launch;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
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.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.BooleanStringConverter;

public class Test31 extends Application {

    private Parent createContent() {

        TableView<TestModel> table = new TableView<>();

        ObservableList<TestModel> olTestModel = FXCollections.observableArrayList(testmodel -> new Observable[] {});
        olTestModel.add(new TestModel("1", true));
        olTestModel.add(new TestModel("2", false));
        olTestModel.add(new TestModel("3", false));
        olTestModel.add(new TestModel("4", true));
        olTestModel.add(new TestModel("5", false));

        TableColumn<TestModel, String> colText = new TableColumn<>("textfield");
        colText.setCellValueFactory(cb -> cb.getValue().textFieldProperty());
        colText.setCellFactory(TextFieldTableCell.forTableColumn());

        TableColumn<TestModel, Boolean> colBoolean = new TableColumn<>("locked");
        colBoolean.setCellValueFactory(cb -> cb.getValue().lockedProperty());
        colBoolean.setCellFactory(TextFieldTableCell.forTableColumn(new BooleanStringConverter()));

        table.getSelectionModel().setCellSelectionEnabled(true);
        table.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
        table.setEditable(true);
        table.getColumns().addAll(Arrays.asList(colText, colBoolean));
        table.setItems(olTestModel);

        //****************************************************************************************
        //First row factory:  Set background colour based on whether or not the row is selected
        final ObservableSet<Integer> selectedRowIndexes = FXCollections.observableSet();

        table.getSelectionModel().getSelectedCells().addListener((Change<? extends TablePosition> change) -> {  
            selectedRowIndexes.clear();
            selectedRowIndexes.add( (table.getSelectionModel().getSelectedCells().get(0)).getRow() );
        });

        table.setRowFactory(tv -> {
            TableRow<TestModel> row = new TableRow<>();
            BooleanBinding selected = Bindings.createBooleanBinding(() ->  
                selectedRowIndexes.contains(new Integer(row.getIndex())), row.indexProperty(), selectedRowIndexes);
            row.styleProperty().bind(Bindings.when(selected)
                .then("-fx-background-color:  green;")
                .otherwise(""));
            return row;
        });

        //****************************************************************************************
        //Second row factory:  Set background colour based on the value of a boolean property
        table.setRowFactory(tv -> {
            TableRow<TestModel> row = new TableRow<TestModel>() {
                @Override
                public void updateItem(TestModel testmodel, boolean empty) {
                    super.updateItem(testmodel, empty);
                    boolean locked = false;
                    if ( getItem() != null ) {
                        locked = getItem().lockedProperty().get();
                        setEditable( ! locked);
                    }
                    if (!isEmpty() && locked ) {
                        setStyle("-fx-background-color: red;");
                    }else{
                        setStyle(null);
                    }
                }
            };
            return row;
        });

        BorderPane content = new BorderPane(table);

        return content;

    }

    public class TestModel {

        private StringProperty textField;
        private BooleanProperty locked;

        public TestModel() {
            this("", false);
        }

        public TestModel(
            String textField,
            boolean locked
        ) {
            this.textField = new SimpleStringProperty(textField);
            this.locked = new SimpleBooleanProperty(locked);
        }

        public String getTextField() {
            return textField.get().trim();
        }

        public void setTextField(String textField) {
            this.textField.set(textField);
        }

        public StringProperty textFieldProperty() {
            return textField;
        }

        public boolean getLocked() {
            return locked.get();
        }

        public void setLocked(boolean locked) {
            this.locked.set(locked);
        }

        public BooleanProperty lockedProperty() {
            return locked;
        }

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle("Test");
        stage.setWidth(500);
        stage.show();
    }

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

}

回答1:


There are a couple ways you can do this. Here's an example using external CSS and pseudo-classes:

Main.java

import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class Main extends Application {

  @Override
  public void start(Stage primaryStage) {
    TableView<Item> table = new TableView<>(createDummyData(100));
    table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
    table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);

    table.setRowFactory(t -> new ItemTableRow());

    TableColumn<Item, String> nameCol = new TableColumn<>("Name");
    nameCol.setCellValueFactory(features -> features.getValue().nameProperty());
    table.getColumns().add(nameCol);

    TableColumn<Item, Boolean> validCol = new TableColumn<>("Valid");
    validCol.setCellValueFactory(features -> features.getValue().validProperty());
    table.getColumns().add(validCol);

    primaryStage.setScene(new Scene(new StackPane(table), 800, 600));
    primaryStage.getScene().getStylesheets().add(getClass().getResource("Main.css").toExternalForm());
    primaryStage.setTitle("JavaFX Application");
    primaryStage.show();
  }


  private ObservableList<Item> createDummyData(int count) {
    return IntStream.rangeClosed(1, count)
        .mapToObj(i -> "Item #" + i)
        .map(name -> new Item(name, Math.random() >= 0.5))
        .collect(Collectors.toCollection(FXCollections::observableArrayList));
  }

}

ItemTableRow.java

import javafx.beans.value.ChangeListener;
import javafx.beans.value.WeakChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.control.TableRow;

public class ItemTableRow extends TableRow<Item> {

  private static final PseudoClass VALID = PseudoClass.getPseudoClass("valid");

  private final ChangeListener<Boolean> listener = (obs, oldVal, newVal) -> updateValidPseudoClass(newVal);
  private final WeakChangeListener<Boolean> weakListener = new WeakChangeListener<>(listener);

  public ItemTableRow() {
    getStyleClass().add("item-table-row");
  }

  @Override
  protected void updateItem(Item item, boolean empty) {
    Item oldItem = getItem();
    if (oldItem != null) {
      oldItem.validProperty().removeListener(weakListener);
    }
    super.updateItem(item, empty);
    if (empty || item == null) {
      updateValidPseudoClass(false);
    } else {
      item.validProperty().addListener(weakListener);
      updateValidPseudoClass(item.isValid());
    }
  }

  private void updateValidPseudoClass(boolean active) {
    pseudoClassStateChanged(VALID, active);
  }

}

Item.java

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Item {

  private final StringProperty name = new SimpleStringProperty(this, "name");
  public final void setName(String name) { this.name.set(name); }
  public final String getName() { return name.get(); }
  public final StringProperty nameProperty() { return name; }

  private final BooleanProperty valid = new SimpleBooleanProperty(this, "valid");
  public final void setValid(boolean valid) { this.valid.set(valid); }
  public final boolean isValid() { return valid.get(); }
  public final BooleanProperty validProperty() { return valid; }

  public Item() {}

  public Item(String name, boolean valid) {
    setName(name);
    setValid(valid);
  }

}

Main.css

.item-table-row:selected {
    -fx-background-color: -fx-control-inner-background, green;
}

.item-table-row:valid {
    -fx-background-color: -fx-control-inner-background, yellow;
}

.item-table-row:valid:selected {
    -fx-background-color: -fx-control-inner-background, red;
}

If you prefer to only use code, change ItemTableRow to this (and remove getStylesheets().add(...) from Main):

import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.scene.control.TableRow;

public class ItemTableRow extends TableRow<Item> {

  private final InvalidationListener listener = observable -> updateStyle();
  private final WeakInvalidationListener weakListener = new WeakInvalidationListener(listener);

  public ItemTableRow() {
    getStyleClass().add("item-table-row");
    selectedProperty().addListener(listener); // could also override updateSelected
  }

  @Override
  protected void updateItem(Item item, boolean empty) {
    Item oldItem = getItem();
    if (oldItem != null) {
      oldItem.validProperty().removeListener(weakListener);
    }
    super.updateItem(item, empty);
    if (item != null) {
      item.validProperty().addListener(weakListener);
    }
    updateStyle();
  }

  private void updateStyle() {
    final Item item = getItem();
    if (item == null || (!isSelected() && !item.isValid())) {
      setStyle(null);
    } else if (isSelected() && item.isValid()) {
      setStyle("-fx-background-color: -fx-control-inner-background, red;");
    } else if (isSelected()) {
      setStyle("-fx-background-color: -fx-control-inner-background, green;");
    } else if (item.isValid()) {
      setStyle("-fx-background-color: -fx-control-inner-background, yellow;");
    } else {
      // I don't think this branch is possible, but not 100% sure
      throw new AssertionError("Shouldn't be here?");
    }
  }

}

The -fx-control-inner-background value is defined in modena.css (the default stylesheet for JavaFX 8+). It gives the TableRow that little bit of color padding.



来源:https://stackoverflow.com/questions/52885237/how-to-set-a-tablerows-background-colour-based-on-whether-or-not-its-selected

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!