FXML Динамическая инициализация ObservableList для ComboBox и TableView

Я пытаюсь создать собственный конструктор, предложенный в комментарии Дэна Никса к этот вопрос.
Идея состоит в том, чтобы установить данные комбо перед его созданием.
combo.fxml:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>

<ComboBox  fx:id="combo1" items="${itemLoader.items}"  prefWidth="150.0" 
   xmlns:fx="http://javafx.com/fxml/1">
</ComboBox>

Класс, который предоставляет данные:

public class ComboLoader {

    public ObservableList<Item> items;

    public ComboLoader() {

        items = FXCollections.observableArrayList(createItems());
    }

    private List<Item> createItems() {
            return IntStream.rangeClosed(0, 5)
                    .mapToObj(i -> "Item "+i)
                    .map(Item::new)
                    .collect(Collectors.toList());
        }

    public ObservableList<Item> getItems(){

        return items;
    }

    public static class Item {

        private final StringProperty name = new SimpleStringProperty();

        public Item(String name) {
            this.name.set(name);
        }

        public final StringProperty nameProperty() {
            return name;
        }
     
    }
}

И тест:

public class ComboTest extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {

        primaryStage.setTitle("Populate combo from custom builder");

        Group group = new Group();

        GridPane grid = new GridPane();
        grid.setPadding(new Insets(25, 25, 25, 25));
        group.getChildren().add(grid);

        FXMLLoader loader = new FXMLLoader();
        ComboBox combo = loader.load(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboLoader());
        grid.add(combo, 0, 0);

        Scene scene = new Scene(group, 450, 175);

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

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

Ошибок не выдается, но комбо не заполняется.
Чего не хватает?


Кстати: аналогичное решение для TableView работает нормально:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.cell.PropertyValueFactory?>

 <TableView items="${itemLoader.items}"   xmlns:fx="http://javafx.com/fxml/1">
     <columns>
         <TableColumn text="Item">
             <cellValueFactory><PropertyValueFactory property="name" /></cellValueFactory>
         </TableColumn>
     </columns>
 </TableView>

person c0der    schedule 02.06.2017    source источник


Ответы (2)


Начиная с придирки, я провел несколько экспериментов о том, как на самом деле реализовать то, что я пытался изложить в своих комментариях к c0der answer.

Основная идея состоит в том, чтобы следовать тому же подходу для listCell, что и для данных, то есть настроить как содержимое, так и внешний вид через пространство имен (мой элемент изучения дня). Ингредиенты:

  • общий пользовательский listCell, настраиваемый с помощью функции преобразования элемента в текст
  • общий класс фабрики cellFactory для предоставления фабрики cellFactory, создающей эту ячейку

Ячейка/фабрика:

public class ListCellFactory<T> {
    
    private Function<T, String> textProvider;

    public ListCellFactory(Function<T, String> provider) {
        this.textProvider = provider;
    }

    public Callback<ListView<T>, ListCell<T>> getCellFactory() {
        return cc -> new CListCell<>(textProvider);
    }
    
    public ListCell<T> getButtonCell() {
        return getCellFactory().call(null);
    }
    
    public static class CListCell<T> extends ListCell<T> {
        
        private Function<T, String> converter;

        public CListCell(Function<T, String> converter) {
            this.converter = Objects.requireNonNull(converter, "converter must not be null");
        }

        @Override
        protected void updateItem(T item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setText(null);
            } else {
                setText(converter.apply(item));
            }
        }
        
    }

}

Файл fxml для создания и настройки комбинации:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ComboBox?>

<ComboBox  fx:id="combo1" items="${itemLoader.items}"  
    cellFactory="${cellFactoryProvider.cellFactory}" 
    buttonCell = "${cellFactoryProvider.buttonCell}"
    prefWidth="150.0" 
   xmlns:fx="http://javafx.com/fxml/1">
</ComboBox>

Пример его использования:

public class LocaleLoaderApp extends Application {

    private ComboBox<Locale> loadCombo(Object itemLoader, Function<Locale, String> extractor) throws IOException {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("comboloader.fxml"));
        loader.getNamespace().put("itemLoader", itemLoader);
        loader.getNamespace().put("cellFactoryProvider", new ListCellFactory<Locale>(extractor));
        ComboBox<Locale> combo = loader.load();
        return combo;
    }
    
    @Override
    public void start(Stage primaryStage) throws IOException {

        primaryStage.setTitle("Populate combo from custom builder");

        Group group = new Group();
        GridPane grid = new GridPane();
        grid.setPadding(new Insets(25, 25, 25, 25));
        group.getChildren().add(grid);
        LocaleProvider provider = new LocaleProvider();
        grid.add(loadCombo(provider, Locale::getDisplayName), 0, 0);
        grid.add(loadCombo(provider, Locale::getLanguage), 1, 0);
        Scene scene = new Scene(group, 450, 175);

        primaryStage.setScene(scene);
        primaryStage.show();
    }
    
    public static class LocaleProvider {
        ObservableList<Locale> locales = FXCollections.observableArrayList(Locale.getAvailableLocales());
        
        public ObservableList<Locale> getItems() {
            return locales;
        }
    }
    

    public static void main(String[] args) {
        launch(args);
    }
}
person kleopatra    schedule 29.06.2020
comment
Вы тоже работаете (0: это шире, чем ответ, и очень полезно. Мне нравится использование Function<Locale, String> extractor. В моем комментарии к другому вопросу я спрашивал об этом: cellFactory="${cellFactoryProvider.cellFactory}. Все эти вкусности задокументированы? Спасибо за исчерпывающий ответ. - person c0der; 01.07.2020

Отредактированы следующие комментарии kleopatra:
Загрузка combo.fxml, указанных в вопросе со строками, может быть выполнена с использованием следующего загрузчика:

//load observable list with strings
public class ComboStringLoader {

    private final ObservableList<String> items;

    public ComboStringLoader() {
        items = FXCollections.observableArrayList(createStrings());
    }

    private List<String> createStrings() {
            return IntStream.rangeClosed(0, 5)
                    .mapToObj(i -> "String "+i)
                    .map(String::new)
                    .collect(Collectors.toList());
    }

    //name of this method corresponds to itemLoader.items in xml.
    //if xml name was itemLoader.a this method should have been getA(). 
    public ObservableList<String> getItems(){
        return items;
    }
}

Загрузка комбо с экземплярами Item аналогичным образом просто означает, что Item#toString для текста в комбо:

//load observable list with Item#toString
public class ComboObjectLoader1 {

    public ObservableList<Item> items;

    public ComboObjectLoader1() {
        items = FXCollections.observableArrayList(createItems());
    }

    private List<Item> createItems() {
        return IntStream.rangeClosed(0, 5)
                .mapToObj(i -> "Item "+i)
                .map(Item::new)
                .collect(Collectors.toList());
    }

    public ObservableList<Item> getItems(){
        return items;
    }
}

Где Item определяется как:

class Item {

    private final StringProperty name = new SimpleStringProperty();

    public Item(String name) {
        this.name.set(name);
    }

    public final StringProperty nameProperty() {
        return name;
    }

    @Override
    public String toString() {
        return name.getValue();
    }
}

Лучший подход — загрузить комбо с пользовательским ListCell<item>:

//load observable list with custom ListCell
public class ComboObjectLoader2 {

    private final ObservableList<ItemListCell> items;

    public ComboObjectLoader2() {
        items =FXCollections.observableArrayList (createCells());
    }

    private List<ItemListCell> createCells() {
            return IntStream.rangeClosed(0, 5)
                    .mapToObj(i -> "Item "+i)
                    .map(Item::new)
                    .map(ItemListCell::new)
                    .collect(Collectors.toList());
    }

    public ObservableList<ItemListCell> getItems(){
        return items;
    }
}

class ItemListCell extends ListCell<Item> {

    private final Label text;

    public ItemListCell(Item item) {
        text = new Label(item.nameProperty().get());
        setGraphic(new Pane(text));
    }

    @Override
    public void updateItem(Item item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            text.setText(item.nameProperty().get());
        }
    }
}

Последней, но не менее важной альтернативой является установка пользовательского ListCell<Item> в качестве фабрики ячеек для комбо.
Это можно сделать, добавив контроллер в файл fxml:
combo2.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}" prefWidth="150.0" xmlns:fx="http://javafx.com/fxml/1" 
      xmlns="http://javafx.com/javafx/10.0.1" fx:controller="test.ComboObjectLoaderAndController">
</ComboBox>

Где ComboObjectLoaderAndController — это и загрузчик, и контроллер:

//loads observable list with Items and serves as controller to set cell factory
public class ComboObjectLoaderAndController {

    public ObservableList<Item> items;
    @FXML ComboBox<Item> combo1;

    public ComboObjectLoaderAndController() {
        items = FXCollections.observableArrayList(createItems());
    }

    @FXML
    public void initialize() {
        combo1.setCellFactory(l->new ItemListCell());
    }

    private List<Item> createItems() {
        return IntStream.rangeClosed(0, 5)
                .mapToObj(i -> "Item "+i)
                .map(Item::new)
                .collect(Collectors.toList());
    }

    public ObservableList<Item> getItems(){
        return items;
    }

    class ItemListCell extends  ListCell<Item>{

        @Override
        public void updateItem(Item item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                setText(item.nameProperty().get());
            }
        }
    }
}

Редактировать:
после ответа Клеопатры я добавил общий пользовательский ListCell

public class ObjectListCell<T> extends ListCell<T> {

    Function<T,String> textSupplier;

    public  ObjectListCell(Function<T,String> textSupplier) {
        this.textSupplier = textSupplier;
    }

    public Callback<ListView<T>, ListCell<T>> getFactory() {
        return cc -> new ObjectListCell<>(textSupplier);
    }

    public ListCell<T> getButtonCell() {
        return getFactory().call(null);
    }

    @Override
    public void updateItem(T t, boolean empty) {
        super.updateItem(t, empty);
        if (t== null || empty) {
            setText(null);
            setGraphic(null);
        } else {
            setText(textSupplier.apply(t));
        }
    }
}

Фабрика задается в файле fxml:
combo3.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.ComboBox?>
<ComboBox fx:id="combo1" items="${itemLoader.items}"  cellFactory="${cellFactoryProvider.factory}" 
   buttonCell = "${cellFactoryProvider.buttonCell}"
   prefWidth="150.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/10.0.1">
</ComboBox>

Тестовый класс:

public class ComboTest extends Application {

    @Override
    public void start(Stage primaryStage) throws IOException {

        primaryStage.setTitle("Populate combo from custom builder");

        //Combo of Strings
        FXMLLoader loader = new FXMLLoader(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboStringLoader());
        ComboBox<String>stringCombo = loader.load();

        //Combo of Item
        loader = new FXMLLoader(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
        ComboBox<Item>objectsCombo1 = loader.load();

        //Combo of custom ListCell
        loader = new FXMLLoader(getClass().getResource("combo.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoader2());
        ComboBox<ItemListCell>objectsCombo2 = loader.load();

        //Combo of Item with custom ListCell factory
        loader = new FXMLLoader(getClass().getResource("combo2.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoaderAndController());
        ComboBox<Item>objectsCombo3 = loader.load();

        //Combo of Item with custom ListCell factory. Factory is set in FXML
        loader = new FXMLLoader(getClass().getResource("combo3.fxml"));
        loader.getNamespace().put("itemLoader", new ComboObjectLoader1());
        loader.getNamespace().put("cellFactoryProvider", new ObjectListCell<Item>(t -> t.nameProperty().get()));
        ComboBox<Item>objectsCombo4 = loader.load();

        HBox pane = new HBox(25, stringCombo, objectsCombo1,objectsCombo2, objectsCombo3, objectsCombo4);
        pane.setPadding(new Insets(25, 25, 25, 25));

        Scene scene = new Scene(pane, 550, 175);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}
person c0der    schedule 02.06.2017
comment
не переопределяйте toString по причинам просмотра, вместо этого реализуйте собственный listCell с соответствующим updateItem - person kleopatra; 28.06.2020
comment
@kleopatra Это означает, что мне придется добавить контроллер (или использовать загрузчик как один), чтобы установить фабрику. Или есть способ определить ссылку на фабрику в fxml? - person c0der; 28.06.2020
comment
нет, почему вы так думаете? пользовательская ячейка — единственный способ - person kleopatra; 29.06.2020
comment
@kleopatra да. Мой комментарий/вопрос о том, как применить пользовательский ListCell к комбо. Насколько мне известно, для этого требуется setCellFactory, отсюда и мой вопрос: это означает, что мне придется добавить контроллер (или использовать загрузчик как один), чтобы установить фабрику. Или есть способ определить ссылку на фабрику в fxml? - person c0der; 29.06.2020
comment
мой комментарий был (может быть, недостаточно ясным, простите): реализовать пользовательскую ячейку и использовать ее в cellFactory (возможно, нужно будет выставить ее как пользовательский класс, если честно). Что меня волнует (и что принесло вам мой отрицательный голос;) является частью первого предложения в ответе добавление метода toString, он отлично работает в вашем ответе - это совершенно неправильно, какой бы ни была проблема. Он ничего не решает, а только размазывает краску. - person kleopatra; 29.06.2020
comment
вау .. у вас работает ‹g› хороший набор опций (кроме первого с переопределенным toString :) - опубликую еще один (утро понедельника, время для веселых экспериментов :) - person kleopatra; 29.06.2020