Search code examples
javafxtableviewtablecolumn

How do I make a generic TableColumn renderer for JavaFX TableView column


In a javaFX view controller, initialize method, I have a TableView column renderer that shows financial amounts, with each cell being rendered with an amount and in red or blue color depending on whether the amount is a debit or credit amount.

@Override
  public void initialize(URL url, ResourceBundle rb) 
 budgetAmountCol.setCellValueFactory(cellData ->       cellData.getValue().budgetAmountProperty());
// Custom rendering of the budget amount cells in the column.
 budgetAmountCol.setCellFactory((TableColumn<TxnObject, String> column) -> {
return new TableCell<TxnObject, String>() {
    @Override
    protected void updateItem(String item, boolean empty) {
      super.updateItem(item, empty); 
      if (item == null || empty) {
      }
      else {
        double amt = Double.parseDouble(item);
        if (amt == 0) {
          setId("zero-amt");
        } else {
          if (amt < 0){
            item = item.substring(1, item.length());
            setId("debit-amt"); 
          } else {
            setId("credit-amt");               
          }
        }
      }
      setText(item);
    }
  };
});

Because I have several columns that need to be rendered similarily I am trying to avoid copious amounts of source code and turn the above into a single method that can be passed parameters and thus render the columns, something like

private void renderColumn(..) { }

The variables that occupy is table cell in the column are defined in an object definition like this:

public class TxnObject {
      ..
      private final StringProperty budgetAmount;

    ..
    public String getbudgetAmount() {
        double d = Double.parseDouble(budgetAmount.get());
        setbudgetAmount(MainApp.formatAmount(d));
        return budgetAmount.get();
      }

      public void setbudgetAmount(String budgetAmount) {
        this.budgetAmount.set(budgetAmount);
      }

      public StringProperty budgetAmountProperty() {
        return budgetAmount;
      }

The requirement is therefore to pass the content of the second line of the code, i.e.

budgetAmountCol.setCellValueFactory(cellData -> cellData.getValue().budgetAmountProperty());

to render column: So at a minimum the method needs the table column and the variable property:

private void renderColumn(TableColumn<TxnObject, String> tcol, StringProperty sprop) {
    node.setCellValueFactory(..)
}

But I can’t get the correct call to the method. I have tried the following with the indicated result:

renderColumn(budgetAmountCol, budgetAmountProperty());
syntax error: no method BudgetAmountProperty()
renderColumn(budgetAmountCol, cellData.getValue().budgetAmountProperty());
syntax error: Cannot find symbol CellData
renderColumn(budgetAmountCol, cellData ->    cellData.getValue().budgetAmountProperty());
syntax error: StringProperty is not a functional interface

Im finding the syntax and understanding of how to achieve my object rather challenging and would appreciate if I could get some suggestions to try and achieve a solution.


Solution

  • The only way to "pass a method" is by creating a object containing the logic to do this or by using a method reference.

    Furthermore even though JavaFX does not enforce those restrictions, id should be unique. Use pseudoclasses instead.

    You could use

    private static final PseudoClass ZERO = PseudoClass.getPseudoClass("zero-atm");
    private static final PseudoClass CREDIT = PseudoClass.getPseudoClass("credit-atm");
    private static final PseudoClass DEBIT = PseudoClass.getPseudoClass("debit-atm");
    
    public static <S, T> void renderColumn(TableColumn<S, T> column,
            final Callback<? super S, ObservableValue<T>> extractor, final Callback<? super T, String> converter,
            final Comparator<T> comparator, final T zero) {
        if (extractor == null || comparator == null || zero == null) {
            throw new IllegalArgumentException();
        }
    
        column.setCellValueFactory(cd -> extractor.call(cd.getValue()));
        column.setCellFactory(col -> new TableCell<S, T>() {
            @Override
            protected void updateItem(T item, boolean empty) {
                super.updateItem(item, empty);
    
                pseudoClassStateChanged(ZERO, false);
                pseudoClassStateChanged(CREDIT, false);
                pseudoClassStateChanged(DEBIT, false);
    
                if (empty || item == null) {
                    setText("");
                } else {
                    setText(converter.call(item));
                    int comparison = comparator.compare(zero, item);
                    if (comparison == 0) {
                        pseudoClassStateChanged(ZERO, true);
                    } else {
                        pseudoClassStateChanged(comparison < 0 ? CREDIT : DEBIT, true);
                    }
                }
            }
        });
    }
    
    public static <S, T extends Comparable<T>> void renderColumn(TableColumn<S, T> column,
            Callback<? super S, ObservableValue<T>> extractor, Callback<? super T, String> converter, T zero) {
        renderColumn(column, extractor, converter, Comparator.naturalOrder(), zero);
    }
    

    Assuming you change the type of the budget to ObjectProperty<BigDecimal>, the method can be invoked like as shown below. (BigDecimal is a lot easier to work with when it comes to comparing values and doing other mathematical operation in addition to avoiding rounding errors.) Otherwise you could simply hardcode the second type parameter and the other functionality and only keep the first 2 parameters of the method.

    renderColumn(column, TxnObject::budgetAmountProperty, (BigDecimal val) -> val.abs().toString(), BigDecimal.ZERO);