Formatting Rows in a JavaFX TableView Using CSS Pseudo Classes

One strategy for formatting cells in a JavaFX TableView is to utilize CSS Pseudo classes and a TableRowFactory to select the appropriate class to style the row. Below is a screenshot of an applcation that is configured to monitor real time stock quotes. Each row displays price information for a single stock. I would like the rows to be colored based on the price change of the particular stock, with a red row representing a stock that is trading lower since the previous day, and a green row to represent a stock whose current price is higher than the previous day.

enter image description here

First the domain object which represents the Stock is below. Notice that the class uses JavaFX properties to represent the various values that are displayed in each column.

public class Stock implements Level1QuoteListener {

protected StockTicker ticker;
protected SimpleStringProperty symbol = new SimpleStringProperty();
protected SimpleDoubleProperty bid = new SimpleDoubleProperty();
protected SimpleDoubleProperty ask= new SimpleDoubleProperty();
protected SimpleDoubleProperty last = new SimpleDoubleProperty();
protected SimpleDoubleProperty percentChange = new SimpleDoubleProperty();
protected SimpleIntegerProperty volume = new SimpleIntegerProperty();
protected SimpleDoubleProperty previousClose = new SimpleDoubleProperty();
protected SimpleDoubleProperty open = new SimpleDoubleProperty();


public Stock( StockTicker ticker ) {
    this.ticker = ticker;
    setSymbol( ticker.getSymbol() );
}

public void setSymbol( String symbol ) {
    this.symbol.set(symbol);
}

public String getSymbol() {
    return symbol.get();
}

public StringProperty symbol() {
    return symbol;
}

public void setBid( double bid ) {
    this.bid.set(bid);
}

public double getBid() {
    return bid.get();
}

public SimpleDoubleProperty bid() {
    return bid;
}

public void setAsk( double ask ) {
    this.ask.set(ask);
}

public double getAsk() {
    return ask.get();
}

public DoubleProperty ask() {
    return ask;
}

public void setLast( double last ) {
    this.last.set(last);
    double change = ((last-closeProperty().get() ) / closeProperty().get() ) * 100.0;
    setPercentChange(change);

}

public double getLast() {
    return last.get();
}

public DoubleProperty last() {
    return last;
}

public void setPercentChange( double percentChange ) {
    this.percentChange.set(percentChange);
}

public double getPercentChange() {
    return percentChange.get();
}

public void setVolume( int volume ) {
    this.volume.set(volume);
}

public int getVolume() {
    return volume.get();
}

public void setPreviousClose( double close ) {
    this.previousClose.set(close);
    double change = ((last.get()-close ) / close ) * 100.0;
    setPercentChange(change);
}


public double getPreviousClose() {
    return previousClose.get();
}

public DoubleProperty previousClose() {
    return previousClose;
}

public void setOpen( double open ) {
    this.open.set(open);
}

public double getOpen() {
    return open.get();
}

public DoubleProperty openProperty() {
    return open;
}

public DoubleProperty closeProperty() {
    return previousClose;
}

public DoubleProperty percentChangeProperty() {
    return percentChange;
}

public StockTicker getTicker() {
    return ticker;
}



@Override
public void quoteRecieved(ILevel1Quote quote) {
    if( quote.getTicker().equals(ticker)) {
        BigDecimal quoteValue = quote.getValue();
        switch( quote.getType() ) {
            case ASK:
                setAsk(quoteValue.doubleValue());
                break;
            case BID:
                setBid(quoteValue.doubleValue());
                break;
            case LAST:
                setLast(quoteValue.doubleValue());
                break;
            case CLOSE:                  
                setPreviousClose(quoteValue.doubleValue());
                break;
            case OPEN:
                setOpen(quoteValue.doubleValue());
                break;
            case VOLUME:
                setVolume(quoteValue.intValue());
                break;
            default:
                break;

        }
    }
}

The first step in creating a custom format is to define the styles in the CSS stylesheet that is associated with the TableView. The CSS file below shows how the table row will be formatted. Comments below describe each item including the ‘up’ and ‘down’ Pseudo classes that are defined for the .table-row-cell class.

/* Default row background is black */
.table-row-cell {
    -fx-background-color: black;
}

/* Text in the rows will be white */
.table-row-cell .text {
       -fx-fill: white ;
}

/*
Pseudo class 'up' will change the row background
 color to green when activated 
 */
.table-row-cell:up 
{
    -fx-background-color: #007054; 
}

/*
Pseudo class 'down' will change the row background 
color to red when activated
*/
.table-row-cell:down {
    -fx-background-color: #A30029;
}

Ok, so far this seems easy enough. Now the tricky part is to activate the proper Pseudo class when the % change of the stock price is positive or negative. This can be done using a TableRowFactory in the controller class which is managing the TableView.

Below is an example controller which has a reference to a TableView object called tickerTableView. The setup() method contains logic for selecting the appropriate pseudo class based on the stock data.

public class FXMLController implements Initializable {


@FXML
private TableView tickerTableView;


 public void setup() {
 //The pseudo classes 'up' and 'down' that were defined in the css file.
    PseudoClass up = PseudoClass.getPseudoClass("up");
    PseudoClass down = PseudoClass.getPseudoClass("down");



//Set a rowFactory for the table view.
tickerTableView.setRowFactory(tableView -> {
        TableRow<Stock> row = new TableRow<>();
        ChangeListener<Number> changeListener = (obs, oldPrice, newPrice) -> {
            row.pseudoClassStateChanged(up, newPrice.doubleValue() > 0);
            row.pseudoClassStateChanged(down, newPrice.doubleValue() <= 0);
        };

        row.itemProperty().addListener((obs, previousStock, currentStock) -> {
            if (previousStock != null) {
                previousStock.percentChangeProperty().removeListener(changeListener);
            }
            if (currentStock != null) {
                currentStock.percentChangeProperty().addListener(changeListener);
                row.pseudoClassStateChanged(up, currentStock.getPercentChange() > 0);
                row.pseudoClassStateChanged(down, currentStock.getPercentChange() <= 0);
            } else {
                row.pseudoClassStateChanged(up, false);
                row.pseudoClassStateChanged(down, false);
            }
        });
        return row;
    });
}

The first step is to create 2 new PseudoClass object which will map to the Pseudo classes that were defined in the css file.

PseudoClass up = PseudoClass.getPseudoClass("up");
PseudoClass down = PseudoClass.getPseudoClass("down");

Next a RowFactory needs to be defined which knows how to select the proper PseudoClass. The setRowFactory() method on the TableView class takes a Callback object which in turn takes a TableView as an argument and returns a TableRow.

The first thing to do in the row factory is to create a new change listener. This listener will monitor the stock for a particular row. The oldPrice and newPrice are passed into the change listener. The pseudo class of the row can then be activated or deactivated by invoking the pseudoClassStateChanged() method on the TableRow and passing the appropriate PseudoClass and boolean value to indicate whether or not the class should be active.

tickerTableView.setRowFactory(tableView -> {
   TableRow<Stock> row = new TableRow<>();
   ChangeListener<Number> changeListener = (obs, oldPrice, newPrice) -> {
     row.pseudoClassStateChanged(up, newPrice.doubleValue() > 0);
     row.pseudoClassStateChanged(down, newPrice.doubleValue() <= 0);
   };

The final piece is to tie the change listener to the item in the TableRow. This is done by adding a listener on the ItemProperty of the row. Once a Stock item is added to the TableRow, the ChangeListener defined above can be bound to the Stock’s percentChangeProperty. The initial state of the classes are also set when the new item is added. If a Stock item is being removed from the TableRow, the previousStock variable will be populate with the Stock object that is being removed. At this point the change listener can be removed from the row for the old Stock before the new one is added.

row.itemProperty().addListener((obs, previousStock, currentStock) -> {
            if (previousStock != null) {
                previousStock.percentChangeProperty().removeListener(changeListener);
            }
            if (currentStock != null) {
                currentStock.percentChangeProperty().addListener(changeListener);
                //Set the initial state of the pseudo classes
                row.pseudoClassStateChanged(up, currentStock.getPercentChange() > 0);
                row.pseudoClassStateChanged(down, currentStock.getPercentChange() <= 0);
            } else {
                row.pseudoClassStateChanged(up, false);
                row.pseudoClassStateChanged(down, false);
            }
        });
        return row;
    });

The final result is displayed in the table below Rows will change between red and green automatically in real time as the price of the stock fluctuates throughout the trading day.

enter image description here

twitter: @RobTerpilowski

Advertisements

11 thoughts on “Formatting Rows in a JavaFX TableView Using CSS Pseudo Classes

  1. Pingback: Formatting Rows in a JavaFX TableView Using CSS Pseudo Classes | Dinesh Ram Kali.

  2. I don’t understand Why do you the following test ?:
    if (previousStock != null) { previousStock.percentChangeProperty().removeListener(changeListener);
    }

    • Basically what I’m doing is if the item properties for a row had changed, I wanted to remove any listeners from the percentChange property of the previous item that was in that row, since that item may have been removed from the table altogether.

  3. Hi Rob;
    Great Work, i have a problem and you maybe can help me.
    i have a TableView ( JavaFx ) and i want to to use 2 color ( like your example ), but this color is depending of an integer value. ( column in tableview )
    For example:
    Number Name Age
    9 Robert 33
    9 Alex 33
    11 David 45
    11 Peter 56
    21 Joey 58

    Then i want to put color “blue” for rows with number “9”, then i want to put color “red” for number 11, and then i want to put color “blue” for rows with number 21, etc, etc…..

    Do you have an idea for this ? ( i use Java 8 )

    Thanks in advance.
    Aldo.

    • Hi Aldo,

      you should be able to create a custom TableRowFactory which will assign a CSS pseudo class depending on the value of the your “Number” attribute. You would define the styles in the CSS file and then should be able to access them from the Factory. If the values in your table are dynamic and will potentially be updated after the table data has initially been loaded, then you will need to include some change listeners like in the example above to modify the pseudo class that is assigned to a specific row.

      hth,
      -Rob

  4. You, sir, are a miracle worker. A million thanks. I was trying to do this in a very convoluted way. I was trying to bind a Bindings.when().then().otherwise() to the idProperty of the row. This is clean and maintainable.

    Again, thanks so much.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s