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:

Customising Columns
Without customisation, the JavaFX TableView
displays text as standard. That means by default using the toString() method of most common objects:
- Float and Double: printed with no formatting to the minimum number of significant digits
- 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:

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.

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!

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:
- We can set the node manually – with any node.
- The
TableCell
will then internally bind thisNode
inside anObjectProperty
wrapper, so that if theNode
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.

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..

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 (we won’t do it for real here, but check out the link for complete guide on how to do that!), 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):
- Create a GridPane to hold the contents
- Over the first two rows and in the first column, display the Driver’s picture
- In the top right cell (row 0, column 1), print the driver’s name
- 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)
- Return the GridPane to the CellFactory to display

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.

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.