If you’re designing a rich user interface, printing out the basic information your user needs as text might be efficient. But, it may not make for a great user experience. Fortunately, JavaFX is highly customizable: columns and cells can be designed and displayed using custom CellFactory and CellValueFactory callbacks.

JavaFX provides functionality to customise columns, rows, cell values and cell graphics within its TableView through the use of Factory classes and Callbacks. In each of these cases, providing a Callback to the TableView in place of the default will modify the behaviour and appearance of the TableView.

Specifically, you can customise how elements display their data programatically using the CellFactory and CellValueFactory classes. We’ll do both in this project.

The starting code for this tutorial is based on the Styling A JavaFX TableView tutorial. There, we created a table that summarises orders and prints them to the screen. In this project, we’ll expand the table to include details about the cost, courier and tracking information. You can find the starting code here.

Finally, we’ll modify the priority, cost and driver columns using CellValueFactory and CellFactory to give us an interface users will enjoy interacting with:

A JavaFX TableView with custom CellFactory callbacks

Customising Columns

Without customisation, the JavaFX TableView displays text as standard. That means by default using the toString() method of most common objects:

  1. Float and Double: printed with no formatting to the minimum number of significant digits
  2. Boolean: printed as ‘true’ and ‘false’.

Combined with that, most modern programs store information as a series of connected data models. In this case, every order has a courier and a driver. It would be a waste of memory – and a nightmare to maintain – if every order stored that information separately. Instead, we store an ID, which can be used to look up the right information. This works well with databases, which underpin most modern programs too.

The resulting default table is rather underwhelming:

A JavaFX TableView without customisation

The Cell Value Factory

The first place we’ll start is with cost. Each Order will have an associated cost, but we’ll calculate a per-item surcharge of 10% for every item with priority shipping.

Good program organisation would almost certainly place the responsibility for calculating this with the Order class, or perhaps some Shipping class, but for the sake of demonstrating the use of the CellValueFactory, we’ll demonstrate how to perform calculations on the fly.

Finally, we’ll format the text to show a cost in dollars to two decimal places.

Formatting Text

The input for the cost column could be a Double. We’re displaying a cost, and that needs a decimal point, right? We’d define that each Row represents an Order, and that this column will return a Double. However, although we want to factor in a cost, we’ll be outputting a formatted String. So, in this case we define the TableColumn to take an Order, and return a String:

public TableColumn<Order, String> costColumn;

Once we’ve defined the column, we need a method to calculate the shipping cost of the item. We have the item weight, so for simplicity let’s assume we’ve got some special deal. I don’t know how, but we’re paying a buck per kilogram. So, all we need to do to test whether the item is a priority order and apply the premium overhead.

private double calculateItemCost(Order order){
    double priorityMultiplier = order.isPriorityShipping() ? 1.1d : 1d;
    return order.getWeight() * priorityMultiplier;
}

Finally, we’ll use the setCellValueFactory() method available on the column to define how we want that text to be displayed. Here, we’ll define a DecimalFormat, specifying we want two – and only two – digits after the decimal point. We’ll grab the cost, format it, and return it as a SimpleStringProperty.

DecimalFormat currency = new DecimalFormat("$0.00");

costColumn.setCellValueFactory(cellData -> {
     String formattedCost = currency.format(calculateItemCost(cellData.getValue()));
    return new SimpleStringProperty(formattedCost);
});

With that, the String that’s returned will be formatted to display cost. We included the currency symbol – dollars – and the correct number of decimal places.

A JavaFX TableView with a custom CellValueFactory for the cost column.

Next, we’ll deal with the priority shipping column. Here, we’ll create a VIP sticker and display it instead of the text “true”.

Customising cells with a CellFactory

The TableColumn setCellFactory() method allows the user to completely customise the contents of a cell. The only thing to be aware of is that the size of the column won’t expand to fit the Node. That’s good to know before you design an impressive layout no one will ever see.

Instead, the width of a column is set by the TableView ResizeConstraints property, in combination with the preferred width of each column. Because JavaFX expects text contents as default, it assumes that it can wrap the text as needed.

CellFactory 101: Replacing text with an image

Before creating some completely custom elements for our TableView, we’ll start simple. Firstly, we’ll replace the ‘true’ and ‘false’ automatically created by the TableColumn. We’ll replace them with an optional VIP sticker, which we’ll display when priority shipping equals true.

In JavaFX the CellFactory defines how to render both the text and the overall appearance. The default is to create a new TableCell of the correct sort (here, it’s Order, Boolean) and return it unchanged. We’ll add this code just beneath where we define the column, in the initialize() method of the Controller. Don’t worry, we’ll customise it later.

shipTypeColumn.setCellFactory(col -> {
    TableCell<Order, Boolean> cell = new TableCell<>();
    return cell;
});

Next, we’ll create a method to generate a graphic for each Order. Later we’ll add this into the Cell factory method, but for now, let’s just create the graphic.

private Node createPriorityGraphic(){
    HBox graphicContainer = new HBox();
    graphicContainer.setAlignment(Pos.CENTER);
    ImageView imageView = new ImageView(new Image(getClass().getResourceAsStream("/img/important.png")));
    imageView.setFitHeight(25);
    imageView.setPreserveRatio(true);
    graphicContainer.getChildren().add(imageView);
    return graphicContainer;
}

Don’t forget to add the image file to your resources or you’ll get java.lang.NullPointerException: Input stream must not be null thrown every time you load the method! The “important.png” file, along with the final code for this project can be found in our Github here.

As with all the little graphics we’ll use today, these are free resources from Flaticon.com. The VIP sticker was designed by Freepik and the rest were designed by Pixel Perfect.

CellFactory Entity Cycle

It’s really important to note that at the point in code where we define the CellFactory – the initialise() method of the Controller – that there will be no TableView yet. This code prepares the content for display to the screen, rather than operating on something on-screen.

That means that if we try to access the Order via cell.getTableRow().getItem() we’ll get hit with a NullPointerException. JavaFX will loop through our column and create all the cells later, but right now the rows don’t exist.

Instead, we’ll hook into JavaFX’s Property support and add a listener to the TableCell. That way, when JavaFX loops through to set the row item on each cell, it will update automatically. Because JavaFX can create cells where the item is null, we’ll test for this before we apply our change. That way, we’ll avoid any Runtime exceptions later on.

We’ll add this code in between where we create the cell and return it in the setCellFactory() method, remembering that newVal is the contents of the cell – which is the boolean isPriorityShipping from the Order.

cell.itemProperty().addListener((obs, old, newVal) -> {
    if (newVal != null) {
        Node centreBox = createPriorityGraphic(newVal);
        cell.graphicProperty().bind(Bindings.when(cell.emptyProperty()).then((Node) null).otherwise(centreBox));
    }
});

With that, we create a node on the fly. In cases where an order has priority shipping, we have a sticker. Perfect!

A JavaFX TableView with a custom CellFactory for the priority column.

Custom Graphics for Cells

There’s no specific support for images or graphics in the TableView – the entire support is through the CellFactory with the Cell graphic, which is an ObjectProperty<Node>.

In the createPriorityGraphic() method above, we created an HBox first. This helped to centre the content (without having to set that separately) but importantly returned the whole thing as a Node. Any Node can be set as the contents for a TableCell in a JavaFX TableView.

This has two effects:

  1. We can set the node manually – with any node.
  2. The TableCell will then internally bind this Node inside an ObjectProperty wrapper, so that if the Node changes, it will automatically be updated in the table

Creating a custom Driver graphic

In a modern program, we might expect that our Order class would store the Driver’s details as an ID that lets us look it up in a database. Storing the details for every driver is memory-intensive and inefficient. So by setting up a database, we’re free to query the database for the drivers we need.

For efficiency, we might cache the results and then bind the data to the Node that contains that data. We talked about this a little above.

A usual scenario might be a database query followed by creating a TableView CellFactory and custom graphic

With your anticipated forgiveness, we’ll load in some Driver details using a method with dummy values. The overall effect is the same from the viewpoint of the CellFactory method. It just means we didn’t have to set up a database for this project..

In this example, we'll replace the database query with an internal method before creating of a TableView CellFactory and custom graphic

Inside the CellFactory, as before, we will need to create a Cell, add a listener to it’s ItemProperty and then use this value (remembering it’s the driver’s ID we’re handling here) to generate the custom graphic with a createDriverGraphic() method.

driverColumn.setCellFactory(col -> {
    TableCell<Order, Integer> cell = new TableCell<>();

    cell.itemProperty().addListener((observableValue, o, newValue) -> {
        if (newValue != null) {
            Node graphic = createDriverGraphic(newValue);
            cell.graphicProperty().bind(Bindings.when(cell.emptyProperty()).then((Node) null).otherwise(graphic));
        }
    });
    return cell;
});

Creating the Driver Info Graphic

With a simple example under our belts, it’s time to dabble in some complete customisation. Here, we’ll load in the data for our driver as if from a database (but not!), and display additional information about the driver in the table.

If you want to peek at how we generated the graphic, it’s here. However, how we generate the Node is less important than the fact that you’re free to generate whatever you want!

Here, we took 5 steps to create the panel (again, look at the code here if you want to see the implementation):

  1. Create a GridPane to hold the contents
  2. Over the first two rows and in the first column, display the Driver’s picture
  3. In the top right cell (row 0, column 1), print the driver’s name
  4. Create the driver’s rating graphic
    • Create an HBox to hold the rating stars
    • For every rating point, add a gold star
    • For every point remaining up to 5, add a black star
    • Add the rating HBox to the bottom right cell (row 1, column 1)
  5. Return the GridPane to the CellFactory to display
A custom node has been created to showcase a driver's details. This can be displayed in a TableView using a CellFactory
The output is pretty simple – but better than text!

Now that we’ve cached the driver’s data, we could bind the rating graphic to the driver’s details. That way, if we change them, we don’t have to worry about updating the graphic. But that’s more of a lesson in binding, rather than customisation so we settled for creating an instance.

That graphic is exported back into the TableCell during creation and now takes pride of place in our TableView.

A JavaFX TableView with custom CellValueFactories for the shipping, cost and driver columns

Conclusions

There are three key ways to customise the appearance of a TableColumn in a JavFX TableView:

  • Customise the table with CSS
  • Modify the values in the columns with a CellValueFactory
  • Create a completely custom display for each cell with a CellFactory

We can use CSS to customise the general feel of a table. But – and it’s a big but – we’ll still have to use the basic values of each property. Instead, you can customise the values for formatting, or create a custom graphic, with a few lines of code. The CellFactory and CellValueFactory classes are what you need.

As always, the full code for this example is available on our GitHub.