Background with 2 colors in JavaFX?

I used a simple layer of background colors to produce a red highlight (similar to Stefan’ suggested solution).

/**
 * file: table.css
 *   Place in same directory as TableViewPropertyEditorWithCSS.java.
 *   Have your build system copy this file to your build output directory.
 **/

.highlighted-cell {
  -fx-text-fill: -fx-text-inner-color;
  -fx-background-color: firebrick, gainsboro;
  -fx-background-insets: 0, 2 0 0 0;
}

For a standard region like a stackpane, all you really need to do is apply the above css (less the -fx-text-fill) to get the desired result.


Here is another tricky way to define the color using a gradient:

-fx-background-color: 
  linear-gradient(
    from 0px 0px to 0px 2px, 
      firebrick, firebrick 99%, 
    gainsboro
  );

In the screenshot below, the value cells are highlighted (by having the highlighted-cell css class applied to them) if have the value false.

highlightedcells

Highlight cell style class switch logic:

public void updateItem(Object item, boolean empty) {
  super.updateItem(item, empty);
  if (empty) {
    ....
    getStyleClass().remove("highlighted-cell");
  } else {
    if (getItem() instanceof Boolean && (Boolean.FALSE.equals((Boolean) getItem()))) {
      getStyleClass().add("highlighted-cell");
    } else {
      getStyleClass().remove("highlighted-cell");
    }
    ...
  }
}

It looks good when the highlighted-cell style class applied to a standard table cell (during an updateItem call for a custom cell) but does have a couple of drawbacks. The table coloring scheme is very subtle and complex. It has highlights for odd/even values, highlights for selected rows, highlights for selected hovered rows, highlights for focused rows and cells, etc. Plus it has various combinations of all of the above. Just setting the background-color directly in the highlight-cell class is a kind of brute force way to achieve what you want because it does not take all these other subtleties into account and just overrides them, so a cell which has been highlighted using this style always looks the same no matter what temporary css psuedo-class state has been applied to it.

It’s fine really, but a nicer solution would color the highlighted cell differently depending on psuedo-class states. That is quite a tricky thing to do though and you could waste a lot of time playing around with various states and css selector combinations to try to get the nice changing highlight. In all, for this example it didn’t seem worth that extra effort for me, though it may be for you.


Test program (apologies for length and complexity of this, it was just easier for me to integrate the style highlighting logic into an existing program):

import java.lang.reflect.*;
import java.util.logging.*;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import javafx.util.Callback;
// click in the value column (a couple of times) to edit the value in the column.
// property editors are defined only for String and Boolean properties.
// change focus to something else to commit the edit.
public class TableViewPropertyEditorWithCSS extends Application {

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

  @Override
  public void start(Stage stage) {
    final Person aPerson = new Person("Fred", false, false, "Much Ado About Nothing");
    final Label currentObjectValue = new Label(aPerson.toString());
    TableView<NamedProperty> table = new TableView();
    table.setEditable(true);
    table.setItems(createNamedProperties(aPerson));
    TableColumn<NamedProperty, String> nameCol = new TableColumn("Name");
    nameCol.setCellValueFactory(new PropertyValueFactory<NamedProperty, String>("name"));
    TableColumn<NamedProperty, Object> valueCol = new TableColumn("Value");
    valueCol.setCellValueFactory(new PropertyValueFactory<NamedProperty, Object>("value"));
    valueCol.setCellFactory(new Callback<TableColumn<NamedProperty, Object>, TableCell<NamedProperty, Object>>() {
      @Override
      public TableCell<NamedProperty, Object> call(TableColumn<NamedProperty, Object> param) {
        return new EditingCell();
      }
    });
    valueCol.setOnEditCommit(
            new EventHandler<CellEditEvent<NamedProperty, Object>>() {
      @Override
      public void handle(CellEditEvent<NamedProperty, Object> t) {
        int row = t.getTablePosition().getRow();
        NamedProperty property = (NamedProperty) t.getTableView().getItems().get(row);
        property.setValue(t.getNewValue());
        currentObjectValue.setText(aPerson.toString());
      }
    });
    table.getColumns().setAll(nameCol, valueCol);
    table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
    VBox layout = new VBox(10);
    layout.setStyle("-fx-background-color: cornsilk; -fx-padding: 10;");
    layout.getChildren().setAll(
            currentObjectValue,
            table);
    VBox.setVgrow(table, Priority.ALWAYS);

    Scene scene = new Scene(layout, 650, 600);
    scene.getStylesheets().add(getClass().getResource("table.css").toExternalForm());
    stage.setScene(scene);
    stage.show();
  }

  private ObservableList<NamedProperty> createNamedProperties(Object object) {
    ObservableList<NamedProperty> properties = FXCollections.observableArrayList();
    for (Method method : object.getClass().getMethods()) {
      String name = method.getName();
      Class type = method.getReturnType();
      if (type.getName().endsWith("Property")) {
        try {
          properties.add(new NamedProperty(name, (Property) method.invoke(object)));
        } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
          Logger.getLogger(TableViewPropertyEditorWithCSS.class.getName()).log(Level.SEVERE, null, ex);
        }
      }
    }
    return properties;
  }

  public class NamedProperty {

    public NamedProperty(String name, Property value) {
      nameProperty.set(name);
      valueProperty = value;
    }
    private StringProperty nameProperty = new SimpleStringProperty();

    public StringProperty nameProperty() {
      return nameProperty;
    }

    public StringProperty getName() {
      return nameProperty;
    }

    public void setName(String name) {
      nameProperty.set(name);
    }
    private Property valueProperty;

    public Property valueProperty() {
      return valueProperty;
    }

    public Object getValue() {
      return valueProperty.getValue();
    }

    public void setValue(Object value) {
      valueProperty.setValue(value);
    }
  }

  public class Person {

    private final SimpleStringProperty firstName;
    private final SimpleBooleanProperty married;
    private final SimpleBooleanProperty hasChildren;
    private final SimpleStringProperty favoriteMovie;

    private Person(String firstName, Boolean isMarried, Boolean hasChildren, String favoriteMovie) {
      this.firstName = new SimpleStringProperty(firstName);
      this.married = new SimpleBooleanProperty(isMarried);
      this.hasChildren = new SimpleBooleanProperty(hasChildren);
      this.favoriteMovie = new SimpleStringProperty(favoriteMovie);
    }

    public SimpleStringProperty firstNameProperty() {
      return firstName;
    }

    public SimpleBooleanProperty marriedProperty() {
      return married;
    }

    public SimpleBooleanProperty hasChildrenProperty() {
      return hasChildren;
    }

    public SimpleStringProperty favoriteMovieProperty() {
      return favoriteMovie;
    }

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

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

    public Boolean getMarried() {
      return married.get();
    }

    public void setMarried(Boolean isMarried) {
      married.set(isMarried);
    }

    public Boolean getHasChildren() {
      return hasChildren.get();
    }

    public void setHasChildren(Boolean hasChildren) {
      this.hasChildren.set(hasChildren);
    }

    public String getFavoriteMovie() {
      return favoriteMovie.get();
    }

    public void setFavoriteMovie(String movie) {
      favoriteMovie.set(movie);
    }

    @Override
    public String toString() {
      return firstName.getValue() + ", isMarried? " + married.getValue() + ", hasChildren? " + hasChildren.getValue() + ", favoriteMovie: " + favoriteMovie.get();
    }
  }

  class EditingCell extends TableCell<NamedProperty, Object> {

    private TextField textField;
    private CheckBox checkBox;

    public EditingCell() {
    }

    @Override
    public void startEdit() {
      if (!isEmpty()) {
        super.startEdit();
        if (getItem() instanceof Boolean) {
          createCheckBox();
          setText(null);
          setGraphic(checkBox);
        } else {
          createTextField();
          setText(null);
          setGraphic(textField);
          textField.selectAll();
        }
      }
    }

    @Override
    public void cancelEdit() {
      super.cancelEdit();
      if (getItem() instanceof Boolean) {
        setText(getItem().toString());
      } else {
        setText((String) getItem());
      }
      setGraphic(null);
    }

    @Override
    public void updateItem(Object item, boolean empty) {
      super.updateItem(item, empty);
      if (empty) {
        setText(null);
        setGraphic(null);
        getStyleClass().remove("highlighted-cell");
      } else {
        if (getItem() instanceof Boolean && (Boolean.FALSE.equals((Boolean) getItem()))) {
          getStyleClass().add("highlighted-cell");
        } else {
          getStyleClass().remove("highlighted-cell");
        }
        if (isEditing()) {
          if (getItem() instanceof Boolean) {
            if (checkBox != null) {
              checkBox.setSelected(getBoolean());
            }
            setText(null);
            setGraphic(checkBox);
          } else {
            if (textField != null) {
              textField.setText(getString());
            }
            setText(null);
            setGraphic(textField);
          }
        } else {
          setText(getString());
          setGraphic(null);
        }
      }
    }

    private void createTextField() {
      textField = new TextField(getString());
      textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
      textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
          if (!newValue) {
            commitEdit(textField.getText());
          }
        }
      });
    }

    private void createCheckBox() {
      checkBox = new CheckBox();
      checkBox.setSelected(getBoolean());
      checkBox.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
      checkBox.focusedProperty().addListener(new ChangeListener<Boolean>() {
        @Override
        public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
          if (!newValue) {
            commitEdit(checkBox.isSelected());
          }
        }
      });
    }

    private String getString() {
      return getItem() == null ? "" : getItem().toString();
    }

    private Boolean getBoolean() {
      return getItem() == null ? false : (Boolean) getItem();
    }
  }
}

Leave a Comment