Featured Post

Applying Email Validation to a JavaFX TextField Using Binding

This example uses the same controller as in a previous post but adds a use case to support email validation.  A Commons Validator object is ...

Sunday, March 8, 2015

Styling Table Cells in JavaFX

In the JavaFX TableView component, off-the-shelf cells are Labels that turn to TextFields when the user double-clicks and the column or table is set to "editable".  This blog post shows an example where certain cells are rendered differently both from a style perspective (red versus black) and from a functionality perspective (not editable).

This is a screenshot from a JavaFX app called the Maven POM Updater.  A TableView displays a list of Maven POM files, one per row.  If the POM file can be parsed successfully, its values are displayed and it is eligible to be edited.  If the parsing throws an error, an error message is displayed in the row itself, the row is colored red, and the row (actually the set of cells) becomes un-editable.

Cells are Rendered Black for Normal Usage and Red for a Parsing Error
tblPOMs is a TableView created in Scene Builder with 4 TableColumns: update, path, version, and parent (version).  Path is uneditable at all times.  Update is a checkbox and is a subject of another post.  Version and Parent display values taken from a POM and -- if the parsing was successful -- allow the user to double-click and change the values.  If the parsing is unsuccessful, the row is not editable and the user is presented with a warning message.

Controller Code

The version TableColumn is tcVersion and the parent TableColumn is tcParent.

In the JavaFX Controller, tcVersion and tcParent are initialized as follows.  Note the reference to the custom class WarningCellFactory.  POMObject is a model POJO with fields for each of the TableColumns.

tcVersion.setCellValueFactory(
  new PropertyValueFactory<POMObject, String>("version")
);
tcVersion.setCellFactory(new WarningCellFactory());
tcVersion.setOnEditCommit(t -> {
  tblPOMSDirty = true;
  ((POMObject) t.getTableView().getItems().get(t.getTablePosition().getRow())).setVersion(t.getNewValue());
});

tcParentVersion.setCellValueFactory(
  new PropertyValueFactory<POMObject, String>("parentVersion")
);
tcParentVersion.setCellFactory(new WarningCellFactory());
tcParentVersion.setOnEditCommit(t -> {
  tblPOMSDirty = true;
  ((POMObject) t.getTableView().getItems().get(t.getTablePosition().getRow())).setParentVersion(t.getNewValue());
});

tblPOMSDirty is a special flag for the operation of the system.

The setOnEditCommit Lambda will update the underlying model POMObject POJO when the user presses the return key in one of the textboxes.

Cell Factory

I supply a customized CellFactory to the TableColumns called WarningCellFactory.  This can be used to produce a very different rendering of cells, say an Image or other specialized control.  In this case, I want to use as much off-the-shelf functionality as possible, but I don't want to accept the default styling and behavior entirely.

WarningCellFactory is based on the TextFieldTableCell class.  This class provides the desired editing behavior: Label -> TextField on a double-click.  My WarningCellFactory class implements the required Callback / call() method.  My call() method returns an anonymous class that extends TextFieldTableCell.  I introduce a private method to make the code a little more readable called "createTableCell()"; this can be removed by a direct "new" of the anonymous class.

public class WarningCellFactory implements Callback<TableColumn<POMObject,String>, TableCell<POMObject,String>>{

  @Override   public TableCell<POMObject, String> call(TableColumn<POMObject, String> col) {
    return createTableCell(col);
  }

  private TextFieldTableCell<POMObject, String> createTableCell(TableColumn<POMObject, String> col) {
    TextFieldTableCell<POMObject, String> cell = new TextFieldTableCell<POMObject, String>(new DefaultStringConverter()) {
    @Override public void updateItem(String arg0, boolean empty) {
      super.updateItem(arg0, empty);
        if( !empty ) {
   this.setText( arg0 );
   POMObject pomObject = (POMObject) this.getTableRow().getItem();
   if( pomObject != null && pomObject.getParseError() ) {
     this.setTextFill(Color.RED);
            this.setEditable( false );
          } else {
            this.setTextFill(Color.BLACK);
            this.setEditable( true );
          }
         } else {
           this.setText( null );  // clear from recycled obj                    
           this.setTextFill(Color.BLACK);
           this.setEditable( true );
         }
        }
    };
    return cell;
  }
}

TextFieldTableCell will also need a StringConverter supplied as a constructor argument.  Since I'm dealing entirely with Strings, this can be safely assumed as DefaultStringConverter.  If my POMObject POJO contained fields with different types, I would prepare different flavors of TextFieldTableCell constructors with different StringConverts (say a LongStringConverter for a Long field).

The special rendering sets the fill and editable properties on the returned component of the TableCell.

You could also try to style the TableView after-the-fact.  I vastly prefer building the TableCells correctly from the start.  That way, we don't have to worry about re-applying any after-action formatting if the TableView is modified.




1 comment:

  1. Thank you very much! This post concluded my misfortunate googling all over the web. :-)

    ReplyDelete