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:
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:
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
Related
Using this example here:
https://stackoverflow.com/a/30509195
Works great to create multiple tables for summation rows. I also needed the scrollbar visible on the bottom table as well. However, the bottom table's scrollbar doesn't sync with the main table (at first with empty content). When there is data, the scrollbar syncs properly.
When you add data to the table, then remove the data, again, scroll bars sync properly. So I know they can still be synced with a table with empty content.
Here is the example code (with two buttons on the top to add and clear items)
package testsummary;
import java.text.Format;
import java.time.LocalDate;
import java.time.Month;
import java.util.Set;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
import javafx.util.Callback;
/**
* Table with a summary table. The summary table is a 2nd table which is
* synchronized with the primary table.
*
* TODO: + always show vertical bars for both the primary and the summary table,
* otherweise the width of both tables wouldn't be the same + hide the
* horizontal scrollbar of the summary table
*
*/
public class SummaryTableDemo extends Application
{
private TableView<Data> mainTable = new TableView<>();
private TableView<SumData> sumTable = new TableView<>();
private final ObservableList<Data> data
= FXCollections.observableArrayList();
// TODO: calculate values
private final ObservableList<SumData> sumData
= FXCollections.observableArrayList(
new SumData("Sum", 0.0, 0.0, 0.0),
new SumData("Min", 0.0, 0.0, 0.0),
new SumData("Max", 0.0, 0.0, 0.0)
);
final HBox hb = new HBox();
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage)
{
Scene scene = new Scene(new Group());
// load css
// scene.getStylesheets().addAll(getClass().getResource("application.css").toExternalForm());
stage.setTitle("Table View Sample");
stage.setWidth(250);
stage.setHeight(550);
// setup table columns
setupMainTableColumns();
setupSumTableColumns();
// fill tables with data
mainTable.setItems(data);
sumTable.setItems(sumData);
// set dimensions
sumTable.setPrefHeight(90);
// bind/sync tables
for (int i = 0; i < mainTable.getColumns().size(); i++)
{
TableColumn<Data, ?> mainColumn = mainTable.getColumns().get(i);
TableColumn<SumData, ?> sumColumn = sumTable.getColumns().get(i);
// sync column widths
sumColumn.prefWidthProperty().bind(mainColumn.widthProperty());
// sync visibility
sumColumn.visibleProperty().bindBidirectional(mainColumn.visibleProperty());
}
// allow changing of column visibility
//mainTable.setTableMenuButtonVisible(true);
// hide header (variation of jewelsea's solution: http://stackoverflow.com/questions/12324464/how-to-javafx-hide-background-header-of-a-tableview)
sumTable.getStyleClass().add("tableview-header-hidden");
// hide horizontal scrollbar via styles
// sumTable.getStyleClass().add("sumtable");
// create container
BorderPane bp = new BorderPane();
Button addButton = new Button("+");
Button clearButton = new Button("X");
addButton.setOnAction((ActionEvent c) ->
{
data.add(new Data(LocalDate.of(2015, Month.JANUARY, 11), 40.0, 50.0, 60.0));
});
clearButton.setOnAction((ActionEvent c) ->
{
data.clear();
});
HBox buttonBar = new HBox(clearButton, addButton);
bp.setTop(buttonBar);
bp.setCenter(mainTable);
bp.setBottom(sumTable);
// fit content
bp.prefWidthProperty().bind(scene.widthProperty());
bp.prefHeightProperty().bind(scene.heightProperty());
((Group) scene.getRoot()).getChildren().addAll(bp);
stage.setScene(scene);
stage.show();
// synchronize scrollbars (must happen after table was made visible)
ScrollBar mainTableHorizontalScrollBar = findScrollBar(mainTable, Orientation.HORIZONTAL);
ScrollBar sumTableHorizontalScrollBar = findScrollBar(sumTable, Orientation.HORIZONTAL);
mainTableHorizontalScrollBar.valueProperty().bindBidirectional(sumTableHorizontalScrollBar.valueProperty());
}
/**
* Primary table column mapping.
*/
private void setupMainTableColumns()
{
TableColumn<Data, LocalDate> dateCol = new TableColumn<>("Date");
dateCol.setPrefWidth(120);
dateCol.setCellValueFactory(new PropertyValueFactory<>("date"));
TableColumn<Data, Double> value1Col = new TableColumn<>("Value 1");
value1Col.setPrefWidth(90);
value1Col.setCellValueFactory(new PropertyValueFactory<>("value1"));
value1Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<Data, Double> value2Col = new TableColumn<>("Value 2");
value2Col.setPrefWidth(90);
value2Col.setCellValueFactory(new PropertyValueFactory<>("value2"));
value2Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<Data, Double> value3Col = new TableColumn<>("Value 3");
value3Col.setPrefWidth(90);
value3Col.setCellValueFactory(new PropertyValueFactory<>("value3"));
value3Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
mainTable.getColumns().addAll(dateCol, value1Col, value2Col, value3Col);
}
/**
* Summary table column mapping.
*/
private void setupSumTableColumns()
{
TableColumn<SumData, String> textCol = new TableColumn<>("Text");
textCol.setCellValueFactory(new PropertyValueFactory<>("text"));
TableColumn<SumData, Double> value1Col = new TableColumn<>("Value 1");
value1Col.setCellValueFactory(new PropertyValueFactory<>("value1"));
value1Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<SumData, Double> value2Col = new TableColumn<>("Value 2");
value2Col.setCellValueFactory(new PropertyValueFactory<>("value2"));
value2Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
TableColumn<SumData, Double> value3Col = new TableColumn<>("Value 3");
value3Col.setCellValueFactory(new PropertyValueFactory<>("value3"));
value3Col.setCellFactory(new FormattedTableCellFactory<>(TextAlignment.RIGHT));
sumTable.getColumns().addAll(textCol, value1Col, value2Col, value3Col);
}
/**
* Find the horizontal scrollbar of the given table.
*
* #param table
* #return
*/
private ScrollBar findScrollBar(TableView<?> table, Orientation orientation)
{
// this would be the preferred solution, but it doesn't work. it always gives back the vertical scrollbar
// return (ScrollBar) table.lookup(".scroll-bar:horizontal");
//
// => we have to search all scrollbars and return the one with the proper orientation
Set<Node> set = table.lookupAll(".scroll-bar");
for (Node node : set)
{
ScrollBar bar = (ScrollBar) node;
if (bar.getOrientation() == orientation)
{
return bar;
}
}
return null;
}
/**
* Data for primary table rows.
*/
public static class Data
{
private final ObjectProperty<LocalDate> date;
private final SimpleDoubleProperty value1;
private final SimpleDoubleProperty value2;
private final SimpleDoubleProperty value3;
public Data(LocalDate date, double value1, double value2, double value3)
{
this.date = new SimpleObjectProperty<LocalDate>(date);
this.value1 = new SimpleDoubleProperty(value1);
this.value2 = new SimpleDoubleProperty(value2);
this.value3 = new SimpleDoubleProperty(value3);
}
public final ObjectProperty<LocalDate> dateProperty()
{
return this.date;
}
public final LocalDate getDate()
{
return this.dateProperty().get();
}
public final void setDate(final LocalDate date)
{
this.dateProperty().set(date);
}
public final SimpleDoubleProperty value1Property()
{
return this.value1;
}
public final double getValue1()
{
return this.value1Property().get();
}
public final void setValue1(final double value1)
{
this.value1Property().set(value1);
}
public final SimpleDoubleProperty value2Property()
{
return this.value2;
}
public final double getValue2()
{
return this.value2Property().get();
}
public final void setValue2(final double value2)
{
this.value2Property().set(value2);
}
public final SimpleDoubleProperty value3Property()
{
return this.value3;
}
public final double getValue3()
{
return this.value3Property().get();
}
public final void setValue3(final double value3)
{
this.value3Property().set(value3);
}
}
/**
* Data for summary table rows.
*/
public static class SumData
{
private final SimpleStringProperty text;
private final SimpleDoubleProperty value1;
private final SimpleDoubleProperty value2;
private final SimpleDoubleProperty value3;
public SumData(String text, double value1, double value2, double value3)
{
this.text = new SimpleStringProperty(text);
this.value1 = new SimpleDoubleProperty(value1);
this.value2 = new SimpleDoubleProperty(value2);
this.value3 = new SimpleDoubleProperty(value3);
}
public final SimpleStringProperty textProperty()
{
return this.text;
}
public final java.lang.String getText()
{
return this.textProperty().get();
}
public final void setText(final java.lang.String text)
{
this.textProperty().set(text);
}
public final SimpleDoubleProperty value1Property()
{
return this.value1;
}
public final double getValue1()
{
return this.value1Property().get();
}
public final void setValue1(final double value1)
{
this.value1Property().set(value1);
}
public final SimpleDoubleProperty value2Property()
{
return this.value2;
}
public final double getValue2()
{
return this.value2Property().get();
}
public final void setValue2(final double value2)
{
this.value2Property().set(value2);
}
public final SimpleDoubleProperty value3Property()
{
return this.value3;
}
public final double getValue3()
{
return this.value3Property().get();
}
public final void setValue3(final double value3)
{
this.value3Property().set(value3);
}
}
/**
* Formatter for table cells: allows you to align table cell values
* left/right/center
*
* Example for alignment form
* http://docs.oracle.com/javafx/2/fxml_get_started/fxml_tutorial_intermediate.htm
*
* #param <S>
* #param <T>
*/
public static class FormattedTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>>
{
private TextAlignment alignment = TextAlignment.LEFT;
private Format format;
public FormattedTableCellFactory()
{
}
public FormattedTableCellFactory(TextAlignment alignment)
{
this.alignment = alignment;
}
public TextAlignment getAlignment()
{
return alignment;
}
public void setAlignment(TextAlignment alignment)
{
this.alignment = alignment;
}
public Format getFormat()
{
return format;
}
public void setFormat(Format format)
{
this.format = format;
}
#Override
#SuppressWarnings("unchecked")
public TableCell<S, T> call(TableColumn<S, T> p)
{
TableCell<S, T> cell = new TableCell<S, T>()
{
#Override
public void updateItem(Object item, boolean empty)
{
if (item == getItem())
{
return;
}
super.updateItem((T) item, empty);
if (item == null)
{
super.setText(null);
super.setGraphic(null);
} else if (format != null)
{
super.setText(format.format(item));
} else if (item instanceof Node)
{
super.setText(null);
super.setGraphic((Node) item);
} else
{
super.setText(item.toString());
super.setGraphic(null);
}
}
};
cell.setTextAlignment(alignment);
switch (alignment)
{
case CENTER:
cell.setAlignment(Pos.CENTER);
break;
case RIGHT:
cell.setAlignment(Pos.CENTER_RIGHT);
break;
default:
cell.setAlignment(Pos.CENTER_LEFT);
break;
}
return cell;
}
}
}
No solution (which probably would require some real work in the bowels of VirtualFlow and/or TableViewSkin) but a dirty trick: add/remove data after wiring the scrollBars
addButton.fire();
Platform.runLater(( ) -> {
clearButton.fire();
});
The drawback is a short but perceptible flicker ...
Update
After a bit of digging ("geht-nicht-gibt's-nicht" - don't know the English idiom, sry) I found a way to force the VirtualFlow into an initial layout pass even if there are no items: the basic idea is to temporarily set the flow's cellCount > 0 even if there are no items. The tricky part is to do it at the right time in the skin's life: only once, sometime early but only after the normal layout has happened.
The implementation below
has a flag to indicate whether or not to fake the itemCount
sets the flag in a listener to the containing window's showing property: this assumes a normal setting of the default skin while being added to the scenegraph
overridden getItemCount to return at least 1 if the flag is set
overridden layoutChildren that forces a fake layout if the flag is set
the forced layout is achieved by calling updateItemCount twice: once with and once without flag
Still dirty, but more fun :)
public static class TweakedTableSkin<T> extends TableViewSkin<T> {
private boolean forceNotEmpty = false;
ChangeListener showingListener = (src, ov, nv) -> {
initForceNotEmpty(src);
};
public TweakedTableSkin(TableView<T> control) {
super(control);
Window window = getSkinnable().getScene().getWindow();
if (window != null)
window.showingProperty().addListener(showingListener);
}
/**
* Overridden to force a re-layout with faked itemCount after calling
* super if the fake flag is true.
*/
#Override
protected void layoutChildren(double x, double y, double w, double h) {
super.layoutChildren(x, y, w, h);
if (forceNotEmpty) {
forceNotEmptyLayout();
}
}
/**
* Callback from listener installed on window's showing property.
* Implemented to set the forceNotEmpty flag and remove the listener.
*/
private void initForceNotEmpty(ObservableValue src) {
forceNotEmpty = true;
src.removeListener(showingListener);
}
/**
* Enforces a layout pass on the flow with at least one row.
* Resets the forceNotEmpty flag and triggers a second
* layout pass with the correct count.
*/
private void forceNotEmptyLayout() {
if (!forceNotEmpty) return;
updateItemCount();
forceNotEmpty = false;
updateItemCount();
}
/**
* Overridden to return at least 1 if forceNotEmpty is true.
*/
#Override
protected int getItemCount() {
int itemCount = super.getItemCount();
if (forceNotEmpty && itemCount == 0) {
itemCount = 1;
}
return itemCount;
}
}
Usage by extending TableView with an overridden createDefaultSkin:
private TableView<Data> mainTable = new TableView<>() {
#Override
protected Skin<?> createDefaultSkin() {
return new TweakedTableSkin<>(this);
}
};
I've a JFXTreeTableView and i want to center the text of the data for each column.
there is one of my creating columns code :
JFXTreeTableColumn<TableData, String> DrinkColumn = new JFXTreeTableColumn<>("Drink");
DrinkColumn.setPrefWidth(100);
DrinkColumn.setCellValueFactory(new Callback<TreeTableColumn.CellDataFeatures<TableData, String>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(TreeTableColumn.CellDataFeatures<TableData, String> param) {
return param.getValue().getValue().Drink;
}
}
);
I don't use JFoenix, but using a standard TreeTableView, the following external CSS will center the text in tree table cells:
.tree-table-cell {
-fx-alignment: center ;
}
Here's a SSCCE (the code above goes in style.css):
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewTest extends Application {
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<>();
TreeTableColumn<Item, String> col = new TreeTableColumn<>("Item");
col.setCellValueFactory(cellData -> cellData.getValue().getValue().nameProperty());
col.setPrefWidth(250);
table.getColumns().add(col);
TreeTableColumn<Item, Number> valueCol = new TreeTableColumn<>("Value");
valueCol.setCellValueFactory(cellData -> cellData.getValue().getValue().valueProperty());
valueCol.setPrefWidth(150);
table.getColumns().add(valueCol);
table.setRoot(createRandomTree(50));
Scene scene = new Scene(table);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TreeItem<Item> createRandomTree(int nItems) {
Random rng = new Random();
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", rng.nextInt(1000)));
root.setExpanded(true);
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= nItems ; i++) {
TreeItem<Item> item = new TreeItem<>(new Item("Item "+i, rng.nextInt(1000)));
item.setExpanded(true);
items.get(rng.nextInt(items.size())).getChildren().add(item);
items.add(item);
}
return root ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
If you want to center only specific columns, then use a cell factory on the column and set a CSS class or PseudoClass on the cell:
valueCol.setCellFactory(column -> {
TreeTableCell<Item, Number> cell = new TreeTableCell<Item, Number>() {
#Override
protected void updateItem(Number value, boolean empty) {
super.updateItem(value, empty);
if (empty) {
setText(null);
} else {
setText(value.toString());
}
}
};
cell.pseudoClassStateChanged(PseudoClass.getPseudoClass("centered"), true);
return cell ;
});
and modify the CSS accordingly:
.tree-table-cell:centered {
-fx-alignment: center ;
}
The latter version gives
I'm stuck with trying to format Long values in a TableView with JavaFX.
I have following class to store the rows that I want to display on the table:
import java.text.DecimalFormat;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleLongProperty;
import javafx.beans.property.SimpleStringProperty;
public class DataByCurrencyPairRow {
private DecimalFormat integerFormat = new DecimalFormat("#,###");
private SimpleStringProperty currencyPair = new SimpleStringProperty("");
private SimpleDoubleProperty shareOfTotalVolume = new SimpleDoubleProperty(0);
private SimpleLongProperty totalVolume = new SimpleLongProperty(0);
private SimpleLongProperty currencyBought = new SimpleLongProperty(0);
private SimpleLongProperty currencySold = new SimpleLongProperty(0);
private SimpleLongProperty monthlyAverage = new SimpleLongProperty(0);
public DataByCurrencyPairRow() {
currencyPair.set("");
shareOfTotalVolume.set(0);
totalVolume.set(0);
currencyBought.set(0);
currencySold.set(0);
monthlyAverage.set(0);
}
public String getCurrencyPair() {
return currencyPair.getValue();
}
public void setCurrencyPair(String currencyPair) {
this.currencyPair.setValue(currencyPair);
}
public Long getMonthlyAverage() {
return monthlyAverage.getValue();
}
public void setMonthlyAverage(Long monthlyAverage) {
this.monthlyAverage.setValue(monthlyAverage);
}
public Long getCurrencySold() {
return currencySold.getValue();
}
public void setCurrencySold(Long currencySold) {
this.currencySold.setValue(currencySold);
}
public Long getCurrencyBought() {
return currencyBought.getValue();
}
public void setCurrencyBought(Long currencyBought) {
this.currencyBought.setValue(currencyBought);
}
public Long getTotalVolume() {
return totalVolume.getValue();
}
public void setTotalVolume(Long totalVolume) {
this.totalVolume.setValue(totalVolume);
}
public Double getShareOfTotalVolume() {
return shareOfTotalVolume.getValue();
}
public void setShareOfTotalVolume(Double shareOfTotalVolume) {
this.shareOfTotalVolume.setValue(shareOfTotalVolume);
}
}
Then I have the controller with initialize method where I have been trying to override the updateItem method to get the table to show comma as a thousand separator:
public class MainController {
private static final String DEFAULT_TIME_HORIZON = new String("0");
private final NumberFormat integerFormat = new DecimalFormat("#,###");
#FXML
TableView<DataByCurrencyPairRow> tableTransactionsByCurrencyPair;
#FXML
TableColumn<DataByCurrencyPairRow, Long> columnTotal;
#FXML
void initialize() {
columnTotal.setCellFactory(
new Callback<TableColumn<DataByCurrencyPairRow, SimpleLongProperty>, TableCell<DataByCurrencyPairRow, SimpleLongProperty>>() {
#Override
public TableCell<DataByCurrencyPairRow, SimpleLongProperty> call(TableColumn<DataByCurrencyPairRow, SimpleLongProperty> param
) {
return new TableCell<DataByCurrencyPairRow, SimpleLongProperty>() {
#Override
protected void updateItem(SimpleLongProperty item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText("0");
setStyle("");
} else {
setText(integerFormat.format(item.longValue()));
}
}
};
}
}
);
And this is the method that populates the TableView:
public void updateByCurrencyPairTable() {
System.out.println("#MainController: Updating data in table view Markets volumes by currency pair");
ObservableList<DataByCurrencyPairRow> data = tableTransactionsByCurrencyPair.getItems();
data.clear();
// Add row items to the table view Markets volume by currency
for (DataByCurrencyPairRow row : customer.getDataByCurrencyPairR12m().getDataByCurrencyPair()) {
data.add(row);
}
}
Please help me by showing how to do this!! I also tried to override the updateItem method as Long instead of SimpleLongProperty and my IDE accepted the code but still the number is not formatted in the table.
Thank you guys in advance!!!
LongProperty implements ObservableValue<Number>, not ObservableValue<Long> (or ObservableValue<SimpleLongProperty>). So your table columns need to be of type TableColumn<DataByCurrencyPair, Number> and your cell factory needs to match those types accordingly.
Here's a simple example of a formatted column with Longs:
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.util.Random;
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.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.stage.Stage;
public class TableWithFormattedLong extends Application {
private final NumberFormat integerFormat = new DecimalFormat("#,###");
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemColumn = new TableColumn<>("Item");
itemColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());
TableColumn<Item, Number> valueColumn = new TableColumn<>("Value");
valueColumn.setCellValueFactory(cellData -> cellData.getValue().valueProperty());
valueColumn.setCellFactory(tc -> new TableCell<Item, Number>() {
#Override
protected void updateItem(Number value, boolean empty) {
super.updateItem(value, empty);
if (value == null || empty) {
setText("");
} else {
setText(integerFormat.format(value));
}
}
});
table.getColumns().add(itemColumn);
table.getColumns().add(valueColumn);
Random rng = new Random();
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextLong()));
}
primaryStage.setScene(new Scene(table, 600, 600));
primaryStage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final LongProperty value = new SimpleLongProperty();
public Item(String name, long value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final LongProperty valueProperty() {
return this.value;
}
public final long getValue() {
return this.valueProperty().get();
}
public final void setValue(final long value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
There is no need to set the Cellfactory, just set the CellValueFactory.
TableColumn<DataByCurrencyPairRow, String> columnTotal;
columnTotal.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<DataByCurrencyPairRow,String>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(CellDataFeatures<DataByCurrencyPairRow, String> param) {
DataByCurrencyPairRow value = param.getValue();
return new ReadOnlyStringWrapper(NumberFormat.getNumberInstance(Locale.US).format(123123)); //replace the number with the calculated total
}
});
My users can create jobs that are added to a MySQL database. The jobs have a priority (1, 2 or 3). What I would like to do is modify the colour of individual rows based on the priority of the job, for example prioirty 3 is a red row as this is a more urgent job, priority 1 is a green row as it has a lower urgency.
I have a job model class that has a getter/setter for priority;
public int getPrioritySetting() {
return prioritySetting;
}
public void setPrioritySetting(final int prioritySetting) {
this.prioritySetting = prioritySetting;
}
I have two questions, what is the "easiest" way to get the priority of each inidivual job from the MySQL database and (using this), what is the "easiest" way to modify the appearance of the row? I'm currently using TableView in JavaFX with FXML files built through scenebuilder.
I don't understand the first question: presumably you are getting the Job objects from the database at some point anyway, so you would just populate the prioritySetting field when you do so.
To change the appearance of the row, use a row factory, and set some CSS pseudoclasses
PseudoClass highPriority = PseudoClass.getPseudoClass("high-priority");
PseudoClass lowPriority = PseudoClass.getPseudoClass("low-priority");
table.setRowFactory(tv -> new TableRow<Job>() {
#Override
public void updateItem(Job item, boolean empty) {
super.updateItem(item, empty);
pseudoClassStateChanged(highPriority, item != null && item.getPrioritySetting() == 3);
pseudoClassStateChanged(lowPriority, item != null && item.getPrioritySetting() == 1);
}
});
Then just define whatever style you need in an external CSS file:
.table-row-cell:high-priority {
-fx-background: red ;
}
.table-row-cell:low-priority {
-fx-background: green ;
}
Here is an SSCCE
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewWithPriorityRowColor extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Job> table = new TableView<>();
table.getColumns().add(column("Name", Job::nameProperty));
table.getColumns().add(column("Value", Job::valueProperty));
table.getColumns().add(column("Priority", Job::priorityProperty));
PseudoClass highPriority = PseudoClass.getPseudoClass("high-priority");
PseudoClass lowPriority = PseudoClass.getPseudoClass("low-priority");
table.setRowFactory(tv -> new TableRow<Job>(){
#Override
public void updateItem(Job job, boolean empty) {
super.updateItem(job, empty);
pseudoClassStateChanged(highPriority, job != null && job.getPriority() == 3);
pseudoClassStateChanged(lowPriority, job != null && job.getPriority() == 1);
}
});
table.getItems().addAll(createJobs());
Scene scene = new Scene(new BorderPane(table), 800, 600);
scene.getStylesheets().add("table-view-with-priority.css");
primaryStage.setScene(scene);
primaryStage.show();
}
public List<Job> createJobs() {
Random rng = new Random();
return IntStream.rangeClosed(1, 40)
.mapToObj(i -> new Job("Job "+i, i, rng.nextInt(3) + 1))
.collect(Collectors.toList());
}
public static <S,T> TableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class Job {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
private final IntegerProperty priority = new SimpleIntegerProperty();
public Job(String name, int value, int priority) {
setName(name);
setValue(value);
setPriority(priority);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
public final IntegerProperty priorityProperty() {
return this.priority;
}
public final int getPriority() {
return this.priorityProperty().get();
}
public final void setPriority(final int priority) {
this.priorityProperty().set(priority);
}
}
public static void main(String[] args) {
launch(args);
}
}
with the CSS code shown above in the file table-view-with-priority.css.
So I'm developing a "to-do list" application where there is a TableView containing the tasks to do and in my TableView there is a "deadline" column. I would like that if the deadline date of a task has been passed it makes that specific deadlines date red.
So if the user opens the app on the first of January/2015 and one of his tasks deadline was the 31st of December/2014, its corresponding deadline cell has its text colored in red.
The table is obviously associated to an ObservableList of "Task" objects which have an ObjectProperty "deadline" field.
How would I go about doing this?
Define a CSS PseudoClass to represent an overdue item. Use a cell factory that sets the pseudoclass state according to whether or not the deadline has passed. You didn't post any code, but the following should help:
TableColumn<Task, LocalDate> deadlineColumn = new TableColumn<>("Deadline");
deadlineColumn.setCellValueFactory( cellData -> cellData.getValue().deadlineProperty() ); // or similar...
PseudoClass overdue = PseudoClass.getPseudoClass("overdue");
deadlineColumn.setCellFactory(col -> new TableCell<Task, LocalDate>() {
#Override
public void updateItem(LocalDate deadline, boolean empty) {
super.updateItem(deadline, empty) ;
if (empty) {
pseudoClassStateChanged(overdue, false);
setText(null);
} else {
pseudoClassStateChanged(overdue, LocalDate.now().isAfter(deadline));
setText(deadline.toString());
}
}
});
And then in an external css file you can do
.table-cell:overdue {
-fx-text-fill: red ;
}
Update: here's a complete example, with the CSS shown above in a file called overdue.css:
import java.time.LocalDate;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ToDoTable extends Application {
#Override
public void start(Stage primaryStage) {
TableView<ToDoItem> table = new TableView<>();
table.getColumns().add(createColumn("Name", ToDoItem::nameProperty));
TableColumn<ToDoItem, LocalDate> deadlineCol = createColumn("Deadline", ToDoItem::deadlineProperty);
PseudoClass overdue = PseudoClass.getPseudoClass("overdue");
deadlineCol.setCellFactory(col -> new TableCell<ToDoItem, LocalDate>() {
#Override
public void updateItem(LocalDate deadline, boolean empty) {
super.updateItem(deadline, empty);
if (empty) {
pseudoClassStateChanged(overdue, false);
setText(null);
} else {
pseudoClassStateChanged(overdue, LocalDate.now().isAfter(deadline));
setText(deadline.toString());
}
}
});
table.getColumns().add(deadlineCol);
for (int i=1; i <= 10; i++) {
LocalDate deadline = LocalDate.now().plusDays(i - 5);
ToDoItem item = new ToDoItem("Item "+i, deadline);
table.getItems().add(item);
}
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root, 800, 600);
scene.getStylesheets().add("overdue.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S,T> TableColumn<S,T> createColumn(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col ;
}
public static class ToDoItem {
private final StringProperty name = new SimpleStringProperty();
private final ObjectProperty<LocalDate> deadline = new SimpleObjectProperty<>();
public ToDoItem(String name, LocalDate deadline) {
this.name.set(name);
this.deadline.set(deadline);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final ObjectProperty<LocalDate> deadlineProperty() {
return this.deadline;
}
public final java.time.LocalDate getDeadline() {
return this.deadlineProperty().get();
}
public final void setDeadline(final java.time.LocalDate deadline) {
this.deadlineProperty().set(deadline);
}
}
public static void main(String[] args) {
launch(args);
}
}