How to Populate a Listview in Javafx Using Custom Objects

How can I Populate a ListView in JavaFX using Custom Objects?

Solution Approach

I advise using a cell factory to solve this problem.

listViewOfWords.setCellFactory(param -> new ListCell<Word>() {
@Override
protected void updateItem(Word item, boolean empty) {
super.updateItem(item, empty);

if (empty || item == null || item.getWord() == null) {
setText(null);
} else {
setText(item.getWord());
}
}
});

Sample Application

add image

import javafx.application.Application;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class CellFactories extends Application {
@Override
public void start(Stage stage) {
ObservableList<Word> wordsList = FXCollections.observableArrayList();
wordsList.add(new Word("First Word", "Definition of First Word"));
wordsList.add(new Word("Second Word", "Definition of Second Word"));
wordsList.add(new Word("Third Word", "Definition of Third Word"));
ListView<Word> listViewOfWords = new ListView<>(wordsList);
listViewOfWords.setCellFactory(param -> new ListCell<Word>() {
@Override
protected void updateItem(Word item, boolean empty) {
super.updateItem(item, empty);

if (empty || item == null || item.getWord() == null) {
setText(null);
} else {
setText(item.getWord());
}
}
});
stage.setScene(new Scene(listViewOfWords));
stage.show();
}

public static class Word {
private final String word;
private final String definition;

public Word(String word, String definition) {
this.word = word;
this.definition = definition;
}

public String getWord() {
return word;
}

public String getDefinition() {
return definition;
}
}

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

Implementation Notes

Although you could override toString in your Word class to provide a string representation of the word aimed at representation in your ListView, I would recommend providing a cell factory in the ListView for extraction of the view data from the word object and representation of it in your ListView. Using this approach you get separation of concerns as you don't tie a the graphical view of your Word object with it's textual toString method; so toString could continue to have different output (for example full information on Word fields with a word name and a description for debugging purposes). Also, a cell factory is more flexible as you can apply various graphical nodes to create a visual representation of your data beyond just a straight text string (if you wish to do that).

Also, as an aside, I recommend making your Word objects immutable objects, by removing their setters. If you really need to modify the word objects themselves, then the best way to handle that is to have exposed observable properties for the object fields. If you also want your UI to update as the observable properties of your objects change, then you need to make your list cells aware of the changes to the associated items, by listening for changes to them (which is quite a bit more complex in this case, an extractor can help). Note that, the list containing the words is already observable and ListView will take care of handling changes to that list, but if you modified the word definition for instance within a displayed word object, then your list view wouldn't pick up changes to the definition without appropriate listener logic in the ListView cell factory (or, preferably, an extractor).

How can I Populate a ListView in JavaFX using Custom Objects?

Solution Approach

I advise using a cell factory to solve this problem.

listViewOfWords.setCellFactory(param -> new ListCell<Word>() {
@Override
protected void updateItem(Word item, boolean empty) {
super.updateItem(item, empty);

if (empty || item == null || item.getWord() == null) {
setText(null);
} else {
setText(item.getWord());
}
}
});

Sample Application

add image

import javafx.application.Application;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.stage.Stage;

public class CellFactories extends Application {
@Override
public void start(Stage stage) {
ObservableList<Word> wordsList = FXCollections.observableArrayList();
wordsList.add(new Word("First Word", "Definition of First Word"));
wordsList.add(new Word("Second Word", "Definition of Second Word"));
wordsList.add(new Word("Third Word", "Definition of Third Word"));
ListView<Word> listViewOfWords = new ListView<>(wordsList);
listViewOfWords.setCellFactory(param -> new ListCell<Word>() {
@Override
protected void updateItem(Word item, boolean empty) {
super.updateItem(item, empty);

if (empty || item == null || item.getWord() == null) {
setText(null);
} else {
setText(item.getWord());
}
}
});
stage.setScene(new Scene(listViewOfWords));
stage.show();
}

public static class Word {
private final String word;
private final String definition;

public Word(String word, String definition) {
this.word = word;
this.definition = definition;
}

public String getWord() {
return word;
}

public String getDefinition() {
return definition;
}
}

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

Implementation Notes

Although you could override toString in your Word class to provide a string representation of the word aimed at representation in your ListView, I would recommend providing a cell factory in the ListView for extraction of the view data from the word object and representation of it in your ListView. Using this approach you get separation of concerns as you don't tie a the graphical view of your Word object with it's textual toString method; so toString could continue to have different output (for example full information on Word fields with a word name and a description for debugging purposes). Also, a cell factory is more flexible as you can apply various graphical nodes to create a visual representation of your data beyond just a straight text string (if you wish to do that).

Also, as an aside, I recommend making your Word objects immutable objects, by removing their setters. If you really need to modify the word objects themselves, then the best way to handle that is to have exposed observable properties for the object fields. If you also want your UI to update as the observable properties of your objects change, then you need to make your list cells aware of the changes to the associated items, by listening for changes to them (which is quite a bit more complex in this case, an extractor can help). Note that, the list containing the words is already observable and ListView will take care of handling changes to that list, but if you modified the word definition for instance within a displayed word object, then your list view wouldn't pick up changes to the definition without appropriate listener logic in the ListView cell factory (or, preferably, an extractor).

How to create a ListView of complex objects and allow editing a field on the object?

TextFieldListCell is just a convenience implementation of ListCell that provides the most common form of editing for list cells (i.e. if the items in the list are Strings, or objects that have an easy conversion to and from strings). You'll often find that you need more specific editing (e.g. you'll often want to filter the text allowed in the editing text field using a TextFormatter), and in that case you just implement the ListCell yourself. I think this is a case where, on balance, it makes more sense to implement ListCell from scratch.

It seems you can force the TextFieldListCell to work for this use case, using:

    listView.setCellFactory(lv -> {
TextFieldListCell<Person> cell = new TextFieldListCell<Person>();
cell.setConverter(new StringConverter<Person>() {
@Override
public String toString(Person person) {
return person.getName();
}
@Override
public Person fromString(String string) {
Person person = cell.getItem();
person.setName(string);
return person ;
}
});
return cell;
});

(Note that in your code, your updateItem() method is equivalent to the one already implemented in TextFieldListCell, so it's redundant, and the extra functionality in commitEdit(...) is now in the (typesafe) StringConverter, so there's no longer any need for a subclass.)

This just feels a little fragile, as it relies on a particular implementation of committing the new value from the text field and its interaction with the string converter, but it seems to work fine in tests.

My preference for this, however, would be to implement the ListCell directly yourself, as it gives you full control over the interaction between the text field and the editing process. This is pretty straightforward:

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;

public class Main extends Application {

@Override
public void start(Stage primaryStage) {
ListView<Person> listView = new ListView<>();
ObservableList<Person> people = FXCollections.observableArrayList(
new Person("John Doe", "123 New York"),
new Person("Jane Doe", "456 San Francisco")
);
listView.setEditable(true);
listView.setItems(people);

listView.setCellFactory(lv -> new ListCell<Person>() {
private TextField textField = new TextField() ;

{
textField.setOnAction(e -> {
commitEdit(getItem());
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}

@Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(person.getName());
setText(null);
setGraphic(textField);
} else {
setText(person.getName());
setGraphic(null);
}
}

@Override
public void startEdit() {
super.startEdit();
textField.setText(getItem().getName());
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}

@Override
public void cancelEdit() {
super.cancelEdit();
setText(getItem().getName());
setGraphic(null);
}

@Override
public void commitEdit(Person person) {
super.commitEdit(person);
person.setName(textField.getText());
setText(textField.getText());
setGraphic(null);
}
});

// for debugging:
listView.setOnMouseClicked(e -> {
if (e.getClickCount() == 2) {
listView.getItems().forEach(p -> System.out.println(p.getName()));
}
});

Scene scene = new Scene(listView,400,300);
primaryStage.setScene(scene);
primaryStage.show();

}

public static class Person {
private String name;
private String address;
public Person(String name, String address) {
this.name = name;
this.address = address;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String toString() {
return name+" at "+address;
}
}

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

If you might need this kind of functionality frequently, you could easily create a reusable class:

import java.util.function.BiFunction;
import java.util.function.Function;

import javafx.scene.control.ListCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public class EditingListCell<T> extends ListCell<T> {
private final TextField textField ;
private final Function<T, String> propertyAccessor ;

public EditingListCell(Function<T, String> propertyAccessor, BiFunction<String, T, T> updater) {
this.propertyAccessor = propertyAccessor ;
this.textField = new TextField();

textField.setOnAction(e -> {
T newItem = updater.apply(textField.getText(), getItem());
commitEdit(newItem);
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if (e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}

@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
textField.setText(propertyAccessor.apply(item));
setText(null);
setGraphic(textField);
} else {
setText(propertyAccessor.apply(item));
setGraphic(null);
}
}

@Override
public void startEdit() {
super.startEdit();
textField.setText(propertyAccessor.apply(getItem()));
setText(null);
setGraphic(textField);
textField.selectAll();
textField.requestFocus();
}

@Override
public void cancelEdit() {
super.cancelEdit();
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}

@Override
public void commitEdit(T item) {
super.commitEdit(item);
getListView().getItems().set(getIndex(), item);
setText(propertyAccessor.apply(getItem()));
setGraphic(null);
}
}

and then you just need

listView.setCellFactory(lv -> new EditingListCell<>(
Person::getName,
(text, person) -> {
person.setName(text);
return person ;
})
);

JavaFX populate a simple listView

Specify the controller to your fxml file with all other details as below,

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="557.0" prefWidth="1012.0" style="-fx-background-color: #0288D1;" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="FXMLDocumentController">

</AnchorPane>

Add controller class with full specified name (with package name, if u have any)

ListView with custom content in JavaFX

You can provide a custom CellFactory via see ListView.setCellFactory(...)

Example

Working example

import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Callback;

public class CustomListView extends Application {
private static class CustomThing {
private String name;
private int price;
public String getName() {
return name;
}
public int getPrice() {
return price;
}
public CustomThing(String name, int price) {
super();
this.name = name;
this.price = price;
}
}

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

@Override
public void start(Stage primaryStage) {
ObservableList<CustomThing> data = FXCollections.observableArrayList();
data.addAll(new CustomThing("Cheese", 123), new CustomThing("Horse", 456), new CustomThing("Jam", 789));

final ListView<CustomThing> listView = new ListView<CustomThing>(data);
listView.setCellFactory(new Callback<ListView<CustomThing>, ListCell<CustomThing>>() {
@Override
public ListCell<CustomThing> call(ListView<CustomThing> listView) {
return new CustomListCell();
}
});

StackPane root = new StackPane();
root.getChildren().add(listView);
primaryStage.setScene(new Scene(root, 200, 250));
primaryStage.show();
}

private class CustomListCell extends ListCell<CustomThing> {
private HBox content;
private Text name;
private Text price;

public CustomListCell() {
super();
name = new Text();
price = new Text();
VBox vBox = new VBox(name, price);
content = new HBox(new Label("[Graphic]"), vBox);
content.setSpacing(10);
}

@Override
protected void updateItem(CustomThing item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) { // <== test for null item and empty parameter
name.setText(item.getName());
price.setText(String.format("%d $", item.getPrice()));
setGraphic(content);
} else {
setGraphic(null);
}
}
}

}




Related Topics



Leave a reply



Submit