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:
Related
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.
TL;DR: Listener is also activated on other cells.
I have a TreeView containing different TreeItems representing data of a custom class. If a BooleanProperty of the underlying data changes, the cell should change its colour. If it changes again, the colour should be removed.
I use a listener, but when I scroll through the TreeView, a changing property of a certain cell also changes the colour of other cells. This behaviour can be reproduced when running my MWE and right clicking on some cells, scrolling, clicking again, and so on. The TreeView might be cleaned by just scrolling so that the concerned cells are out a view for a moment.
I could remove the listener, but then the colour will only be changed, if the cell reappears after scrolling away and back to it.
The question is: How can I use a listener properly in a cellFactory?
MWE
CellFactoryQuestion.java
package cellfactoryquestion;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class CellFactoryQuestion extends Application {
/** Custom class used as underlying data of TreeItems */
class CustomObject {
String label;
BooleanProperty state = new SimpleBooleanProperty(false);
CustomObject(String s) { label = s; }
}
/** Cell Factory for CustomObject */
class CustomTreeCell extends TreeCell<CustomObject>{
PseudoClass customClass = PseudoClass.getPseudoClass("custom");
#Override
protected void updateItem(CustomObject co, boolean empty) {
super.updateItem(co, empty);
if (empty || co == null) {
setText(null);
setGraphic(null);
pseudoClassStateChanged(customClass, false);
} else {
setText(co.label);
setGraphic(null);
// BEGIN PROBLEMATIC
/* define background color of cell according to state */
pseudoClassStateChanged(customClass, co.state.getValue());
co.state.addListener((o, ov, nv) -> {
pseudoClassStateChanged(customClass, nv);
});
// END PROBLEMATIC
/* if right click, switch state */
this.setOnContextMenuRequested(e -> {
co.state.setValue(co.state.getValue() ^ true);
});
}
}
}
#Override
public void start(Stage primaryStage) {
/* define TreeView 1/3 */
TreeView tw = new TreeView();
TreeItem rootTreeItem = new TreeItem(new CustomObject("Root"));
rootTreeItem.setExpanded(true);
/* define TreeView 2/3 */
for (int c = 0; c != 5; c++) {
TreeItem ci = new TreeItem(new CustomObject("Cat " + c));
rootTreeItem.getChildren().add(ci);
ci.setExpanded(true);
for (int i = 0; i != 5; i++) {
TreeItem ii = new TreeItem(new CustomObject("Item " + i));
ci.getChildren().add(ii);
}
}
/* define TreeView 3/3 */
tw.setRoot(rootTreeItem);
tw.setCellFactory(value -> new CustomTreeCell());
/* define Scene */
StackPane root = new StackPane();
root.getChildren().add(tw);
Scene scene = new Scene(root, 300, 250);
scene.getStylesheets().add("/styles/Styles.css");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Styles.css
.tree-cell:custom {
-fx-background-color: salmon;
}
The problem is that you do not unregister the listener. Do this before calling super.updateItem. This allows you to retrieve the old item using getItem:
class CustomTreeCell extends TreeCell<CustomObject>{
private final ChangeListener<Boolean> listener = (o, ov, nv) -> pseudoClassStateChanged(customClass, nv);
PseudoClass customClass = PseudoClass.getPseudoClass("custom");
#Override
protected void updateItem(CustomObject co, boolean empty) {
// remove listener from old item
CustomObject oldItem = getItem();
if (oldItem != null) {
oldItem.state.removeListener(listener);
}
super.updateItem(co, empty);
if (empty || co == null) {
setText(null);
setGraphic(null);
pseudoClassStateChanged(customClass, false);
} else {
setText(co.label);
setGraphic(null);
/* define background color of cell according to state */
pseudoClassStateChanged(customClass, co.state.getValue());
co.state.addListener(listener);
...
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.
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);
}
}