javafxtableviewtablecell

JavaFx tablecell linked to more than one property


Hello I am new to JavaFX and when working with tables cells I ran into some issue updating display data. I would like to be able to set up my table cells so that they listen to more than one value without having to initialized listeners in the update item method.

For example I have a bus class that contains three properties a string bus id a string street name and a movement bool. I currently have it setup with the bus id in column 1 and the current street in column 2 and would like to be able to set up such that if the bus is moving the street name is green and if stopped the street name is red. currently I have it set up that the setCellValueFactory for Column 2 is passed the street name property and in the updateItem method for those cells it initializes a listener for the movement bool to update the color. While this current works it is hard to work with should I add more listeners to the cell, can I pass the cell more than one property during the setCellValueFactory method or another such method on the table columns to have the cell call the updateItem method for multiple events.


Solution

  • Given a standard JavaFX model class:

    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    
    public class Bus {
    
        private final StringProperty id = new SimpleStringProperty();
        private final StringProperty streetName = new SimpleStringProperty();
        private final BooleanProperty moving = new SimpleBooleanProperty();
        
        public Bus(String id, String streetName, boolean moving) {
            setId(id);
            setStreetName(streetName);
            setMoving(moving);
        }
        
        
        public final StringProperty idProperty() {
            return this.id;
        }
        
        public final String getId() {
            return this.idProperty().get();
        }
        
        public final void setId(final String id) {
            this.idProperty().set(id);
        }
        
        public final StringProperty streetNameProperty() {
            return this.streetName;
        }
        
        public final String getStreetName() {
            return this.streetNameProperty().get();
        }
        
        public final void setStreetName(final String streetName) {
            this.streetNameProperty().set(streetName);
        }
        
        public final BooleanProperty movingProperty() {
            return this.moving;
        }
        
        public final boolean isMoving() {
            return this.movingProperty().get();
        }
        
        public final void setMoving(final boolean moving) {
            this.movingProperty().set(moving);
        }
        
    }
    

    the usual approach is the one you describe. You can perhaps clean the code up a little by making the type of the column a TableColumn<Bus, Bus> and using bindings instead of listeners:

    import java.util.Random;
    
    import javafx.animation.Animation;
    import javafx.animation.KeyFrame;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    import javafx.beans.property.SimpleObjectProperty;
    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;
    import javafx.util.Duration;
    
    public class App extends Application {
        
        private final String[] streets = {
                "Main Street",
                "Sunset Boulevard",
                "Electric Avenue",
                "Winding Road"
        };
        
        private final Random rng = new Random();
        
        
        @Override
        public void start(Stage stage) {
            TableView<Bus> table = new TableView<>();
            TableColumn<Bus, String> idCol = new TableColumn<>("Id");
            idCol.setCellValueFactory(cellData -> cellData.getValue().idProperty());
            
            TableColumn<Bus,  Bus> streetColumn = new TableColumn<>("Street");
            streetColumn.setCellValueFactory(cellData -> new SimpleObjectProperty<>(cellData.getValue()));
            
            
            
            streetColumn.setCellFactory(tc -> new TableCell<>() {
                @Override
                protected void updateItem(Bus bus, boolean empty) {
                    super.updateItem(bus, empty);
                    
                    textProperty().unbind();
                    styleProperty().unbind();
                    
                    if (empty || bus == null) {
                        setText("");
                        setStyle("");
                    } else {
                        textProperty().bind(bus.streetNameProperty());
                        styleProperty().bind(
                            Bindings.when(bus.movingProperty())
                                .then("-fx-text-fill: green;")
                                .otherwise("-fx-text-fill: red;")
                        );
                    }
                }
            });
    
            table.getColumns().add(idCol);
            table.getColumns().add(streetColumn);
            
    
            // to check it works:
            TableColumn<Bus, Boolean> movingColumn = new TableColumn<>("Moving");
            movingColumn.setCellValueFactory(cellData -> cellData.getValue().movingProperty());
            table.getColumns().add(movingColumn);
    
            for (int busNumber = 1 ; busNumber <= 20 ; busNumber++) {
                table.getItems().add(createBus("Bus Number "+busNumber));
            }
            
            
            Scene scene = new Scene(new BorderPane(table));
            stage.setScene(scene);
            stage.show();
        }
        
        // Create a Bus and a timeline 
        // that makes it start and stop and change streets at random:
        private Bus createBus(String id) {
            String street = streets[rng.nextInt(streets.length)];
            Bus bus = new Bus(id, street, true);
            Timeline timeline = new Timeline(
                    new KeyFrame(Duration.seconds(1 + rng.nextDouble()), 
                        event -> {
                            double choose = rng.nextDouble();
                            if (bus.isMoving() && choose < 0.25) {
                                bus.setStreetName(streets[rng.nextInt(streets.length)]);
                            } else {
                                if (choose < 0.5) {
                                    bus.setMoving(! bus.isMoving());
                                }
                            }
                        }
                    )
            );
            timeline.setCycleCount(Animation.INDEFINITE);
            timeline.play();
            return bus ;
        }
    
        
        public static void main(String[] args) {
            launch();
        }
    
    }
    

    Another approach is to define an immutable class (or record, if you are using Java 15 or later) encapsulating the street and whether or not the bus is moving. Then use a cell value factory that returns a binding which wraps an instance of that class and is bound to both the streetNameProperty and the movingProperty. The cell implementation will then be notified if either change, so no listeners or bindings are needed there:

    import java.util.Random;
    
    import javafx.animation.Animation;
    import javafx.animation.KeyFrame;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.binding.Bindings;
    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;
    import javafx.util.Duration;
    
    public class App extends Application {
    
        private final String[] streets = {
                "Main Street",
                "Sunset Boulevard",
                "Electric Avenue",
                "Winding Road"
        };
    
        private final Random rng = new Random();
    
    
        @Override
        public void start(Stage stage) {
            TableView<Bus> table = new TableView<>();
            TableColumn<Bus, String> idCol = new TableColumn<>("Id");
            idCol.setCellValueFactory(cellData -> cellData.getValue().idProperty());
    
            TableColumn<Bus,  StreetMoving> streetColumn = new TableColumn<>("Street");
            streetColumn.setCellValueFactory(cellData -> {
                Bus bus = cellData.getValue();
                return Bindings.createObjectBinding(
                    () -> new StreetMoving(bus.getStreetName(), bus.isMoving()), 
                    bus.streetNameProperty(), 
                    bus.movingProperty());
            });
    
            streetColumn.setCellFactory(tc -> new TableCell<>() {
                @Override
                protected void updateItem(StreetMoving street, boolean empty) {
                    super.updateItem(street, empty);
                    if (empty || street == null) {
                        setText("");
                        setStyle("");
                    } else {
                        setText(street.street());
                        String color = street.moving() ? "green" : "red" ;
                        setStyle("-fx-text-fill: " + color + ";");
                    }
                }
            });
    
            table.getColumns().add(idCol);
            table.getColumns().add(streetColumn);
    
    
            // to check it works:
            TableColumn<Bus, Boolean> movingColumn = new TableColumn<>("Moving");
            movingColumn.setCellValueFactory(cellData -> cellData.getValue().movingProperty());
            table.getColumns().add(movingColumn);
    
            for (int busNumber = 1 ; busNumber <= 20 ; busNumber++) {
                table.getItems().add(createBus("Bus Number "+busNumber));
            }
    
    
            Scene scene = new Scene(new BorderPane(table));
            stage.setScene(scene);
            stage.show();
        }
    
        private Bus createBus(String id) {
            String street = streets[rng.nextInt(streets.length)];
            Bus bus = new Bus(id, street, true);
            Timeline timeline = new Timeline(
                    new KeyFrame(Duration.seconds(1 + rng.nextDouble()), 
                        event -> {
                            double choose = rng.nextDouble();
                            if (bus.isMoving() && choose < 0.25) {
                                bus.setStreetName(streets[rng.nextInt(streets.length)]);
                            } else {
                                if (choose < 0.5) {
                                    bus.setMoving(! bus.isMoving());
                                }
                            }
                        }
                    )
            );
            timeline.setCycleCount(Animation.INDEFINITE);
            timeline.play();
            return bus ;
        }
    
        public static record StreetMoving(String street, boolean moving) {};
    
    
        public static void main(String[] args) {
            launch();
        }
    
    }
    

    I generally prefer this second approach from a design perspective. Since the streetColumn changes its appearance when either the street or the moving properties change, it should be regarded as a view of both of those properties. Thus it makes sense to define a class representing the entity of which the column is a view; this is the role of the StreetMoving record. This is done externally to the model (Bus) so as not to "pollute" the model with details of the view. You can think of StreetMoving as playing the role of a Data Transfer Object (DTO) between the model and the view. The cell implementation is now very clean, because the updateItem() method receives exactly the data it is supposed to present (the street name and whether or not the bus is moving) and simply has to set graphical properties in response.

    In real life I would probably implement the cell with a custom CSS Pseudoclass and toggle its value depending on the moving value; then delegate the actual choice of color to an external CSS file.