0

I defined a TableView with 4 columns, the first 3 of them should be editable. The fourth column is the mathematical result of the difference of the second (expenses) and third (earnings) one. I managed this so far but when I edit the second or third column the fourth column dont get updatet. I tried different approaches but it didnt work. The problem is that I dont know how to get access to the neighbouring cell.

package application;

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.text.Text;

public class Main extends Application {
    private TableView<Member> table = new TableView<>();
    //private final ObservableList<Member> data = FXCollections.observableArrayList();
    @SuppressWarnings("unchecked")
    @Override

    public void start(Stage primaryStage) {
        try {
            //Definition Layout-Container
            GridPane primarygridpane = new GridPane();
            primarygridpane.setHgap(10);
            primarygridpane.setVgap(10);
            primarygridpane.setPadding(new Insets(5, 5, 5, 5));
            HBox hboxTable = new HBox(10);
            hboxTable.setPadding(new Insets(5, 5, 5, 5));
            Text titel = new Text("Application");
            titel.setFont(Font.font("Arial", FontWeight.BOLD, 28));

            // Spalte Name mit Edit-Funktion
            TableColumn<Member, String> memberColumn = new TableColumn<>("Name");
            memberColumn.setMinWidth(150);
            memberColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("member"));
            memberColumn.setCellFactory(TextFieldTableCell.forTableColumn());
            memberColumn.setOnEditCommit(
                    new EventHandler<CellEditEvent<Member, String>>() {
                        @Override
                        public void handle(CellEditEvent<Member, String> t) {
                            ((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setMember(t.getNewValue());
                        }
                    }
            );

            // Spalte Ausgaben mit Edit-Funktion
            TableColumn<Member, String> expensesColumn = new TableColumn<>("Ausgaben");
            expensesColumn.setMinWidth(50);
            expensesColumn.setCellValueFactory(new PropertyValueFactory<>("expenses"));
            expensesColumn.setCellFactory(TextFieldTableCell.forTableColumn());
            expensesColumn.setOnEditCommit(
                    new EventHandler<CellEditEvent<Member, String>>() {
                        @Override
                        public void handle(CellEditEvent<Member, String> t) {
                            ((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setExpenses(t.getNewValue());
                            //((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setDifference(Double.parseDouble(t.getNewValue()));
                        }
                    }
            );

            // Spalte Pfand mit Edit-Funktion
            TableColumn<Member, String> earningsColumn = new TableColumn<>("Pfand");
            earningsColumn.setMinWidth(50);
            earningsColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("earnings"));
            earningsColumn.setCellFactory(TextFieldTableCell.forTableColumn());
            earningsColumn.setOnEditCommit(
                    new EventHandler<CellEditEvent<Member, String>>() {
                    @Override
                    public void handle(CellEditEvent<Member, String> t) {
                        ((Member) t.getTableView().getItems().get(t.getTablePosition().getRow())).setEarnings(t.getNewValue());
                        }
                    }
            );

            //Spalte Differenz ohne Edit-Funktion
            TableColumn<Member, Double> differenceColumn = new TableColumn<>("Differenz");
            differenceColumn.setMinWidth(50);
            differenceColumn.setCellValueFactory(new PropertyValueFactory<>("difference"));

            //Editier-Leiste
            TextField tfMember = new TextField();
            tfMember.setMinWidth(150);
            tfMember.setPromptText("Name");

            TextField tfExpenses = new TextField();
            tfExpenses.setMinWidth(50);
            tfExpenses.setPromptText("Ausgaben");

            TextField tfEarnings = new TextField();
            tfEarnings.setMinWidth(50);
            tfEarnings.setPromptText("Pfand");

            Button btnAdd = new Button("Hinzufügen");
            Button btnDelete = new Button("Löschen");
            hboxTable.getChildren().addAll(tfMember, tfExpenses, tfEarnings, btnAdd, btnDelete);

            // Spalten der Tabelle hinzufügen und Tabelle editierbar machen
            table.getColumns().addAll(memberColumn, expensesColumn, earningsColumn, differenceColumn);
            table.setEditable(true);
            // table.setItems(data);

            btnAdd.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent e) {
                    try {
                        Member member = new Member();
                        member.setMember(tfMember.getText());
                        member.setExpenses(tfExpenses.getText());
                        member.setEarnings(tfEarnings.getText());
                        member.setDifference(Double.parseDouble(tfExpenses.getText()) - Double.parseDouble(tfEarnings.getText()));
                        table.getItems().add(member);
                        //data.add(member);
                        tfMember.clear();
                        tfExpenses.clear();
                        tfEarnings.clear();
                    } catch (NumberFormatException Exception) {}
                }
            });

            //Elemente dem Gridpane hinzufügen und Rest
            primarygridpane.add(titel, 0, 0, 2, 1);
            primarygridpane.add(table, 0, 2);
            primarygridpane.add(hboxTable, 0, 3);
            Scene scene = new Scene(primarygridpane,450,550);
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {e.printStackTrace();}
    }

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

package application;

import javafx.beans.binding.DoubleBinding;
import javafx.beans.binding.NumberBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

public class Member {
    private SimpleStringProperty member = new SimpleStringProperty();
    private DoubleProperty expenses = new SimpleDoubleProperty();
    private DoubleProperty earnings = new SimpleDoubleProperty();
    private DoubleProperty difference = new SimpleDoubleProperty();

    public Member(String member, Double expenses, Double earnings) {
        this.member = new SimpleStringProperty(member);
        this.expenses = new SimpleDoubleProperty(expenses);
        this.earnings = new SimpleDoubleProperty(earnings);
        // NumberBinding nb = earningsProperty().subtract(expensesProperty());
        // this.difference = new SimpleDoubleProperty(nb);
        // NumberBinding nb2 = expenses.subtract(earnings);

    }

    public String getMember() {
        return member.get();
    }
    public void setMember(String name) {
        member.set(name);
    }
    public final StringProperty MemberProperty(){
        return member;
    }



    public Double getExpenses() {
        return expenses.get();
    }
    public void setExpenses(Double value) {
        expenses.set(value);
    }
    public final DoubleProperty expensesProperty(){
        return expenses;
    }


    public Double getEarnings() {
        return earnings.get();
    }
    public void setEarnings(Double value) {
        earnings.set(value);
    }
    public final DoubleProperty earningsProperty(){
        return earnings;
    }



    public Double getDifference() {
        return difference.get();
    }
    public void setDifference(Double value) {
        difference.set(value);
    }
    public final DoubleProperty differenceProperty(){
        return difference;
    }
}

I would be very thankfull if someone could help me :)

  • 2
    You shouldn't try to access one cell from another; just handle this directly in your `Member` class, so that any time `expenses` or `earnings` change, `difference` also changes. You can do this with JavaFX property bindings, assuming you use JavaFX properties in your model. – James_D Mar 16 '20 at 13:02
  • Thanks for your help. I tried to implement your solution. I dont know how exactly to bind them. Should "difference" also be a Property (private DoubleProperty difference = new SimpleDoubleProperty();) ? And where exactly should I bind them together (e.g. in de constructor or in the implementation of the button)? – Make_Ilanti Mar 16 '20 at 17:00
  • Using a `DoubleProperty` would work. Establish the binding in the `Member` constructor. You should also omit the `setDifference()` method, since setting a bound property throws an exception. (Even better would be to use a `ReadOnlyDoubleWrapper` and only expose a `ReadOnlyDoubleProperty`, but get it working with a plain property first.) Post your `Member` class if you can't get it working. – James_D Mar 16 '20 at 17:12
  • Incidentally, there's also a solution where you omit the `difference` property entirely from your `Member` class, and do `differenceColumn.setCellValueFactory(cellData -> cellData.getValue().earningsProperty().subtract(cellData.getValue().expensesProperty()));` – James_D Mar 16 '20 at 17:14
  • Hi James, thank you for your effort. I tried to implement your second solution with omiting the difference property entirely from my Member-Class. Following error-message pops up: "Type mismatch: cannot convert from DoubleBinding to observable Value. What can I do? – Make_Ilanti Mar 17 '20 at 09:03
  • I now also postet my Member class and tried to implement the first solution but I can't get it working....I tried two different approaches but they didnt work. I commented (//) them. – Make_Ilanti Mar 17 '20 at 09:18
  • doesn't even compile (f.i. there is no setExpenses(String) nor is there a new Member()) - please fix at least the basics before posting code! That said: with proper setup there is no need for commit handlers, tableView will handle all automagically :) – kleopatra Mar 17 '20 at 11:39

1 Answers1

0

You can establish the binding simply by calling bind(...) on the property, in the constructor.

Since the differenceProperty() is bound, calling setDifference() would throw an exception, so you should omit that method1. You can do:

public class Member {
    private StringProperty member = new SimpleStringProperty();
    private DoubleProperty expenses = new SimpleDoubleProperty();
    private DoubleProperty earnings = new SimpleDoubleProperty();
    private DoubleProperty difference = new SimpleDoubleProperty();

    public Member(String member, double expenses, double earnings) {
        this.member = new SimpleStringProperty(member);
        this.expenses = new SimpleDoubleProperty(expenses);
        this.earnings = new SimpleDoubleProperty(earnings);
        this.difference = new SimpleDoubleProperty();
        this.difference.bind(this.earnings.subtract(this.expenses));
    }

    public String getMember() {
        return member.get();
    }
    public void setMember(String name) {
        member.set(name);
    }
    public final StringProperty memberProperty(){
        return member;
    }



    public Double getExpenses() {
        return expenses.get();
    }
    public void setExpenses(Double value) {
        expenses.set(value);
    }
    public final DoubleProperty expensesProperty(){
        return expenses;
    }


    public Double getEarnings() {
        return earnings.get();
    }
    public void setEarnings(Double value) {
        earnings.set(value);
    }
    public final DoubleProperty earningsProperty(){
        return earnings;
    }



    public Double getDifference() {
        return difference.get();
    }
    // public void setDifference(Double value) {
    //     difference.set(value);
    // }
    public final DoubleProperty differenceProperty(){
        return difference;
    }
}

In your table, the earnings and expenses columns represent numeric values, so they should be typed appropriately. For reasons explained at JavaFX Properties in TableView, the type here should be Number, not Double. Also note that if you tell your table cell implementation how to convert from a String to the value (Number) you want to display:

expensesColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));

then you no longer need to provide the onEditCommit handler (the table cell will take care of the update).

So your table configuration simplifies to

// Spalte Name mit Edit-Funktion
TableColumn<Member, String> memberColumn = new TableColumn<>("Name");
memberColumn.setMinWidth(150);
memberColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("member"));
memberColumn.setCellFactory(TextFieldTableCell.forTableColumn());


// Spalte Ausgaben mit Edit-Funktion
TableColumn<Member, Number> expensesColumn = new TableColumn<>("Ausgaben");
expensesColumn.setMinWidth(50);
expensesColumn.setCellValueFactory(new PropertyValueFactory<>("expenses"));
expensesColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));

// Spalte Pfand mit Edit-Funktion
TableColumn<Member, Number> earningsColumn = new TableColumn<>("Pfand");
earningsColumn.setMinWidth(50);
earningsColumn.setCellValueFactory(new PropertyValueFactory<>("earnings"));
earningsColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));

//Spalte Differenz ohne Edit-Funktion
TableColumn<Member, Number> differenceColumn = new TableColumn<>("Differenz");
differenceColumn.setMinWidth(50);
differenceColumn.setCellValueFactory(new PropertyValueFactory<>("difference"));

Note that the NumberStringConverter has a fairly simplistic implementation; you might want to implement your own StringConverter<Number> to support, e.g., locale-based string parsing.

Also note that the model class Member now ensures that difference is always the difference between earnings and expenses, so you do not need to (and can not) set the difference property:

    btnAdd.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent e) {
            try {
                Member member = new Member(tfMember.getText(),
                        Double.parseDouble(tfExpenses.getText()),
                        Double.parseDouble(tfEarnings.getText()));
                table.getItems().add(member);
                tfMember.clear();
                tfExpenses.clear();
                tfEarnings.clear();
            } catch (NumberFormatException Exception) {}
        }
    });

Just as a side note, use of PropertyValueFactory is not really necessary as of Java 8, and because it relies on reflection is prone to failing silently if, e.g., the property name is mistyped (it's also slightly inefficient). You can prefer implementing the callback directly with a lambda expression:

    // memberColumn.setCellValueFactory(new PropertyValueFactory<Member, String>("member"));
    memberColumn.setCellValueFactory(cellData -> cellData.getValue().memberProperty());

    // expensesColumn.setCellValueFactory(new PropertyValueFactory<>("expenses"));
    expensesColumn.setCellValueFactory(cellData -> cellData.getValue().expensesProperty());


    // earningsColumn.setCellValueFactory(new PropertyValueFactory<>("earnings"));
    earningsColumn.setCellValueFactory(cellData -> cellData.getValue().earningsProperty());

This approach also allows you to omit the difference property entirely from the Member class and just use a binding directly in the table column:

    differenceColumn.setCellValueFactory(cd -> 
        cd.getValue().earningsProperty().subtract(cd.getValue().expensesProperty()));

The choice here depends basically on whether you consider the difference an integral part of the data (so it should be included in the model), or whether the data just consist of the earnings and expenses, and the difference is just something you want to visualize in the table.


(1) Really, you should use a ReadOnlyProperty here to represent the difference, since calling differenceProperty().set(...) would also throw an exception with the code the way it's written. Basically:

private final ReadOnlyDoubleWrapper difference = new ReadOnlyDoubleWrapper() ;

// Constructor and other properties as before ...

public final ReadOnlyDoubleProperty differenceProperty() {
     return difference.getReadOnlyProperty();
}

public final double getDifference() {
    return differenceProperty().get();
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thank you James for your comprehensive answer, especially for your advises which I will now note for my future programming! :) – Make_Ilanti Mar 17 '20 at 18:10