Reputation: 18812
I am trying to make a custom builder proposed in Dan Nicks's comment to this question.
The idea is to set combo's data before constructing it.
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>
The class that provides the data:
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;
}
}
}
And the test:
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);
}
}
No errors produced, but combo is not populated.
What is missing ?
BTW: a similar solution for TableView
works fine:
<?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>
Upvotes: 0
Views: 3045
Reputation: 18812
Edited following comments by kleopatra:
Loading combo.fxml
given in the question with Strings can be done using the following loader:
//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;
}
}
Loading combo with Item
instances in a similar fashion simply means that Item#toString
for the text in the combo:
//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;
}
}
Where Item is defined as:
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();
}
}
A better approach is to load combo with custom 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());
}
}
}
The last but not least alternative is to set a custom ListCell<Item>
as a cell factory for the combo.
This can be done by adding a controller to the fxml file:
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>
Where ComboObjectLoaderAndController
is both a loader and a controller:
//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());
}
}
}
}
Edit:
following kleopatra's answer I added a generic custom 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));
}
}
}
The factory is set in the fxml file:
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>
A test class:
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);
}
}
Upvotes: 0
Reputation: 51535
Starting from a nit-pick, I did some experiments on how to actually implement what I tried to outline in my comments to c0der's answer.
The basic idea is to follow the same approach for the listCell as for the data, that is configure both content and appearance via namespace (my learn item of the day). The ingredients:
The cell/factory:
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));
}
}
}
}
The fxml to create and configure the combo:
<?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>
An example to use it:
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);
}
}
Upvotes: 1