【问题标题】:JavaFx: Populate ComboBox with different enums depending on another ComboBoxJavaFx:根据另一个 ComboBox 使用不同的枚举填充 ComboBox
【发布时间】:2018-03-20 15:28:15
【问题描述】:

我有两个组合框:

    final ComboBox<MainCategory> comboBoxMainCategory = new ComboBox<>();
    final ComboBox<SubCategory> comboBoxSubCategory = new ComboBox<>();

根据在 comboBoxMainCategory 中选择的值,comboBoxSubCategory 应填充相应的枚举。

    public enum MainCategory { // extra enum class
      EUROPE("Europe"),
      USA("USA");
    }

    public enum SubCategoryEurope { // extra enum class
      GERMANY("Germany"),
      FRANCE("France");
    }

    public enum SubCategoryUSA {
      COLORADO("Colorado"),
      CALIFORNIA("California");
    }

如果为 comboBoxMainCategory 选择了“Europe”,comboBoxSubCategory 应填充 SubCategoryEurope。如果是“美国”,则使用 SubCategoryUSA。

您是如何做到这一点的?

这是我的代码:

    final ComboBox<MainCategory> comboBoxMainCategory = new ComboBox<();
    final ComboBox<SubCategory> comboBoxSubCategory = new ComboBox<>(); 

    comboBoxMainCategory.valueProperty().addListener((obs, oldValue, 
    newValue) ->
          {
            if (newValue == null) { // newValue: Europe || USA
              comboBoxSubCategory.getItems().clear();
              comboBoxSubCategory.setDisable(true);
            } else if (newValue.equals(MainCategory.EUROPE)) {

 comboBoxSubCategory.setItems(FXCollections.observableArrayList(SubCategoryEurope.values())); 
            comboBoxSubCategory.setDisable(false);
        } else {
             comboBoxSubCategory.setItems(FXCollections.observableArrayList(SubCategoryUSA.values())); 
            comboBoxSubCategory.setDisable(false);}
        });

问题是,因为 comboBoxSubCategory 是“SubCategory”,如果填充 'SubCategoryEurope' 或 'SubCategoryUSA' 会出现类型错误。

解决此问题的最佳方法是什么?对不起,如果这是一个愚蠢的问题,我是 JavaFx 的新手。

非常感谢!

【问题讨论】:

  • 欧洲/美国和佛罗里达/法国属于同一(子)类别?你需要清理你的逻辑

标签: java javafx enums combobox


【解决方案1】:

我根本不会使用枚举,因为这不允许在不重新编译的情况下进行数据操作。如果您坚持使用枚举,则需要使用Object 或使用两个子类别枚举类型实现的接口作为comboBoxSubCategory 的参数类型:

comboBoxMainCategory.valueProperty().addListener((obs, oldValue, newValue) -> {
        if (newValue == null) { // newValue: Europe || USA
            comboBoxSubCategory.getItems().clear();
            comboBoxSubCategory.setDisable(true);
        } else {
            comboBoxSubCategory.setDisable(false);
            List<? extends Object> list;
            switch (newValue) {
                case EUROPE:
                    list = Arrays.asList(SubCategoryEurope.values());
                    break;
                default:
                    list = Arrays.asList(SubCategoryUSA.values());
                    break;
            }
            comboBoxSubCategory.getItems().setAll(list);
        }
    });

更好的方法是使用Map&lt;String, List&lt;String&gt;&gt; 来存储数据:

Map<String, List<String>> data = new HashMap<>();
data.put("EUROPE", Arrays.asList("GERMANY", "FRANCE"));
data.put("USA", Arrays.asList("COLORADO", "CALIFORNIA"));

comboBoxMainCategory.valueProperty().addListener((obs, oldValue, newValue) -> {
        List<String> list = data.get(newValue);
        if (list != null) {
            comboBoxSubCategory.setDisable(false);
            comboBoxSubCategory.getItems().setAll(list);
        } else {
            comboBoxSubCategory.getItems().clear();
            comboBoxSubCategory.setDisable(true);
        }
    });

【讨论】:

  • 即使以您的方式也可以写入枚举,然后将其缓存在Map 中。 (只是说)。
  • 是的,但这并不能解决不使用相同类型表示子类别的基本设计缺陷。如果所有国家/地区都有区域子类别,您还会使用这种方法吗?您是否建议为美国所有 50 个州制定常量?
  • 我明白你的意思——这就是我写“只是说”的原因。 (可能)有很多情况需要枚举。投票给你的答案... GL,朋友。
  • 非常感谢!现在试试这个。不幸的是,'List list = data.get(newValue); ' 最终以“列表”为空。我不明白为什么......
  • @RuntimeError 此示例要求您使用 String 键作为类别组合框的项目,而不是您的枚举值...
【解决方案2】:

只是为了好玩(并充实我的 cmets):比其他答案中的方法更通用的方法是将关注的焦点从支持数据的具体性质转移到更通用的使用解决方案 -手头的案子。让 UI 实现特殊情况的缺点始终是相同的 - 您必须为每个特殊 UI 和每个特殊数据类型一遍又一遍地执行此操作。出路也总是一样的:实现一个模型来接管一般方面并在具体的 UI/数据上下文中重用它。

这里的一般方面是:

  • 有一个项目列表,每个项目都有一个依赖对象列表(相同或不同类型)
  • 这个(我们称之为根)项目列表显示在控件中
  • 从根列表中,可以选择单个项目(又名:已选择)
  • 另一个控件应该显示根的依赖项

一般的方法是有一个模型

  • 管理项目列表
  • 具有选定项目之一的概念(或当前或活动或..)
  • 管理一个依赖项列表,该列表始终是选定项的依赖项列表
  • 其状态(根项、当前项、依赖项)作为属性公开

这种模型的优点是

  • 可以经过正式和严格的测试,因此使用代码可以依赖其正常运行
  • 它可重复用于任何数据上下文
  • 它可重复用于许多控件
  • 通过绑定使用非常简单

在下面的示例中,模型被命名为 RelationModel,它需要类型为 RelationProvider 的根项(它允许访问依赖项列表,这是一种选择,也可以使用 f.i. 一个函数来构建依赖项)。它与 String/list 的普通 Map 一起使用一次,与 Continents/Countries 的枚举一起使用一次,每个都非常易于实现。请注意,生成的 UI 完全不知道数据的性质,仅针对模型实施。

当然,不是生产级,尤其是未经正式测试的模型,仅具有最基本的功能:)

public class CombosWithCategories extends Application {

    public interface RelationProvider<T> {
        default ObservableList<T> getRelations() {
            return emptyObservableList();
        };
    }

    /**
     * A model that manages a list of RelationProviders and has the notion
     * of a current relationProvider with relations (it's a kind-of selectionModel).
     * 
     * <T> the type of elements in the list of relations 
     */
    public static class RelationModel<T> {

        /**
         * all relationProviders managed by this model
         */
        private ListProperty<RelationProvider<T>> relationProviders;
        /**
         * The owner of the relations. Must be contained in the providers managed
         * by this model.
         */
        private ObjectProperty<RelationProvider<T>> relationProvider;
        private ListProperty<T> relations;

        public RelationModel() {
            initProperties();
        }

        /**
         * The RelationProviders managed by the model.
         */
        public ListProperty<RelationProvider<T>> relationProvidersProperty() {
            return relationProviders;
        }

        /**
         * The RelationProvider that manages the current relations.
         */
        public ObjectProperty<RelationProvider<T>> relationProviderProperty() {
            return relationProvider;
        }

        public RelationProvider<T> getRelationProvider() {
            return relationProviderProperty().get();
        }

        public ListProperty<T> relations() {
            return relations;
        }

        /**
         * Callback from invalidation of current relationProvider.
         * Implemented to update relations.
         */
        protected void relationProviderInvalidated() {
            RelationProvider<T> value = getRelationProvider();
            relations().set(value != null ? value.getRelations() : emptyObservableList());
        }

        /**
         * Creates and wires all properties.
         */
        private void initProperties() {
            relationProviders = new SimpleListProperty<>(this, "relationProviders", observableArrayList());
            relationProvider = new SimpleObjectProperty<>(this, "relationProvider") {

                @Override
                protected void invalidated() {
                    // todo: don't accept providers that are not in the list
                    relationProviderInvalidated();
                }

            };
            relations = new SimpleListProperty<>(this, "relations");
            relationProviderInvalidated();

        }

    }

    /**
     * Implement the ui against a RelationModel. Here we create
     * the same UI with a model backed by enums or a Map, respectively
     */
    private Parent createContent() {
        TabPane tabPane = new TabPane(
                new Tab("Enums", createRelationUI(createEnumRelationModel())),
                new Tab("Manual map", createRelationUI(createMapRelationModel()))
                );

        return new BorderPane(tabPane);
    }

    /**
     * Common factory for UI: creates and returns a Parent that
     * contains two combo's configured to use the model.
     */
    protected <T> Parent createRelationUI(RelationModel<T> model) {
        ComboBox<RelationProvider<T>> providers = new ComboBox<>();
        providers.itemsProperty().bind(model.relationProvidersProperty());
        providers.valueProperty().bindBidirectional(model.relationProviderProperty());

        ComboBox<T> relations = new ComboBox<>();
        relations.itemsProperty().bind(model.relations());
        relations.valueProperty().addListener((src, ov, nv) -> {
            LOG.info("relation changed: " + nv); 
        });

        return new VBox(10, providers, relations);
    }


    // ------------- manual with maps

    /**
     * On-the-fly creation of a RelationModel using a backing map.
     */
    protected RelationModel<String> createMapRelationModel() {
        RelationModel<String> model = new RelationModel<>();
        Map<String, ObservableList<String>> data = new HashMap<>();
        data.put("EUROPE", observableArrayList("GERMANY", "FRANCE"));
        data.put("AMERICA", observableArrayList("MEXICO", "USA"));
        for (String key: data.keySet()) {
            model.relationProvidersProperty().add(new RelationProvider<String>() {

                @Override
                public ObservableList<String> getRelations() {
                    return data.get(key);
                }

                @Override
                public String toString() {
                    return key;
                }


            });
        }
        return model;
    }
    //-------------------- enum
    /**
     * RelationModel using Enums.
     */
    protected RelationModel<Object> createEnumRelationModel() {
        RelationModel<Object> model = new RelationModel<Object>();
        model.relationProvidersProperty().setAll(Continent.values());
        return model;
    }

    public enum EuropeanCountry {
        FRANCE, GERMANY;
    }

    public enum AmericanCountry {
        MEXICO, CANADA, USA;
    }

    public enum Continent implements RelationProvider<Object> {
        AMERICA(AmericanCountry.values()),
        EUROPE(EuropeanCountry.values())
        ;

        ObservableList<Object> subs;
        private Continent(Object[] subs) {
            this.subs = FXCollections.observableArrayList(subs);
        }
        @Override
        public ObservableList<Object> getRelations() {
            return FXCollections.unmodifiableObservableList(subs);
        }
    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent()));
        stage.setTitle(FXUtils.version());
        stage.show();
    }

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

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(CombosWithCategories.class.getName());

}

【讨论】:

    【解决方案3】:

    创建通用占位符接口

    public interface EnumPlaceHolder<E extends Enum<?>> {
    
      public abstract String getDisplayValue();
    
      public abstract E getEnum();
    
    }
    

    为所有枚举创建一个实现。例如

    public class EuropePlaceholder implements EnumPlaceHolder<Europe> {
    
      private final Europe value;
    
      public EuropePlaceholder(Europe pValue){
         value = pValue;
      }
    
      @Override
      public String getDisplayValue() {
        // here you create a user-friendly version of your enum for display
        return value.toString();
      }
    
      @Override
      public Europe getEnum() {
        return value;
      }
    
    }
    

    然后将您的ComboBox&lt;Enum&gt; 的类型更改为ComboBox&lt;EnumPlaceholder&lt;?&gt;&gt;,您可以将任何已实现的EnumPlaceholders 添加到其中。检索所选项目时,您可以通过实例检查检查包含哪个项目

    EnumPlaceholder<?> selectedItem = ...;
    if(selectedItem instanceof EuropePlaceholder){
       Europe selectedEuropeEnum = (Europe) selectedItem.getEnum();
    } else if(....){
      // check with else if for your other enums
    }
    

    要在组合框中显示您的枚举,请调用 EnumPlaceholder 的 getDisplayValue() 并在您的单元格中显示返回的 String :)

    编辑 总的来说,我必须同意fabians的回答。您不应该将枚举用于这样的构造。而是使用具有适当内容和结构的 Map 或 List。

    【讨论】:

    • @fabian 谢谢,已编辑。没有想到这一点,也没有通过编译器或 ide 运行代码:o mb
    猜你喜欢
    • 2015-03-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-10-27
    • 1970-01-01
    相关资源
    最近更新 更多