My TableView uses a custom CellFactory to display a ComboBox in one column, allowing the user to select from available options. Those options are loaded after the TableView is populated (as they can change based on the user's selections elsewhere in the scene).
In the MCVE below, I have two columns for my Item class: Name and Color. Within the Color column, I have the ComboBox which will display the current value of the Item's itemColor property.
You will see that the ComboBox is not populated with a list of values yet and item "Three" has no value selected.
What I need is this:
When the user clicks on the "Load Available Colors" button, the list for the ComboBox is created. The user can now select any of the available colors. However, if there is not already a value for the item's color, I want the first color in the ComboBoxes to be selected automatically; so item "Three" would now show the color "Red" as being selected.
THE MCVE
Item.java:
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Item {
private StringProperty itemName = new SimpleStringProperty();
private StringProperty itemColor = new SimpleStringProperty();
public Item(String name, String color) {
this.itemName.set(name);
this.itemColor.set(color);
}
public String getItemName() {
return itemName.get();
}
public void setItemName(String itemName) {
this.itemName.set(itemName);
}
public StringProperty itemNameProperty() {
return itemName;
}
public String getItemColor() {
return itemColor.get();
}
public void setItemColor(String itemColor) {
this.itemColor.set(itemColor);
}
public StringProperty itemColorProperty() {
return itemColor;
}
}
Main.java:
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
// List of items
private static ObservableList<Item> listOfItems = FXCollections.observableArrayList();
// List of available Colors. These will be selectable from the ComboBox
private static ObservableList<String> availableColors = FXCollections.observableArrayList();
public static void main(String[] args) {
launch(args);
}
private static void buildSampleData() {
availableColors.addAll("Red", "Blue", "Green", "Yellow", "Black");
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Build a list of sample data. This data is loaded from my data model and passed to the constructor
// of this editor in my real application.
listOfItems.addAll(
new Item("One", "Black"),
new Item("Two", "Black"),
new Item("Three", null),
new Item("Four", "Green"),
new Item("Five", "Red")
);
// TableView to display the list of items
TableView<Item> tableView = new TableView<>();
// Create the TableColumn
TableColumn<Item, String> colName = new TableColumn<>("Name");
TableColumn<Item, String> colColor = new TableColumn<>("Color");
// Cell Property Factories
colName.setCellValueFactory(column -> new SimpleObjectProperty<>(column.getValue().getItemName()));
colColor.setCellValueFactory(column -> new SimpleObjectProperty<>(column.getValue().getItemColor()));
// Add ComboBox to the Color column, populated with the list of availableColors
colColor.setCellFactory(tc -> {
ComboBox<String> comboBox = new ComboBox<>(availableColors);
comboBox.setMaxWidth(Double.MAX_VALUE);
TableCell<Item, String> cell = new TableCell<Item, String>() {
#Override
protected void updateItem(String color, boolean empty) {
super.updateItem(color, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(comboBox);
comboBox.setValue(color);
}
}
};
// Set the action of the ComboBox to set the right Value to the ValuePair
comboBox.setOnAction(event -> {
listOfItems.get(cell.getIndex()).setItemColor(comboBox.getValue());
});
return cell;
});
// Add the column to the TableView
tableView.getColumns().addAll(colName, colColor);
tableView.setItems(listOfItems);
// Add button to load the data
Button btnLoadData = new Button("Load Available Colors");
btnLoadData.setOnAction(event -> {
buildSampleData();
});
root.getChildren().add(btnLoadData);
// Add the TableView to the root layout
root.getChildren().add(tableView);
Button btnPrintAll = new Button("Print All");
btnPrintAll.setOnAction(event -> {
for (Item item : listOfItems) {
System.out.println(item.getItemName() + " : " + item.getItemColor());
}
});
root.getChildren().add(btnPrintAll);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
Now, with a regular ComboBox, a simple call to comboBox.getSelectionModel().selectFirst() after loading the availableColors would be fine. But since this ComboBox is created within the CellFactory, I am not sure how to update it once the list of colors is populated.
Indidentally, I use this CellFactory implementation instead of a ComboBoxTableCell because I want them to be visible without having to enter edit mode on the TableView.
I actually took kleopatra's advice and updated my data model to include a default value instead. I agree this is cleaner and more appropriate approach.
Related
I need RadioButtons inside ListView so i find this answer:
javaFX:listview with Radio Button
but the problem is that selected cell in ListView and selected RadioButton are not bind. If a click on cell in list i want automatically to select the corresponding RadioButton.
So my question is how can i bind this two?
UPDATE:
So the only way i managed to do it is similar to #Sedrick Jefferson answer but without adding StackPane in front of RadioButton.
I add list of RadioButtons namesRadioButtons to ToggleGroup and add listener to selectedToggleProperty: when new RadioButton is selected i select corresponding row in ListView
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class RadioButtonListView extends Application
{
public static final ObservableList<RadioButton> namesRadioButtons
= FXCollections.observableArrayList();
private ToggleGroup group = new ToggleGroup();
#Override
public void start(Stage primaryStage)
{
primaryStage.setTitle("List View Sample");
final ListView<RadioButton> listView = new ListView();
listView.setPrefSize(200, 250);
listView.setEditable(true);
String[] names =
{
"Adam", "Alex", "Alfred", "Albert",
"Brenda", "Connie", "Derek", "Donny",
"Lynne", "Myrtle", "Rose", "Rudolph",
"Tony", "Trudy", "Williams", "Zach"
};
for (String name : names)
{
namesRadioButtons.add(new RadioButton(name));
}
group.getToggles().addAll(namesRadioButtons);
listView.setItems(namesRadioButtons);
group.selectedToggleProperty().addListener((obs, oldSel, newSel) -> {
listView.getSelectionModel().select((RadioButton) newSel);
listView.getFocusModel().focus(listView.getSelectionModel().getSelectedIndex());
});
listView.setCellFactory(param -> new RadioListCell());
listView.getSelectionModel().selectedItemProperty().addListener((obs, oldSel, newSel) ->
{
if (newSel != null)
{
RadioButton tempRadioButton = (RadioButton) newSel;
tempRadioButton.setSelected(true);
}
if (oldSel != null)
{
RadioButton tempRadioButton = (RadioButton) oldSel;
tempRadioButton.setSelected(false);
}
});
StackPane root = new StackPane();
root.getChildren().add(listView);
primaryStage.setScene(new Scene(root, 200, 250));
primaryStage.show();
}
public static void main(String[] args)
{
launch(args);
}
private class RadioListCell extends ListCell<RadioButton>
{
#Override
public void updateItem(RadioButton obj, boolean empty)
{
super.updateItem(obj, empty);
if (empty)
{
setText(null);
setGraphic(null);
}
else
{
setGraphic(obj);
}
}
}
}
Question: Is there any better solution to this?
To repeat: adding controls as data items is not a solution!
Instead, use a custom cell that has-a control as needed and configure with the state of the item/list/selection, just as in the QA cited by the OP. The only part missing is the back-sync (from the radio state to the list selection): to achieve that, install a listener in the cell.
Something like (modified example):
public class RadioButtonListView extends Application {
public static final ObservableList names =
FXCollections.observableArrayList();
private ToggleGroup group = new ToggleGroup();
#Override
public void start(Stage primaryStage) {
primaryStage.setTitle("List View Sample");
final ListView listView = new ListView();
listView.setPrefSize(200, 250);
listView.setEditable(true);
names.addAll(
"Adam", "Alex", "Alfred", "Albert",
"Brenda", "Connie", "Derek", "Donny",
"Lynne", "Myrtle", "Rose", "Rudolph",
"Tony", "Trudy", "Williams", "Zach"
);
listView.setItems(names);
listView.setCellFactory(param -> new RadioListCell());
StackPane root = new StackPane();
root.getChildren().add(listView);
primaryStage.setScene(new Scene(root, 200, 250));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private class RadioListCell extends ListCell<String> {
RadioButton radioButton;
ChangeListener<Boolean> radioListener = (src, ov, nv) -> radioChanged(nv);
WeakChangeListener<Boolean> weakRadioListener = new WeakChangeListener(radioListener);
public RadioListCell() {
radioButton = new RadioButton();
radioButton.selectedProperty().addListener(weakRadioListener);
radioButton.setFocusTraversable(false);
// let it span the complete width of the list
// needed in fx8 to update selection state
radioButton.setMaxWidth(Double.MAX_VALUE);
}
protected void radioChanged(boolean selected) {
if (selected && getListView() != null && !isEmpty() && getIndex() >= 0) {
getListView().getSelectionModel().select(getIndex());
}
}
#Override
public void updateItem(String obj, boolean empty) {
super.updateItem(obj, empty);
if (empty) {
setText(null);
setGraphic(null);
radioButton.setToggleGroup(null);
} else {
radioButton.setText(obj);
radioButton.setToggleGroup(group);
radioButton.setSelected(isSelected());
setGraphic(radioButton);
}
}
}
}
Update:
The example is working fine in fx9 but has issues in fx8:
when clicking outside of the radiobutton (somewhere in the trailing whitespace of a row) the radio selected is not always updated. This can be fixed by forcing the radio to stretch to the full width of the list.
the selected state of the radio is not always updated when the listView's selection changed. This can be handled by installing a listener to the cell's selected property and update the radio in that listener.
the selected state of the radio is not reliably updated when the cell is re-used. This needs further digging ...
I am using the comboBox suggested in this answer: JavaFx: show DatePicker, but I have a problem displaying the correct text. If I chose a date from DatePicker I tried to set the editor's text but it happens nothing.
This is what I have tried:
getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) ->{
if (newValue != null) {
if (MyTupe.DATE.equals(newValue.getType())) {
initDatePicker();
datePicker.valueProperty().addListener((obs, oldDate, newDate) -> {
newValue.setValue(newDate);
getEditor().setText(newDate.toString());
datePopOver.hide();
});
StackPane stackPane = new StackPane(datePicker);
stackPane.setPadding(new Insets(10, 10, 10, 10));
datePopOver.setContentNode(stackPane);
datePopOver.show(this);
} else {
datePopOver.hide();
}
}
});
After experimenting a lot I added two events to the editor's textProperty and setOnMouseClicked like this:
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
System.out.println("Text changed");
});
setOnMouseClicked(event -> {
System.out.println(getEditor().getText());
});
First is not triggered if I don't set manually getEditor().setText(); even if I change the value of the comboBox, that is a little bit weird for me, because I thought this textField holds the text of the comboBox.(maybe am I wrong??). The mouseClick event displays every time an empty string if that line is removed.
If the getEditor().setText(); line is there the editor's getText() returns the correct text but it is not displayed in comboBox.
The questions:
Where is the comboBox's text stored?
How can I change the text in the selectedItemProperty's listener?
I assume that maybe I did somewhere a mistake so that's why this is not working, but I have no idea what, can you help me?
In the linked code
Replace:
items.set(0, new ComboBoxNode(newDate, DATE_TYPE));
with:
items.set(customComboBox.getSelectionModel().getSelectedIndex(), new ComboBoxNode(newDate, DATE_TYPE));
The linked code only changes the date at location zero. In this new code, the date in the appropriate location is changed.
Full Code:
import java.time.LocalDate;
import java.time.Month;
import java.util.Objects;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import org.controlsfx.control.PopOver;
/**
*
* #author blj0011
*/
public class JavaFXApplication155 extends Application
{
private static final String DATE_TYPE = "DATE";
private class ComboBoxNode {
private final Object value;
private final String type;
private ComboBoxNode(final Object value, final String type) {
this.value = value;
this.type = type;
}
#Override
public String toString() {
return Objects.toString(value);
}
}
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
final ObservableList<ComboBoxNode> items =
FXCollections.observableArrayList(
new ComboBoxNode(LocalDate.now(), DATE_TYPE),
new ComboBoxNode(LocalDate.of(2017, Month.APRIL, 15), DATE_TYPE),//Added new date to ComboBox
new ComboBoxNode("11:35AM", "TIME"));
final PopOver datePopOver = new PopOver();
datePopOver.setTitle("Enter new date");
datePopOver.setCornerRadius(10);
datePopOver.setHeaderAlwaysVisible(true);
//datePopOver.set(true);
datePopOver.setAutoHide(true);
final ComboBox<ComboBoxNode> customComboBox = new ComboBox<>(items);
customComboBox.getSelectionModel().selectedItemProperty().addListener((o, old, newNode) -> {
if (newNode != null) {
if (newNode.type.equals(DATE_TYPE)) {
final DatePicker datePicker = new DatePicker((LocalDate) newNode.value);
datePicker.valueProperty().addListener((obs, oldDate, newDate) -> {
items.set(customComboBox.getSelectionModel().getSelectedIndex(), new ComboBoxNode(newDate, DATE_TYPE));//Fixed this line of code
datePopOver.hide();
});
final StackPane stackPane = new StackPane(datePicker);
stackPane.setPadding(new Insets(10, 10, 10, 10));
datePopOver.setContentNode(stackPane);
datePopOver.show(customComboBox);
} else {
datePopOver.hide();
}
}
});
final FlowPane pane = new FlowPane(customComboBox);
pane.setPadding(new Insets(10, 10, 10, 10));
pane.setPrefWidth(400);
pane.setPrefHeight(300);
// Show Scene
final Scene scene = new Scene(pane);
primaryStage.setTitle("Popup calendar");
primaryStage.setScene(scene);
primaryStage.show();
}
}
Regarding your question: where is the comboBox's text stored?
In general JavaFX controls are based on MVC architecture where the data is stored in model. Controls just represent model data according to defined converters and automatically reacts on any changes of model.
So, in your case the data is stored in ObservableList items. To have updated combobox you need just to replace the corresponding item by new object.
Note, the combobox already listens the ObservableList and any add/remove/set will be automatically represented on UI side.
Why this code doesn't work:
newValue.setValue(newDate);
Because you change the internal state of an item and ObservableList items doesn't react on such changes. Only the changing of the list will work. #Sedrick proposed correct solution.
See also the related question.
My problem is as follows,
For the sake of this question I reproduced the problem in a new project.
Say I have this application with a combobox in it, there could be 1 or more items in there. And I would like it to be so that when the user clicks an item in the combobox that 'something' happens.
I produced the following code:
obsvList.add("item1");
cbTest.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
System.out.println("Item clicked");
}
});
This works when the application starts and an item is selected for the first time. This also works when there are 2 or more items in the combobox (when the user clicks item 1, then item 2, then item 1 for example)
However my problem is that when there is only 1 item in the combobox, let's say "item1". And the user reopens the combobox and clicks "item1" again then it won't redo the action.
It will only print the line "Item Clicked" when a 'new' item is clicked.
I hope it made it clear what the problem i'm experiencing is, if not please ask for clarification and I will give so where needed.
Thanks in advance!
The functionality of a combo box is to present the user with a list of options from which to choose. When you are using a control which implies selection, you should really ensure that the UI is always consistent with the option that is selected. If you do this, then it makes no sense to "repeat an action" when the user "reselects" the same option (because the UI is already in the required state). One approach to this is to use binding or listeners on the combo box's value:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ComboBoxExample extends Application {
#Override
public void start(Stage primaryStage) {
ComboBox<Item> choices = new ComboBox<>();
for (int i = 1 ; i <=3 ; i++) {
choices.getItems().add(new Item("Choice "+i, "These are the details for choice "+i));
}
Label label = new Label();
choices.valueProperty().addListener((obs, oldItem, newItem) -> {
label.textProperty().unbind();
if (newItem == null) {
label.setText("");
} else {
label.textProperty().bind(newItem.detailsProperty());
}
});
BorderPane root = new BorderPane();
root.setCenter(label);
root.setTop(choices);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public class Item {
private final String name ;
private final StringProperty details = new SimpleStringProperty() ;
public Item(String name, String details) {
this.name = name ;
setDetails(details) ;
}
public String getName() {
return name ;
}
#Override
public String toString() {
return getName();
}
public final StringProperty detailsProperty() {
return this.details;
}
public final String getDetails() {
return this.detailsProperty().get();
}
public final void setDetails(final String details) {
this.detailsProperty().set(details);
}
}
public static void main(String[] args) {
launch(args);
}
}
In this case, there is never a need to repeat an action when the user "reselects" the same option, because the code always assures that the UI is consistent with what is selected anyway (there is necessarily nothing to do if the user selects the option that is already selected). By using bindings in the part of the UI showing the details (just a simple label in this case), we are assured that the UI stays up to date if the data changes externally. (Obviously in a real application, this may be far more complex, but the basic strategy is still exactly the same.)
On the other hand, functionality that requires an action to be repeated if the user selects the same functionality is better considered as presenting the user with a set of "actions". The appropriate controls for this are things like menus, toolbars with buttons, and MenuButtons.
An example of a set of repeatable actions is:
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class MenuButtonExample extends Application {
#Override
public void start(Stage primaryStage) {
MenuButton menuButton = new MenuButton("Items");
Label label = new Label();
Item[] items = new Item[3];
for (int i = 1 ; i <=3 ; i++) {
items[i-1] = new Item("Item "+i);
}
for (Item item : items) {
MenuItem menuItem = new MenuItem(item.getName());
menuItem.setOnAction(e -> item.setTimesChosen(item.getTimesChosen() + 1));
menuButton.getItems().add(menuItem);
}
label.textProperty().bind(Bindings.createStringBinding(() ->
Stream.of(items)
.map(item -> String.format("%s chosen %d times", item.getName(), item.getTimesChosen()))
.collect(Collectors.joining("\n")),
Stream.of(items)
.map(Item::timesChosenProperty)
.collect(Collectors.toList()).toArray(new IntegerProperty[0])));
BorderPane root = new BorderPane();
root.setCenter(label);
root.setTop(menuButton);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
private final String name ;
private final IntegerProperty timesChosen = new SimpleIntegerProperty();
public Item(String name) {
this.name = name ;
}
public String getName() {
return name ;
}
#Override
public String toString() {
return getName();
}
public final IntegerProperty timesChosenProperty() {
return this.timesChosen;
}
public final int getTimesChosen() {
return this.timesChosenProperty().get();
}
public final void setTimesChosen(final int timesChosen) {
this.timesChosenProperty().set(timesChosen);
}
}
public static void main(String[] args) {
launch(args);
}
}
The idea is to set a listener on the ListView pane, that appears whenever you click on the ComboBox. The ListView instance is created once the ComboBox is first loaded in the JavaFX scene. Therefore, we add a listener on the ComboBox to check when it appears on the scene, and then through the "lookup" method we get the ListView and add a listener to it.
private EventHandler<MouseEvent> cboxMouseEventHandler;
private void initComboBox() {
ComboBox<String> comboBox = new ComboBox<String>();
comboBox.getItems().add("Item 1");
comboBox.getItems().add("Item 2");
comboBox.getItems().add("Item 3");
comboBox.sceneProperty().addListener((a,oldScene,newScene) -> {
if(newScene == null || cboxMouseEventHandler != null)
return;
ListView<?> listView = (ListView<?>) comboBox.lookup(".list-view");
if(listView != null) {
cboxMouseEventHandler = (e) -> {
Platform.runLater(()-> {
String selectedValue = (String) listView.getSelectionModel().getSelectedItem();
if(selectedValue.equals("Item 1"))
System.out.println("Item 1 clicked");
});
}; // cboxMouseEventHandler
listView.addEventFilter(MouseEvent.MOUSE_PRESSED, cboxMouseEventHandler);
} // if
});
} // initComboBox
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.
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));
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 ;
}
}));
Thank you!
This helps me to solve my problem.
Thanks for previous answers.
I miss the information that setCellValueFactory is not needed, but value assigned should also be done in setCellFactory. Here is my approach (much copied from previous solution).
public TreeTableColumn<RowContainer, Boolean> treetblcolHide;
...
treetblcolHide.setCellFactory(CheckBoxTreeTableCell.<RowContainer, Boolean>forTreeTableColumn(new Callback<Integer, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(final Integer param) {
final RowContainer rowitem = treetblcolHide.getTreeTableView().getTreeItem(param).getValue();
BooleanProperty observable = new SimpleBooleanProperty();
observable.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
rowitem.setHideMenuItem(newValue.toString());
}
}
);
observable.setValue(Boolean.parseBoolean(rowitem.getHideMenuItem()));
return observable ;
}
}));
Debugging for the code below, it shows that the updateItem() method is called multiple times, but I am unable to figure out why its being called multiple times.
I wanted to add the Tooltip to the ListView.
// THIS TO ADD TOOLTIP, NOT WORKING FULLY.
lstComments.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> p) {
final Tooltip tt = new Tooltip();
final ListCell<String> cell = new ListCell<String>() {
String message = ca.getMessage();
#Override
public void updateItem(String s, boolean empty) {
super.updateItem(s, empty);
tt.setText(message);
setTooltip(tt);
}
};
cell.setText(ca.getMessage());
return cell;
}
});
Recommendation
I find the usability of Tooltips on ListView cells horrible, because the Tooltips intercept standard mouse events used to select rows, scroll the list, etc. So I would not recommend placing Tooltips on ListView cells.
Why multiple cells are created and updateItem is called multiple times
It is expected that a ListView have multiple cells and that updateItem() is potentially called multiple times for each cell.
A cell is created for every row in the ListView that is displayed on the scene, even if some cells are empty. A couple more cells that are offscreen are usually created for efficient scroll handling. Each time the underlying data for a ListView is initially set or modified, or the list is scrolled, updateItem() will be invoked on relevant cells to update the cell's contents. In the case of scrolling a large list, updateItem() will be invoked many, many times for each cell.
Sample code for setting a Tooltip on ListView Cells
The code below is based on the Oracle JavaFX tutorial ListView sample, but customizes it to create Tooltips for cells when you hover over them.
mport javafx.application.Application;
import javafx.collections.*;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewSample extends Application {
ListView<String> list = new ListView<String>();
ObservableList<String> data = FXCollections.observableArrayList(
"chocolate", "salmon", "gold", "coral", "darkorchid",
"darkgoldenrod", "lightsalmon", "black", "rosybrown", "blue",
"blueviolet", "brown");
final Label label = new Label();
#Override
public void start(Stage stage) {
VBox box = new VBox();
Scene scene = new Scene(box, 200, 200);
stage.setScene(scene);
stage.setTitle("ListViewSample");
box.getChildren().addAll(list, label);
VBox.setVgrow(list, Priority.ALWAYS);
label.setLayoutX(10);
label.setLayoutY(115);
label.setFont(Font.font("Verdana", 20));
list.setItems(data);
list.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override public ListCell<String> call(ListView<String> list) {
return new ColorRectCell();
}
});
list.getSelectionModel().selectedItemProperty().addListener(
(ov, old_val, new_val) -> {
label.setText(new_val);
label.setTextFill(Color.web(new_val));
});
stage.show();
}
static class ColorRectCell extends ListCell<String> {
final Rectangle swatch = new Rectangle(30, 30);
final Tooltip tip = new Tooltip();
public ColorRectCell() {
tip.setGraphic(swatch);
}
#Override
public void updateItem(String color, boolean empty) {
super.updateItem(color, empty);
if (color != null) {
swatch.setFill(Color.valueOf(color.toUpperCase()));
setText(color);
setTooltip(tip);
} else {
setText("");
setTooltip(null);
}
}
}
public static void main(String[] args) {
launch(args);
}
}