Showing posts with label tableview. Show all posts
Showing posts with label tableview. Show all posts

Thursday 28 November 2013

JavaFX TableView Cell Renderer

In this post I will show how to customize the rendering of a JavaFX 2 TableView. The Birthdaycolumn in the screenshot below is a formatted Calendar object. Depending on the year, the text color is changed.

How a Table Cell is Rendered

Each table cell will receive an object, in our case it is an instance of Person. To do the rendering, the cell will need a Cell Value Factory) and a Cell Factory):

Cell Value Factory

The cell must know which part of Person it needs to display. For all cells in the birthday columnthis will be the Personbirthday value.
This is our birthday column:
1
private TableColumn<Person, Calendar> birthdayColumn;
And later during initialization, we’ll set the Cell Value Factory:
1
2
birthdayColumn.setCellValueFactory(
    new PropertyValueFactory<Person, Calendar>("birthday"));
So far nothing too fancy.

Cell Factory

Once the cell has the value, it must know how to display that value. In our case, the birthday’s Calendar value must be formatted and colored depending on the year.
[update 2012-12-27: Set text to null if cell is empty. See comment by James_D below]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
birthdayColumn.setCellFactory(new Callback<TableColumn<Person, Calendar>, TableCell<Person, Calendar>>() {
  @Override
  public TableCell<Person, Calendar> call(TableColumn<Person, Calendar> param) {
      return new TableCell<Person, Calendar>() {

          @Override
          protected void updateItem(Calendar item, boolean empty) {
              super.updateItem(item, empty);

              if (!empty) {
                // Use a SimpleDateFormat or similar in the format method
                setText(format(item));

                if (item.get(Calendar.YEAR) == 2011) {
                  setTextFill(Color.CHOCOLATE);
                } else {
                  setTextFill(Color.BLACK);
                }

              } else {
                setText(null);
              }
          }
      };
  }
});
The Cell Factory contains some complicated stuff (CallbackGenerics and Anonymous Inner Classes). Don’t worry too much about all this. Just focus on the important part which is the updateItem(...) method.
This updateItem(...) method gets called whenever the cell should be rendered. We receive the Calendar item that must be rendered. If empty is true we don’t do anything. Otherwise we format the item and set the text of the cell. Depending on the year, we also set the text color.

ListView and TreeView

Note that the JavaFX 2 ListView and TreeView are rendered in a very similar way.

Download

Download the complete tableview-cell-renderer example.

JavaFX updating item in a TableView

JavaFX has a fundamentally different way of handling these kinds of updates, which may make it tricky to implement this in your current system.

In short, the way updates work in JavaFX for the ListView, TreeView and TableView is this:

Each type of View is made out of Cells. The amount of Cells is usually pretty close to the amount of visible rows and each Cell could be thought of to represent one Row.

Each Cell is basically a small piece of UI, that adapts itself to whatever should be displayed at the given row at that time. The updateItem method is called on these Cells to associate them with an underlying Item from the ObservableList (your model).

Let's say your "Items" in the Model are Person objects with a name. A cell implementation might render this as follows:
  private static final class PersonTableCell extends TableCell<Person, Person> {

    @Override
    protected void updateItem(final Person person, boolean empty) {
      super.updateItem(mediaNode, empty);

      if(!empty) {
        setText(person.getTitle());
      }
    }
  }
The example above will also have the same update problem as your code, that is, if you change the Person object in your model the Cell will not reflect the changed name.

In JavaFX to let the Cell know about the change, you have to add a listener to the "text" property of your Person object. This assumes that the properties of a Person object are JavaFX style properties. This is accomplished like this (using a binding which uses a listener internally):
  private static final class PersonTableCell extends TableCell<Person, Person> {

    @Override
    protected void updateItem(final Person person, boolean empty) {
      super.updateItem(mediaNode, empty);

      textProperty().unbind();

      if(!empty) {
        textProperty().bind(person.titleProperty());
      }
    }
  }
The above will update correctly automatically whenever a Person's title property changes.

However, this means that you will have to change your Person object to use JavaFX style properties. You donot always have this luxury. However, there are other options. You could for example only support a "changedProperty" in the Person class, and have the Cells listen for to this, something like this:

  private static final class PersonTableCell extends TableCell<Person, Person> {
    private Person oldPerson;
    private InvalidationListener listener;

    @Override
    protected void updateItem(final Person person, boolean empty) {
      super.updateItem(mediaNode, empty);

      if(oldPerson != null) {
        oldPerson.removeListener(listener);
        oldPerson = null;
      }

      if(!empty) {
        listener = new InvalidatonListener() {
          public void invalidated(Observable o) {
             setText(person.getTitle());
          }
        };
        person.addListener(listener);
        setText(person.getTitle());
      }
    }
  }
Now each time you want to trigger a "change", you call a method on the Person object that triggers an Invalidation event. All the cells that are listening to this Person object will be notified and update themselves.