Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFX 8, ListView with Checkboxes

I want to create a simple ListView. I have figured out I can use the method setCellFactory() but I don't understand how to use them correctly. So far I have:

myListView.setCellFactory(CheckBoxListCell.forListView(property));

With "property" being something called a Callback--I think Callback has something to do with bidirectional bounding. So I created a

property = new CallBack<String, ObservableValue<Boolean>>();

My compiler is telling me if I create a new Callback, I need to overwrite the method call.

And here I am stuck. What do I do with that method call? I can implement it, but what should I return, or use it for? I want to click my checkbox on any listItem and have it display "hi" in console.

like image 320
jan Avatar asked Mar 03 '15 23:03

jan


1 Answers

If you have a ListView<String>, then each item in the ListView is a String, and the CheckBoxListCell.forListView(...) method expects a Callback<String, ObservableValue<Boolean>>.

In the pre-Java 8 way of thinking of things, a Callback<String, ObservableValue<Boolean>> is an interface that defines a single method,

public ObservableValue<Boolean> call(String s) ;

So you need something that implements that interface, and you pass in an object of that type.

The documentation also tells you how that callback is used:

A Callback that, given an object of type T (which is a value taken out of the ListView.items list), will return an ObservableValue that represents whether the given item is selected or not. This ObservableValue will be bound bidirectionally (meaning that the CheckBox in the cell will set/unset this property based on user interactions, and the CheckBox will reflect the state of the ObservableValue, if it changes externally).

(Since you have a ListView<String>, here T is String.) So, for each element in the list view (each element is a String), the callback is used to determine an ObservableValue<Boolean> which is bidirectionally bound to the state of the checkbox. I.e. if the checkbox is checked, that property is set to true, and if unchecked it is set to false. Conversely, if the property is set to true (or false) programmatically, the checkbox is checked (or unchecked).

The typical use case here is that the type of item in the ListView would have a BooleanProperty as part of its state. So you would typically use this with some kind of custom class representing your data, as follows with the inner Item class:

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;

public class ListViewWithCheckBox extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<Item> listView = new ListView<>();
        for (int i=1; i<=20; i++) {
            Item item = new Item("Item "+i, false);

            // observe item's on property and display message if it changes:
            item.onProperty().addListener((obs, wasOn, isNowOn) -> {
                System.out.println(item.getName() + " changed on state from "+wasOn+" to "+isNowOn);
            });

            listView.getItems().add(item);
        }

        listView.setCellFactory(CheckBoxListCell.forListView(new Callback<Item, ObservableValue<Boolean>>() {
            @Override
            public ObservableValue<Boolean> call(Item item) {
                return item.onProperty();
            }
        }));

        BorderPane root = new BorderPane(listView);
        Scene scene = new Scene(root, 250, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();
        private final BooleanProperty on = new SimpleBooleanProperty();

        public Item(String name, boolean on) {
            setName(name);
            setOn(on);
        }

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

        public final String getName() {
            return this.nameProperty().get();
        }

        public final void setName(final String name) {
            this.nameProperty().set(name);
        }

        public final BooleanProperty onProperty() {
            return this.on;
        }

        public final boolean isOn() {
            return this.onProperty().get();
        }

        public final void setOn(final boolean on) {
            this.onProperty().set(on);
        }

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

    }

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

If you genuinely have a ListView<String>, it's not really clear what the property you are setting by clicking on the check box would be. But there's nothing to stop you creating one in the callback just for the purpose of binding to the check box's selected state:

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;

public class ListViewWithStringAndCheckBox extends Application {

    @Override
    public void start(Stage primaryStage) {
        ListView<String> listView = new ListView<>();
        for (int i = 1; i <= 20 ; i++) {
            String item = "Item "+i ;
            listView.getItems().add(item);
        }

        listView.setCellFactory(CheckBoxListCell.forListView(new Callback<String, ObservableValue<Boolean>>() {
            @Override
            public ObservableValue<Boolean> call(String item) {
                BooleanProperty observable = new SimpleBooleanProperty();
                observable.addListener((obs, wasSelected, isNowSelected) -> 
                    System.out.println("Check box for "+item+" changed from "+wasSelected+" to "+isNowSelected)
                );
                return observable ;
            }
        }));

        BorderPane root = new BorderPane(listView);
        Scene scene = new Scene(root, 250, 400);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

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

Notice that in this case, the BooleanPropertys are potentially being created and discarded frequently. This probably isn't a problem in practice, but it does mean the first version, with the dedicated model class, may perform better.

In Java 8, you can simplify the code. Because the Callback interface has only one abstract method (making it a Functional Interface), you can think of a Callback<Item, ObservableValue<Boolean>> as a function which takes a Item and generates an ObservableValue<Boolean>. So the cell factory in the first example could be written with a lambda expression:

    listView.setCellFactory(CheckBoxListCell.forListView(item -> item.onProperty()));

or, even more succinctly using method references:

    listView.setCellFactory(CheckBoxListCell.forListView(Item::onProperty));
like image 70
James_D Avatar answered Oct 26 '22 06:10

James_D