3

Is there a way to create "accounting" style cells in JavaFX tables? By accounting I mean having the dollar sign left-aligned and the values right-aligned in the cell. Here is what that looks like in Excel:

Excel Accounting Cell Format

Here is what I tried so far:

public class PriceTableCell<S> extends TableCell<S, Long>
{
    public PriceTableCell()
    {
        final Label label = new Label("$");
        this.setAlignment(Pos.CENTER_RIGHT);
        this.setContentDisplay(ContentDisplay.LEFT);
        this.setGraphic(label);
    }

    @Override
    protected void updateItem(Long item, boolean empty)
    {
        if (item == null || empty)
        {
            this.setText(null);
            return;
        }

        this.setText(String.format(Locale.ENGLISH, "%,d.%02d", item / 100, Math.abs(item % 100)));
    }
}

Unfortunately I did not find a way to set separate alignments for graphic and text. JavaFX renders the above as follows:

JavaFX attempt

Clashsoft
  • 11,553
  • 5
  • 40
  • 79
  • 1
    unrelated to the problem of alignment: as a general rule, never do any manual number/currency formatting, instead use a NumberFormat to make it Locale-independent – kleopatra Feb 01 '18 at 12:31

1 Answers1

4

Using two labels in an AnchorPane should work.

(Update: Following @kleopatra's suggestion, I incorporated a DecimalFormat into this solution, which will (at least partially) localize the currency symbol, as well as the number of decimal digits, etc. This will make the assumption that the currency symbol is displayed to the left of the currency value, which isn't necessarily true for all currencies, but the assumption is somewhat implicit in the question anyway.)

public class PriceTableCell<S> extends TableCell<S, Long> {

    private final AnchorPane pane ;
    private final Label valueLabel ;
    // locale-aware currency format to use for formatting
    private DecimalFormat format;

    public PriceTableCell() {
        // grab an instance
        format = (DecimalFormat) NumberFormat.getCurrencyInstance();
        //get the currency symbol
        String symbol = format.getCurrency().getSymbol();
        // replace the currency symbol with an empty string
        DecimalFormatSymbols symbols = format.getDecimalFormatSymbols();
        symbols.setCurrencySymbol("");
        format.setDecimalFormatSymbols(symbols);

        Label currencySignLabel = new Label(symbol);
        valueLabel = new Label();
        pane = new AnchorPane(currencySignLabel, valueLabel);
        AnchorPane.setLeftAnchor(currencySignLabel, 0.0);
        AnchorPane.setRightAnchor(valueLabel, 0.0);
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    }

    @Override
    protected void updateItem(Long price, boolean empty) {
        super.updateItem(price, empty);
        if (empty) {
            setGraphic(null);
        } else {
            // manual formatting 
            //String text = String.format("%,d.%02d", price / 100, Math.abs(price % 100));
            valueLabel.setText(format.format(price));
            setGraphic(pane);
        }
    }
}

Here is a SSCCE:

import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Random;
import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.LongProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;

public class TableViewWithAccountingStyleCell extends Application {

    public static class PriceTableCell<S> extends TableCell<S, Long> {

        private final AnchorPane pane ;
        private final Label valueLabel ;
        // locale-aware currency format to use for formatting
        private DecimalFormat format;

        public PriceTableCell() {
            // grab an instance
            format = (DecimalFormat) NumberFormat.getCurrencyInstance();
            //get the currency symbol
            String symbol = format.getCurrency().getSymbol();
            // replace the currency symbol with an empty string
            DecimalFormatSymbols symbols = format.getDecimalFormatSymbols();
            symbols.setCurrencySymbol("");
            format.setDecimalFormatSymbols(symbols);

            Label currencySignLabel = new Label(symbol);
            valueLabel = new Label();
            pane = new AnchorPane(currencySignLabel, valueLabel);
            AnchorPane.setLeftAnchor(currencySignLabel, 0.0);
            AnchorPane.setRightAnchor(valueLabel, 0.0);
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        }

        @Override
        protected void updateItem(Long price, boolean empty) {
            super.updateItem(price, empty);
            if (empty) {
                setGraphic(null);
            } else {
                // manual formatting 
                //String text = String.format("%,d.%02d", price / 100, Math.abs(price % 100));
                valueLabel.setText(format.format(price));
                setGraphic(pane);
            }
        }
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();
        private final LongProperty price = new SimpleLongProperty();

        public Item(String name, long price) {
            setName(name);
            setPrice(price);
        }

        public StringProperty nameProperty() {
            return name ;
        }

        public final String getName() {
            return nameProperty().get();
        }

        public final void setName(String name) {
            nameProperty().set(name);
        }

        public LongProperty priceProperty() {
            return price ;
        }

        public final long getPrice() {
            return priceProperty().get();
        }

        public final void setPrice(long price) {
            priceProperty().set(price);
        }
    }

    @Override
    public void start(Stage primaryStage) {
        TableView<Item> table = new TableView<>();
        table.getColumns().add(column("Item", Item::nameProperty));
        TableColumn<Item, Long> priceColumn = column("Price", item -> item.priceProperty().asObject());
        priceColumn.setPrefWidth(300);

        priceColumn.setCellFactory(tc -> new PriceTableCell<>());

        table.getColumns().add(priceColumn);


        Random rng = new Random();
        for (int i = 1 ; i <= 20 ; i++) {
            table.getItems().add(new Item("Item "+i, rng.nextInt(1_000_000)));
        }

        Scene scene = new Scene(table);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private <S,T> TableColumn<S,T> column(String name, Function<S, ObservableValue<T>> property) {
        TableColumn<S,T> column = new TableColumn<>(name);
        column.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
        return column ;
    }

    public static void main(String[] args) {
        launch(args);
    }
}

which produces

enter image description here

James_D
  • 201,275
  • 16
  • 291
  • 322
  • using two labels and DecimalFormat would make it Locale-independent :) – kleopatra Feb 01 '18 at 12:30
  • @kleopatra I was going to go there, then started wondering if you could determine the position of the currency symbol relative to the numeric portion (iirc the symbol for Italian Lira, for example, came after the number), then wondered what Excel did with those kinds of currencies (if they still exist), then I gave up and copied the OP's format.. ;). – James_D Feb 01 '18 at 12:36
  • yeah that got me thinking, too :) See my addition to your answer and take it into yours (I'll delete mine then) – kleopatra Feb 01 '18 at 12:41
  • Wonderful answer, thanks! It's a shame the "$" runs into the value when the cell gets too small. Using a BorderPane instead seems to mitigate that. Thanks again! – Brad Turek Jan 11 '22 at 18:45