JavaFX:具有自定义单元格工厂的 ComboBox - 错误渲染

JavaFX: ComboBox with custom cell factory - buggy rendering

我有一个带有自定义单元工厂的 ComboBox,但渲染有问题,我不知道我做错了什么。

ComboBox的数据是基于枚举的,我们称它为MyEnum。所以我的 ComboBox 定义为:

@FXML
private ComboBox<MyEnum> comboBox;

我是这样设置的:

comboBox.getItems().addAll(MyEnum.values());
comboBox.setButtonCell(new CustomRenderer());
comboBox.setCellFactory(cell -> new CustomRenderer());
comboBox.getSelectionModel().select(0);

我的 CustomRenderer 看起来像这样:

public class CustomRenderer extends ListCell<MyEnum> {

    @Override
    protected void updateItem(MyEnum enumValue, boolean empty) {
        super.updateItem(enumValue, empty);

        if (enumValue== null || empty) {
            setGraphic(null);
            setText(null);
        } else {
            setGraphic(enumValue.getGraphic());
        }
    }
}

MyEnumgetGraphic() 方法 returns 一个 HBox 可以包含任意数量的元素,但在我的特定情况下它只是一个 ImageView:

public enum MyEnum{

       ONE(new ImageView(new Image(MyApp.class.getResourceAsStream("image1.png"),
            15,
            15,
            true,
            true))),

       TWO(new ImageView(new Image(MyApp.class.getResourceAsStream("image2.png"),
            15,
            15,
            true,
            true)));

    private final HBox graphic;

    MyEnum(Node... graphicContents) {
        graphic = new HBox(graphicContents);
        graphic.setSpacing(5);
    }

    public HBox getGraphic() {
        return graphic;
    }
}

现在,当我启动应用程序时,第一个错误是 ComboBox 没有显示任何 selected,即使我在初始化中有 comboBox.getSelectionModel().select(0)

当我点击它时,下拉菜单是正确的,并显示了我的两个条目及其图像:

当我 select 其中一个条目时,一切似乎仍然很好:

但是当我再次打开下拉菜单时,它看起来像这样:

突然 selected 图片从下拉列表中消失了。

在我select另一个仍然显示图标的条目后,重新打开下拉菜单,然后两个图像都消失了。图片仍然显示在 ButtonCell 中,只是不在下拉列表中。

我一开始以为它可能与 ImageViews 有关系,但是当我用其他节点替换它们时,比如 Labels,它仍然是相同的 2 个错误:

  1. 应用程序启动时select未显示任何内容
  2. 我在下拉框中单击的所有内容都会从下拉列表中消失

如果需要可运行的示例,请告诉我。但也许有人已经可以从给定的代码中发现我的错误。

感谢

你的问题是一个节点不能多次出现在场景图中。

来自node documentation

A node may occur at most once anywhere in the scene graph. Specifically, a node must appear no more than once in all of the following: as the root node of a Scene, the children ObservableList of a Parent, or as the clip of a Node.

If a program adds a child node to a Parent (including Group, Region, etc) and that node is already a child of a different Parent or the root of a Scene, the node is automatically (and silently) removed from its former parent.

您试图在列表选择按钮和选择列表本身中重复使用相同的节点,这是不允许的。

此外,正如 kleopatra 在评论中指出的那样:“将节点用作数据是错误的”。这样做的一个原因(除其他外)是,如果您希望同一数据的多个视图同时可见,您将无法做到,因为与数据相关的节点只能附加到任何给定时间在一个地方的场景。

我特别建议不要将节点放在枚举中。在我看来,这真的不是枚举的目的。

您需要做的是将模型与视图分开。细胞工厂正在创建作为模型的枚举的变化值的视图。此视图只需为单元格创建一次,并且需要在单元格值更改时更新(通过为视图提供适当的图像)。

示例代码

你可以在示例中看到,因为我们创建了同一个怪物(龙)的两个独立视图,所以该示例能够同时渲染两个视图。这是因为视图是完全不同的节点,它们为相同的基础数据(枚举值及其关联图像)提供视图。

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

public class ChooseYourDoomApp extends Application {

    public static final String CSS = "data:text/css," + // language=CSS
            """
            .root {
                -fx-background-color: lightblue;
                -fx-base: palegreen;
            }
            """;

    @Override
    public void start(Stage stage) throws Exception {
        ComboBox<Monster> choiceOfDoom = new ComboBox<>(
                FXCollections.observableArrayList(
                        Monster.values()
                )
        );

        choiceOfDoom.setButtonCell(new MonsterCell());
        choiceOfDoom.setCellFactory(listView -> new MonsterCell());
        choiceOfDoom.getSelectionModel().select(Monster.Dragon);

        StackPane layout = new StackPane(choiceOfDoom);
        layout.setPadding(new Insets(20));

        Scene scene = new Scene(layout);
        scene.getStylesheets().add(CSS);

        stage.setScene(scene);
        stage.show();
    }

    public class MonsterCell extends ListCell<Monster> {
        private ImageView imageView = new ImageView();

        @Override
        protected void updateItem(Monster item, boolean empty) {
            super.updateItem(item, empty);

            if (item == null || empty) {
                imageView.setImage(null);
                setGraphic(null);
            } else {
                imageView.setImage(monsterImages.get(item));
                setGraphic(imageView);
            }
        }
    }

    public enum Monster {
        Medusa,
        Dragon,
        Treant,
        Unicorn
    }

    private Map<Monster, Image> monsterImages = createMonsterImages();

    private Map<Monster, Image> createMonsterImages() {
        Map<Monster, Image> monsterImages = new HashMap<>();

        for (Monster monster : Monster.values()) {
            monsterImages.put(
                    monster,
                    new Image(
                            Objects.requireNonNull(
                                    ChooseYourDoomApp.class.getResource(
                                            monster + "-icon.png"
                                    )
                            ).toExternalForm()
                    )
            );
        }

        return monsterImages;
    }

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

图标放置在与包含主要应用程序代码的包相同层次结构下的资源目录中。

Dragon-icon.png

Medusa-icon.png

Treant-icon.png

Unicorn-icon.png