javaimagelistviewjavafxobservablelist

JavaFX: Display image in ListView without breaking selection model


Issue

I want to display a list that contains text and an image. I am able to do that, but the selection model is funky. When I select an item in the list with my mouse, it seems like the entire listview element is selected. When I use the arrow keys, the selection model works fine.

My code

In my controller I have ObservableList<Game> gameList. The Game class looks like this:

public class Game {
    private String name;
    private Image image;
}

Example from old solutions on Stack Overflow

When searching for a solution on how to display the image and name, I found many Stack Overflow solutions that used the setCellFactory method like the code snippet below:

listView.setCellFactory(param -> new ListCell<>() {
            private final ImageView imageView = new ImageView();
            @Override
            public void updateItem(String item, boolean empty) {
                if (empty) {
                    setText(null);
                    setGraphic(null);
                } else {
                    imageView.setImage(/*Some image*/);
                    setText(game.getName());
                    setGraphic(imageView);
                }
            }
        });

My attempt for a solution

However, the image that I want to display is stored in the Game object in my ObservableList. From my understanding, the String item parameter above is the Game objects toString method, but I want access to the entire Game object when making my custom ListCell. I tried to alter that solution to get access to the entire Game object instead. This is what my code currently looks like:

public class MyController implements Initializable {
    @FXML
    public ListView<Game> listView;

    public ObservableList<Game> gameList;

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        gameList = FXCollections.observableList(/*List of games*/);
        listView.setItems(gameList);
        listView.setCellFactory(param -> new ListCell<>() {
            private final ImageView imageView = new ImageView();
            @Override
            public void updateItem(Game game, boolean empty) {
                if (empty) {
                    setText(null);
                    setGraphic(null);
                } else {
                    imageView.setImage(game.getImage());
                    setText(game.getName());
                    setGraphic(imageView);
                }
            }
        });
    }
}

Result

With the code above, I'm able to display each Game and it's name in my ListView. enter image description here

The issue that I'm trying to fix

The list is displayed exactly like I want, but the selection model seems to be broken. I use listView.getSelectionModel().getSelectedItem(); to fetch the selected Game. When I use my mouse to select an item, the method above returns null. This is what it looks like when I left click on the "Other game" item in the list: enter image description here

However, I can use my arrow keys to select any item in the list I want. When I do that, the selected Game from my ObservableList is returned.

Does anyone have an idea of how I can solve this?


Solution

  • The default ListCell.updateItem(...) method handles selection, among other things. So you need to be sure to call it.

    From the documentation:

    It is very important that subclasses of Cell override the updateItem method properly ... Note in this code sample two important points:

    1. We call the super.updateItem(T, boolean) method. If this is not done, the item and empty properties are not correctly set, and you are likely to end up with graphical issues.
    2. ...

    So you need:

    listView.setCellFactory(param -> new ListCell<>() {
        private final ImageView imageView = new ImageView();
        @Override
        public void updateItem(Game game, boolean empty) {
    
           // call default implementation:
           super.updateItem(item, empty);
    
           if (empty) {
                setText(null);
                setGraphic(null);
            } else {
                imageView.setImage(game.getImage());
                setText(game.getName());
                setGraphic(imageView);
            }
        }
    });