Using a Custom TableCell Factory to Format a JavaFX table in FXML

We are in the process of designing a table within our JavaFX UI which is being developed using Scene Builder and FXML with CSS for the UI styling.  The table contains information about shipping containers with a requirement that the table row be color coded based on the status of the particular container.  The row will need to be red if the container’s arrival has not yet been planned for, green if its arrival has been planned, but it has not yet arrived, and the default background if the container has been planned for and it has arrived.

I could not find an online tutorial which covered this type of conditional formatting with the UI written in FXML and CSS, so I wanted to document a solution for this problem.

The first step is to define the various formats for the table rows in the application’s CSS file

/* Table text should always be white */

.table-cell { -fx-text-fill: white; -fx-font-size: 14px; } /* Style for default table rows */ .defaultTableStyle { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: linear-gradient( to top, derive(-fx-color, 35.77966101694915%) 0%, derive(-fx-color, 10.05084745762713%) 50%, derive(-fx-color, -8.559322033898283%) 50.5%, derive(-fx-color, -30.0%) 100%); -fx-background-image: url('low_contrast_linen.png'); -fx-background-color: rgba(0,0,0,.75); -fx-border-color: gray; -fx-border-width: 1 1 0 0; }

 

 


/* Format for table rows where container's arrival

has been planned */

.planAssigned { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: #347b34; -fx-border-color: gray; -fx-border-width: 1 1 0 0; } /* Format for row where a plan has not yet been assigned */ .planNotAssigned { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: #983737; -fx-border-color: gray; -fx-border-width: 1 1 0 0; }

/* Format for table row where container has arrived */ .vfcRecovered { -fx-font-size: 12px; -fx-base: rgb(0,0,0,0); -fx-background-color: linear-gradient( to top, derive(-fx-color, 35.77966101694915%) 0%, derive(-fx-color, 10.05084745762713%) 50%, derive(-fx-color, -8.559322033898283%) 50.5%, derive(-fx-color, -30.0%) 100%); -fx-background-image: url('low_contrast_linen.png'); -fx-background-color: rgba(0,0,0,.75); -fx-border-color: gray; -fx-border-width: 1 1 0 0; }

The next step is to create a new custom table cell factory which will be used to select the correct CSS style for the table row based on the container’s status.

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.lynden.fx.table;

import com.lynden.fx.InboundBean;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;

/**
 *
 * @author ROBT
 */
public class FormattedTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {

    public FormattedTableCellFactory() {
    }

    @Override
    public TableCell<S, T> call(TableColumn<S, T> p) {
        TableCell<S, T> cell = new TableCell<S, T>() {
            @Override
            protected void updateItem(Object item, boolean empty) {
                // CSS Styles
                String planNotAssignedStyle = "planNotAssigned";
                String planAssignedStyle = "planAssigned";
                String vfcRecoveredStyle = "vfcRecovered";
                String defaultTableStyle = "defaultTableStyle";
                String cssStyle = "";

                InboundBean inboundBean = null;
                if( getTableRow() != null ) {
                    inboundBean = (InboundBean) getTableRow().getItem();
                }

                //Remove all previously assigned CSS styles from the cell.
                getStyleClass().remove(planAssignedStyle);
                getStyleClass().remove(planNotAssignedStyle);
                getStyleClass().remove(vfcRecoveredStyle);
                getStyleClass().remove(defaultTableStyle);

                super.updateItem((T) item, empty);

                //Determine how to format the cell based on the status of the container.
                if( inboundBean == null ) {
                    cssStyle = defaultTableStyle;
                } else if( inboundBean.isRecovered() ) {
                    cssStyle = vfcRecoveredStyle;
                } else if( inboundBean.getVfcPlan() != null && inboundBean.getVfcPlan().length() > 0 ) {
                    cssStyle = planAssignedStyle;
                } else {
                    cssStyle = planNotAssignedStyle;
                }

                //Set the CSS style on the cell and set the cell's text.
                getStyleClass().add(cssStyle);
                if( item != null ) {
                    setText( item.toString()  );
                } else {
                    setText( "" );
                }
            }
        };
        return cell;
    }
}

The final step in the process is to specify the custom factory in the TableView section of the FXML file.  .

First, in the FXML file, define the packages that contain the custom factory classes.

<?xml version="1.0" encoding="UTF-8"?>

<?import com.lynden.fx.*?>
<?import com.lynden.fx.table.*?>

 

The next step is to then specify the FormattedTableCellFactory as the cellFactory (highlighted in red) for the columns in the TableView.

<TableView fx:id="table" prefHeight="451.0" prefWidth="727.0" styleClass="main-back"> <columns> <TableColumn text=""> <cellFactory> <FormattedTableCellFactory /> </cellFactory> </TableColumn> <TableColumn prefWidth="120.0" text="Plan"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="vfcPlan" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="55.0" text="VFC #"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="vfcNumber" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="100.0" text="Location"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="location" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="100.0" text="Arrival"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="arrivalTime" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="35.0" text="Org"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="origin" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="35.0" text="Dst"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="destination" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="65.0" text="Shipments"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="shipments" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="125.0" text="Consignee"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="consignee" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="35.0" text="Haz"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="hazMat" /> </cellValueFactory> </TableColumn> <TableColumn prefWidth="45.0" text="Temp"> <cellFactory> <FormattedTableCellFactory /> </cellFactory> <cellValueFactory> <PropertyValueFactory property="temperature" /> </cellValueFactory> </TableColumn> </columns> <items>

<!—Sample data to illustrate cell formatting –-> <FXCollections fx:factory="observableArrayList"> <InboundBean arrivalTime="03-11-13 15:00" consignee="Fred Meyer" destination="ANC" hazMat="N" location="Consignee" origin="TAC" shipments="1" temperature="KFF" vfcNumber="345440" vfcPlan="" /> <InboundBean arrivalTime="03-11-13 14:00" consignee="Fred Meyer" destination="ANC" hazMat="N" location="Yard" origin="TAC" shipments="1" temperature="KFF" vfcNumber="123456" vfcPlan="" /> <InboundBean arrivalTime="03-11-13 19:00" consignee="Fred Meyer" destination="ANC" hazMat="N" location="Trip 12543" origin="TAC" shipments="1" temperature="KFF" vfcNumber="235555" vfcPlan="Fred Meyer" /> <InboundBean recovered="true" arrivalTime="03-12-13 10:00" consignee="Costco" destination="KNA" hazMat="N" location="Trip 551332" origin="TAC" shipments="1" temperature="KFF" vfcNumber="244000" vfcPlan="KNA" /> <InboundBean arrivalTime="05-23-13 15:00" consignee="Lowes" destination="ANC" eta="05-23-13 15:00" hazMat="N" location="Yard" origin="TAC" shipments="5" temperature="KFF" vfcNumber="123456" vfcPlan="Fred Meyer" /> <InboundBean arrivalTime="05-23-13 15:00" consignee="Lowes" destination="ANC" eta="05-23-13 15:00" hazMat="N" location="Yard" origin="TAC" shipments="5" temperature="KFF" vfcNumber="123456" vfcPlan="Fred Meyer" /> <InboundBean arrivalTime="05-23-13 15:00" consignee="Lowes" destination="ANC" eta="05-23-13 15:00" hazMat="N" location="Yard" origin="TAC" shipments="5" temperature="KFF" vfcNumber="123456" vfcPlan="Fred Meyer" /> </FXCollections> </items> </TableView>

That’s it. By utilizing this strategy, cells in the JavaFX TableView can be conditionally formatted using custom cell factories and CSS.  The table below is the product of the strategy that was implemented in the above code.

image

twitter: @RobTerp