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);
}
}
Related
I have a JavaFX ComboBox with several text choices. It would be nice if the choices used center alignment instead of left alignment, but I haven't figured out how to do this.
Following is a style that I use. I added the clause "-fx-text-alignment:center;" but it had no effect on the placement of the strings in the combobox.
normalStyle = "-fx-font-family:san serif;"
+ "-fx-font-size:12;"
+ "-fx-text-alignment:center;"
+ "-fx-font-weight:normal;";
The style is attached to the Combo Box as follows:
cbChoices.setStyle(normalStyle);
I'm noticing that the size and the weight of Combo Box entries will respond to changes of the above, but not alignment.
I'd prefer not to add spaces to the beginning of my Strings to get them to line up. Is there another way?
You need to set the style on each individual ListCell within the ComboBox, not on the ComboBox itself.
You can do this by providing your own ListCell implementation with the setCellFactory() method:
// Provide our own ListCells for the ComboBox
comboBox.setCellFactory(lv -> new ListCell<String>() {
// We override the updateItem() method
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
// Set the style for this ListCell
setStyle("-fx-alignment: center");
// If there is no item for this cell, leave it empty, otherwise show the text
if (item != null && !empty) {
setText(item);
} else {
setText(null);
}
}
});
Sample Application:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class ComboBoxAlignment extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple interface
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// Simple ComboBox with items
ComboBox<String> comboBox = new ComboBox<>();
comboBox.getItems().addAll("Red", "Orange", "Yellow", "Green", "Blue", "Indigo", "Violet");
// Provide our own ListCells for the ComboBox
comboBox.setCellFactory(lv -> new ListCell<String>() {
// We override the updateItem() method
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
// Set the style for this ListCell
setStyle("-fx-alignment: center");
// If there is no item for this cell, leave it empty, otherwise show the text
if (item != null && !empty) {
setText(item);
} else {
setText(null);
}
}
});
root.getChildren().add(comboBox);
// Show the Stage
primaryStage.setWidth(300);
primaryStage.setHeight(300);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
The Result:
How can I change the background color of only the first cell in listview in JavaFX?I only want to change the background color of the first cell in the listview. Is there any way to do this.
You would need to implement a custom CellFactory on the ListView. We can then determine if the cell belongs to the first item in the List you used to populate the Listview. If so, apply a different style to just that cell.
I am not aware if there is a way to determine the first cell of a ListView, but we can certainly capture the first item in a List.
Consider the following application. We have a ListView that just displays a list of strings.
We set a custom CellFactory on the ListView and set the cell style if the item is the first in the List populating the ListView.
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple interface
VBox root = new VBox(5);
root.setPadding(new Insets(10));
root.setAlignment(Pos.CENTER);
// Create the ListView
ListView<String> listView = new ListView<>();
listView.getItems().setAll("Title", "One", "Two", "Three", "Four", "Five");
// Set the CellFactory for the ListView
listView.setCellFactory(list -> {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
// There is no item to display in this cell, so leave it empty
setGraphic(null);
// Clear the style from the cell
setStyle(null);
} else {
// If the item is equal to the first item in the list, set the style
if (item.equalsIgnoreCase(list.getItems().get(0))) {
// Set the background color to blue
setStyle("-fx-background-color: blue; -fx-text-fill: white");
}
// Finally, show the item text in the cell
setText(item);
}
}
};
return cell;
});
root.getChildren().add(listView);
// Show the Stage
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
The Result
Obviously, you'll need to make some adjustments to match your data model and just matching by a String would not be the best approach.
This does not prevent the user from selecting the first item and may not work as expected if the list is sorted after building the Scene.
While this may answer your direct question, there are other things to consider in order to ensure a good experience for the user.
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.
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 have a ListView with some Labels in it. The labels' width property is bound to the width property of the ListView but they seem to be slightly larger meaning that a horizontal scrollbar is shown on the list view. What I want is to fit the labels in the list view without the scrollbar on the bottom. I have looked at various padding and insets values on both the label and the list view but none I have found are the culprit (most are zero).
Here is an example which demonstrates the problem.
import javafx.application.Application;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
public class ListViewScrollExample extends Application {
private ListView<Node> listView;
#Override
public void start(Stage stage) throws Exception {
listView = new ListView<>();
addItem("Some quite long string to demonstrate the problem");
Scene scene = new Scene(listView);
stage.setScene(scene);
stage.show();
}
public void addItem(String item) {
Label label = new Label(item);
label.setWrapText(true);
label.maxWidthProperty().bind(listView.widthProperty());
listView.getItems().add(label);
}
public static void main(String[] args){
Application.launch(args);
}
}
The default CSS file adds padding to a ListCell (line 2316 in the current release):
.list-cell {
-fx-padding: 0.25em 0.583em 0.25em 0.583em; /* 3 7 3 7 */
}
It generally a bad idea to use Node instances as the data backing a ListView: you should use String in this example, and use the cell factory to create a label displaying the string that is configured as you need. The following seems to work for your example:
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.scene.Scene;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.stage.Stage;
public class ListViewScrollExample extends Application {
private ListView<String> listView;
#Override
public void start(Stage stage) throws Exception {
listView = new ListView<>();
listView.getItems().add("Some quite long string to demonstrate the problem");
listView.setCellFactory(lv -> {
ListCell<String> cell = new ListCell<String>() {
private Label label = new Label();
{
label.setWrapText(true);
label.maxWidthProperty().bind(Bindings.createDoubleBinding(
() -> getWidth() - getPadding().getLeft() - getPadding().getRight() - 1,
widthProperty(), paddingProperty()));
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
label.setText(item);
setGraphic(label);
}
}
};
return cell ;
});
Scene scene = new Scene(listView);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args){
Application.launch(args);
}
}
Here I created a list cell that displays a label as its graphic, with the text of the label set to the string to be displayed. The constructor for the cell binds the label's max width to the width of the cell, less any space required for padding. The call to setContentDisplay(ContentDisplay.GRAPHIC_ONLY) appears necessary, so the cell doesn't try to allocate any space for text.
It may be possible to do this by setting the text directly on the list cell and calling setWrapText(true) on the cell (which is, after all, also a subclass of Labeled), but I couldn't get it to work this way.
I couldn't replicate the problem but you can try the following instead of label.maxWidthProperty().bind(listView.widthProperty());
double i = Double.parseDouble(listView.widthProperty().toString());
label.setMaxWidth((i-2.0));
You can change the 2.0 to any pixel count you need to alter the screen by.