首页 > 解决方案 > JavaFx 表格单元链接到多个属性

问题描述

您好,我是 JavaFX 新手,在使用表格单元格时,我在更新显示数据时遇到了一些问题。我希望能够设置我的表格单元格,以便它们侦听多个值,而不必在更新项方法中初始化侦听器。

例如,我有一个包含三个属性的总线类,一个字符串总线 ID,一个字符串街道名称和一个运动布尔值。我目前将其设置为第 1 列中的公共汽车 ID 和第 2 列中的当前街道,并且希望能够设置这样,如果公共汽车正在移动,则街道名称为绿色,如果停止,则街道名称为红色。目前我已经设置为第 2 列的 setCellValueFactory 传递街道名称属性,并在这些单元格的 updateItem 方法中初始化移动布尔的侦听器以更新颜色。虽然这个当前工作很难使用,但如果我向单元格添加更多侦听器,我可以在 setCellValueFactory 方法期间传递单元格多个属性,或者在表格列上传递另一个这样的方法,以使单元格调用 updateItem 方法多个事件。

标签: javafxtableviewtablecell

解决方案


给定一个标准的 JavaFX 模型类:

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);
    }
    
}

通常的方法是您描述的方法。TableColumn<Bus, Bus>您也许可以通过将列的类型设置为 a并使用绑定而不是侦听器来稍微清理代码:

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();
    }

}

另一种方法是定义一个不可变类(或者record,如果您使用的是 Java 15 或更高版本)封装街道以及公共汽车是否在移动。然后使用一个单元格值工厂,它返回一个绑定,该绑定包装了该类的一个实例并绑定到streetNamePropertymovingProperty。如果其中任何一个发生更改,将通知单元实现,因此那里不需要侦听器或绑定:

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();
    }

}

从设计的角度来看,我通常更喜欢第二种方法。由于streetColumn街道或移动属性发生变化时其外观也会发生变化,因此应将其视为这两个属性的视图。因此,定义一个表示列是视图的实体的类是有意义的;这就是StreetMoving记录的作用。这是在模型(Bus)外部完成的,以免用视图的细节“污染”模型。您可以将其StreetMoving视为在模型和视图之间扮演数据传输对象 (DTO) 的角色。单元实现现在非常干净,因为该updateItem()方法准确地接收它应该呈现的数据(街道名称以及公共汽车是否在移动)并且只需设置图形属性作为响应。

在现实生活中,我可能会使用自定义 CSS 实现单元格Pseudoclass并根据值切换其moving值;然后将颜色的实际选择委托给外部 CSS 文件。


推荐阅读