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.