For some time i have been trying to get my tableview work as kind of spreadsheet that is updated by background thread and when cell get updated, it for few seconds higlights ( changes style ) and then goes back to original style.
I already know, that i can't store and set styles directly in table cell and i need some kind of backing class, that will hold this data. But tableview with its "reusing" of cells (using same cells for different data) acts really weird. When all cells fits on screen it works flawlessly for me, but once i place around 100 cells and it becomes scrollable it starts being buggy, sometimes styles ( or setted graphic) disappears and after scrolling appears, if i disable some top cells of view, some other cells after scrolling get disabled as well and so on. Is there any right way to do this?
What i need basically is
Background data thread ---updates--> tableview
Another thread --after few seconds removes style--> tableview
As i have it now, i have model class that holds data, style and reference to table cell where it should be ( i disabled ordering, so it should be ok ) and background thread updates data in model class, and that model class changes style on referenced cell and register itself in "style remover" thread, that after while removes style.
I think posting my actual code won't be useful, because once i've discovered that cells are being reused my code has become too complicated and a little bit unreadable so i want to completely redo it right way.
Peformance is not that important for me, there wont be more than 100 cells, but this highlighting and having buttons in tableview must work flawlessly.
This is how my app looks like now - for idea of what i need.
EDIT: here is link to my another question related to this.
The collaborators:
on the data side, a (view) model which has a recentlyChanged property, that's updated whenever the value is changed
on the view side, a custom cell that listens to that recentlyChanged property and updates its style as appropriate
The tricky part is to clean up cell state when re-used or not-used: the method that's always (hopefully!) called is cell.updateIndex(int newIndex), so that's the place to un-/register the listener.
Below a runnable (though crude ;) example
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import de.swingempire.fx.util.FXUtils;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TableCoreRecentlyChanged extends Application {
public static class RecentChanged extends TableCell<Dummy, String> {
private ChangeListener<Boolean> recentListener = (src, ov, nv) -> updateRecentStyle(nv);
private Dummy lastDummy;
/*
* Just to see any effect.
*/
protected void updateRecentStyle(boolean highlight) {
if (highlight) {
setStyle("-fx-background-color: #99ff99");
} else {
setStyle("-fx-background-color: #009900");
}
}
#Override
public void updateIndex(int index) {
if (lastDummy != null) {
lastDummy.recentlyChangedProperty().removeListener(recentListener);
lastDummy = null;
}
updateRecentStyle(false);
super.updateIndex(index);
if (getTableRow() != null && getTableRow().getItem() != null) {
lastDummy = getTableRow().getItem();
updateRecentStyle(lastDummy.recentlyChangedProperty().get());
lastDummy.recentlyChangedProperty().addListener(recentListener);
}
}
#Override
protected void updateItem(String item, boolean empty) {
if (item == getItem()) return;
super.updateItem(item, empty);
if (item == null) {
super.setText(null);
super.setGraphic(null);
} else {
super.setText(item);
super.setGraphic(null);
}
}
}
private Parent getContent() {
TableView<Dummy> table = new TableView<>(createData(50));
table.setEditable(true);
TableColumn<Dummy, String> column = new TableColumn<>("Value");
column.setCellValueFactory(c -> c.getValue().valueProperty());
column.setCellFactory(e -> new RecentChanged());
column.setMinWidth(200);
table.getColumns().addAll(column);
int editIndex = 20;
Button changeValue = new Button("Edit");
changeValue.setOnAction(e -> {
Dummy dummy = table.getItems().get(editIndex);
dummy.setValue(dummy.getValue()+"x");
});
HBox buttons = new HBox(10, changeValue);
BorderPane content = new BorderPane(table);
content.setBottom(buttons);
return content;
}
private ObservableList<Dummy> createData(int size) {
return FXCollections.observableArrayList(
Stream.generate(Dummy::new)
.limit(size)
.collect(Collectors.toList()));
}
private static class Dummy {
private static int count;
ReadOnlyBooleanWrapper recentlyChanged = new ReadOnlyBooleanWrapper() {
Timeline recentTimer;
#Override
protected void invalidated() {
if (get()) {
if (recentTimer == null) {
recentTimer = new Timeline(new KeyFrame(
Duration.millis(2500),
ae -> set(false)));
}
recentTimer.playFromStart();
} else {
if (recentTimer != null) recentTimer.stop();
}
}
};
StringProperty value = new SimpleStringProperty(this, "value", "initial " + count++) {
#Override
protected void invalidated() {
recentlyChanged.set(true);
}
};
public StringProperty valueProperty() {return value;}
public String getValue() {return valueProperty().get(); }
public void setValue(String text) {valueProperty().set(text); }
public ReadOnlyBooleanProperty recentlyChangedProperty() { return recentlyChanged.getReadOnlyProperty(); }
public String toString() {return "[dummy: " + getValue() + "]";}
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent()));
// primaryStage.setTitle(FXUtils.version());
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableCoreRecentlyChanged.class.getName());
}
Related
I am developing a library application using javafx, where the user can import epub files, and he gets a library with shelves, where each shelf contains at most 6 books. I am using a scrollpane inside it a VBox containing additional VBoxes(each one resembles a shelf) each one contains a image(which is the shelf),and above it an HBox containing images of book covers. I tried with the listview but it doesn't work since a listview lists a list of items that you click on one of them, and in my case the item will be the entire shelf which contains several books(I want to handle the click on each book individually). Sorry for the long description.
This is the image represention of my work
There are many options for this; but likely the best is using a ListView as the comments suggest.
The sample application below will demonstrate one way to do so. I have not done any work on styling the ListView, however. Mostly because I am not very proficient in CSS myself (I welcome edits and suggestions), but also because that is out of the scope of this fairly vague question.
Combining a ListView with a custom CellFactory, you are able to build a layout for each "shelf" in your library; the ListView will display each row using that layout.
There are additional comments in the code below.
Library Example MCVE:
LibraryExample.java:
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Orientation;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import java.util.List;
public class LibraryExample extends Application {
// Our list of shelves that will be displayed in the ListView
private final ObservableList<Shelf> shelves = FXCollections.observableArrayList();
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);
// Build a list of 100 sample books. This list could come from a database or other outside source, of course
List<Book> books = FXCollections.observableArrayList();
for (int i = 0; i < 100; i++) {
books.add(new Book("Book #" + i, new ImageView("sample/generic-cover.png")));
}
// We will now create our shelves for the books. We will limit the number of books to 6 per shelf. This uses
// the subList method of our List to grab every 6 books until we run out.
int index = 0;
while (index < books.size()) {
// Make sure there are at least 6 books remaining, otherwise, we need to get the subList up to the size of
// the original list.
final int numToAdd = (index + 6 <= books.size() ? index + 6 : books.size());
shelves.addAll(new Shelf(books.subList(index, numToAdd)));
index += 6;
}
// Now, let's create our ListView that will hold our shelves.
ListView<Shelf> listView = new ListView<>();
VBox.setVgrow(listView, Priority.ALWAYS);
// Now for the magic. We will override the CellFactory for the ListView so we can provide our own layout
// for each row
listView.setCellFactory(new Callback<ListView<Shelf>, ListCell<Shelf>>() {
#Override
public ListCell<Shelf> call(ListView<Shelf> param) {
return new ShelfListCell();
}
});
listView.setItems(shelves);
root.getChildren().add(listView);
// Show the Stage
primaryStage.setWidth(700);
primaryStage.setHeight(600);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
class ShelfListCell extends ListCell<Shelf> {
#Override
protected void updateItem(Shelf shelf, boolean empty) {
super.updateItem(shelf, empty);
if (shelf == null || empty) {
setGraphic(null);
} else {
// Here, we will build our layout for each shelf
VBox root = new VBox(5);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(5));
HBox hBox = new HBox(20);
hBox.setAlignment(Pos.CENTER);
hBox.setPadding(new Insets(5));
// Add image for each each book on this shelf to the layout
for (Book book : shelf.getBooks()) {
// Get the image of the book and add a simple click listener
ImageView cover = book.getCoverImage();
cover.setPreserveRatio(true);
cover.setFitHeight(100);
cover.setOnMouseClicked(event -> System.out.println("Clicked " + book.getTitle()));
hBox.getChildren().add(book.getCoverImage());
}
root.getChildren().addAll(hBox, new Separator(Orientation.HORIZONTAL));
// Set the cell to display our layout
setGraphic(root);
}
}
}
Book.java:
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.image.ImageView;
public class Book {
private final StringProperty title = new SimpleStringProperty();
private final ObjectProperty<ImageView> coverImage = new SimpleObjectProperty<>();
public Book(String title, ImageView coverImage) {
this.title.set(title);
this.coverImage.set(coverImage);
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public ImageView getCoverImage() {
return coverImage.get();
}
public ObjectProperty<ImageView> coverImageProperty() {
return coverImage;
}
public void setCoverImage(ImageView coverImage) {
this.coverImage.set(coverImage);
}
}
Shelf.java:
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.util.List;
public class Shelf {
// Set max number of books per shelf
private final static int MAX_BOOKS = 6;
// Our observable list of books
private final ListProperty<Book> books = new SimpleListProperty<>(FXCollections.observableArrayList());
public Shelf(List<Book> books) {
this.books.addAll(books);
}
public void addBooks(Book... books) {
this.books.addAll(books);
}
public static int getMaxBooks() {
return MAX_BOOKS;
}
public ObservableList<Book> getBooks() {
return books.get();
}
public ListProperty<Book> booksProperty() {
return books;
}
public void setBooks(ObservableList<Book> books) {
this.books.set(books);
}
}
The Result:
I'm working on a little game. So I create the game interface (countains the gun, the canvas which I use as gamespace and an interface for the user to control is gun). I place the different elements in the window and there is my problem. When I execute my code, all is well placed but once I use one of the buttons (buttons in both codes) or the slider (second code), the slider and the fire button replace themselves. And I don't understand why because I never asked this rellocation in my code. Also, when the items rellocate, I can't use any other items excepted the fire button and the slider.
Here are screenshots of what I have before using a button (first screenshot) (it's also how I want the interface to be) and the second screenshot shows the rellocation I have.
How it looks when I use nothing.
How it looks when I use a button or the slider.
Main.java :
package application;
import bureaux.Bureau;
import canons.Canon;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.control.Button;
import javafx.scene.control.ToolBar;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class Main extends Application{
private StackPane root, rootBureau;
private Scene scene;
private Stage stage;
private Text joueur;
private Button menu, musique, ajoutJoueur;
private FlowPane rootJeu;
private Bureau bureauJoueur;
private ToolBar toolBar;
private Canon canonJoueur;
#Override
public void start(Stage primaryStage) throws IOException {
getRoot();
getScene();
stage = primaryStage;
creerInterface();
stage.setTitle("Mad Java Guns");
stage.setResizable(false);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
public void creerInterface(String mode) { // creating the gamespace and the objects that the user need to play
getToolBar().getItems().addAll(getMusique(), getAjoutJoueur());
getRootBureau().getChildren().add(getBureauJoueur());
getRootJeu().getChildren().add(getCanonJoueur());
getRoot().getChildren().addAll(getToolBar(), getJoueur(), getRootBureau(), getRootJeu());
}
// Getters
public StackPane getRoot() {
if(root == null) {
root = new StackPane();
}
return root;
}
public Scene getScene() {
if(scene == null) {
scene = new Scene(root,1000,800);
}
return scene;
}
public Text getJoueur() { // gamespace
if(joueur == null) {
joueur = new Text("Espace de jeu");
joueur.setFont(Font.font("Arial", 20));
joueur.setTranslateY(120);
}
return joueur;
}
public ToolBar getToolBar() {
if(toolBar == null) {
toolBar = new ToolBar();
toolBar.setTranslateY(122);
toolBar.setTranslateX(3);
toolBar.setStyle("-fx-background-color: transparent");
}
return toolBar;
}
public Button getMusique() { // button too change the music in game
if (musique == null) {
musique = new Button("Musique");
musique.setOnMouseClicked(e -> {
System.out.println("musique"); // not coded yet
});
musique.setFocusTraversable(false);
}
return musique;
}
public Button getAjoutJoueur() { // add players in the game
if(ajoutJoueur == null) {
ajoutJoueur = new Button("Ajouter un joueur");
ajoutJoueur.setOnMouseClicked(e -> {
System.out.println("ajoutJoueur"); //not coded yet
});
ajoutJoueur.setFocusTraversable(false);
}
return ajoutJoueur;
}
public StackPane getRootBureau() { // pane where the user's interface will be placed
if(rootBureau == null) {
rootBureau = new StackPane();
rootBureau.setStyle("-fx-background-color: lightgrey");
rootBureau.setMaxSize(990, 250);
rootBureau.setTranslateY(270);
}
return rootBureau;
}
public Bureau getBureauJoueur() { // user's interface
if(bureauJoueur == null) {
bureauJoueur = new Bureau("Billy", getCanonJoueur());
}
return bureauJoueur;
}
}
Class Bureau.java :
package bureaux;
import canons.Canon;
import javafx.geometry.Orientation;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Slider;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
public class Bureau extends Parent {
private Slider sliderCanon;
private HBox boxPrincipale;
private VBox boxControlesCanon;
private Button feu;
public Bureau(String nom, Canon canon) {
getBoxControlesCanon().getChildren().addAll(getSliderCanon(), getFeu());
getBoxPrincipale().getChildren().add(getBoxControlesCanon());
this.setTranslateX(-480); // placing the boxes
this.setTranslateY(-95);
this.getChildren().add(getBoxPrincipale());
}
//Getteurs
public HBox getBoxPrincipale() {
if(boxPrincipale == null) { // return a HBox which countains the VBox (next function)
// and other elements which aren't created yet.
boxPrincipale = new HBox();
}
return boxPrincipale;
}
public VBox getBoxControlesCanon() { // return a VBox which countains the controls of the gun
//(gun not showed in the code, doesn't concern the problem)
if(boxControlesCanon == null) {
boxControlesCanon = new VBox();
boxControlesCanon.setSpacing(20);
}
return boxControlesCanon;
}
public Slider getSliderCanon() { //slider to orient the gun (gun not showed in the code, doesn't concern the problem)
if(sliderCanon == null) {
sliderCanon = new Slider(0, 360, 0);
sliderCanon.setOrientation(Orientation.VERTICAL);
sliderCanon.valueProperty().addListener(e -> {
System.out.println(sliderCanon.getValue());
});
sliderCanon.setShowTickMarks(true);
sliderCanon.setShowTickLabels(true);
sliderCanon.setMajorTickUnit(90f);
}
return sliderCanon;
}
public Button getFeu() { // fire button
if(feu == null) {
feu = new Button("Feu");
feu.setOnMouseClicked(e -> {
System.out.println("Feu");
});
feu.setFocusTraversable(false);
}
return feu;
}
}
Please ask for more informations if necessary. Thanks for your help.
EDIT : sorry for the unpoliteness on top of this text, I used to edit it and add "Hello" but it just don't want to show it :/
You are using Bureau extends Parent. You will have to be more specific and used the nodes that will produce the outcome you need.
Try something like Bureau extends HBox. Then
getBoxControlesCanon().getChildren().add(new VBox(getSliderCanon(), getFeu()));
To get the left alignment, you may need to do something like
getBoxControlesCanon().getChildren().addAll(new VBox(getSliderCanon(), getFeu()), someOtherNode);
HBox.setHGrow(someOtherNode, Priority.ALWAYS);
If you look at Parent compared to VBox, you will see that Parent does not describe how children nodes will be laid out. A lot of nodes that are a subclass of Parent do describe how their children nodes will be laid out.
I wrote the following method that is used to react when I press a button:
private void handlePlayButton(ActionEvent e){
for (int i = 0; i < commands.size(); i++) {
list.getSelectionModel().select(i);
try {
Thread.sleep(1000);
} catch (InterruptedException ie){
System.out.println("Error at handlPlayButton: interruption");
}
}
}
In this code I try to select each element starting at the first line and than wait 1 second to select the next one, but it seems like it waits n-1 seconds (where n is the size of the items) and than selects the last item. Is there any way to fix this?
The field list is a ListView<String> by the way.
This question is relatively obscure, so I don't think the solution will be generally applicable to anybody else. The basic solution is to use a Timeline to automate updating the selection in the ListView when the user presses a "Cycle" button in the UI.
There is a bit of additional logic to handle edge cases such as what to do if the user modifies the selection while the cycling is ongoing or if the user restarts the cycling process. If the user clicks on the currently selected item, the automated cycling does not stop, so the original asker might wish to add some of his own code to do that if he adopts a similar solution.
Also there is some logic for placing ImageViews in the ListView, but that isn't central to the application and can be ignored for more common types stored used in a ListView such as Strings. The ImageView related stuff is just there to make the app look a bit better.
import javafx.animation.*;
import javafx.application.Application;
import javafx.collections.*;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.Arrays;
import java.util.stream.Collectors;
public class RotatingSushiMenu extends Application {
private static final Duration AUTO_CHANGE_PAUSE = Duration.seconds(2);
private boolean autoChange;
#Override
public void start(Stage stage) {
ObservableList<Image> images = FXCollections.observableList(
Arrays.stream(IMAGE_LOCS)
.map(Image::new)
.collect(Collectors.toList())
);
ListView<Image> list = new ListView<>(FXCollections.observableList(images));
list.setCellFactory(param -> new ImageListCell());
Timeline timeline = new Timeline(
new KeyFrame(Duration.ZERO),
new KeyFrame(
AUTO_CHANGE_PAUSE,
e -> {
int curIdx = list.getSelectionModel().getSelectedIndex();
if (curIdx < list.getItems().size() - 1) {
autoChange = true;
list.scrollTo(curIdx + 1);
list.getSelectionModel().select(curIdx + 1);
autoChange = false;
}
}
)
);
timeline.setCycleCount(list.getItems().size());
Button cycle = new Button("Cycle");
cycle.setOnAction(event -> {
if (list.getItems().size() > 0) {
list.scrollTo(0);
list.getSelectionModel().select(0);
timeline.playFromStart();
}
});
list.getSelectionModel().getSelectedItems().addListener((ListChangeListener<Image>) c -> {
if (!autoChange) {
timeline.stop();
}
});
VBox layout = new VBox(10, cycle, list);
layout.setPadding(new Insets(10));
stage.setScene(new Scene(layout));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private class ImageListCell extends ListCell<Image> {
final ImageView imageView = new ImageView();
ImageListCell() {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Image item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
imageView.setImage(null);
setText(null);
setGraphic(null);
}
imageView.setImage(item);
setGraphic(imageView);
}
}
// image license: linkware - backlink to http://www.fasticon.com
private static final String[] IMAGE_LOCS = {
"http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Blue-Fish-icon.png",
"http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Red-Fish-icon.png",
"http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Yellow-Fish-icon.png",
"http://icons.iconarchive.com/icons/fasticon/fish-toys/128/Green-Fish-icon.png"
};
}
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 ;
}
}));
I've written the following code.
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.util.Callback;
public class App extends Application {
private ListView<String> listView;
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
List<String> friendList = new ArrayList<String>();
friendList.add("Alice");
friendList.add("Bob");
listView = new ListView<>(FXCollections.observableArrayList(friendList));
listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> p) {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String t, boolean empty) {
super.updateItem(t, empty);
if (t != null) {
Label usernameLabel = new Label(t);
usernameLabel.setFont(Font.font("Arial", FontWeight.BOLD, 12));
Button callButton = new Button("Call");
callButton.setOnAction(e -> System.out.println("action")); // not working
callButton.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> System.out.println("entered"));
callButton.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("clicked")); // not working
HBox usernameBox = new HBox(5);
usernameBox.setAlignment(Pos.CENTER_LEFT);
usernameBox.getChildren().addAll(usernameLabel);
BorderPane borderPane = new BorderPane();
borderPane.setLeft(usernameBox);
borderPane.setRight(callButton);
VBox vbox = new VBox(3);
vbox.getChildren().addAll(borderPane);
setGraphic(vbox);
}
}
};
return cell;
}
});
stage.setScene(new Scene(listView));
stage.show();
}
}
If you look at the callButton, you see that it gets three different handlers. However, only the MOUSE_ENTERED event handler is really triggered. The other ones are completely ignored. What can be the problem?
EDIT: Added and removed some code, in order to make it runnable.
This is a known bug in JavaFX 8, which is fixed in the latest ea release (1.8.0_20).
As a workaround, create the controls once and register handlers with them, then just update their state in the updateItem(...) method:
listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> p) {
Label usernameLabel = new Label();
usernameLabel.setFont(Font.font("Arial", FontWeight.BOLD, 12));
Button callButton = new Button("Call");
HBox usernameBox = new HBox(5);
usernameBox.setAlignment(Pos.CENTER_LEFT);
usernameBox.getChildren().addAll(usernameLabel);
BorderPane borderPane = new BorderPane();
borderPane.setLeft(usernameBox);
borderPane.setRight(callButton);
VBox vbox = new VBox(3);
vbox.getChildren().addAll(borderPane);
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String t, boolean empty) {
super.updateItem(t, empty);
if (t != null) {
usernameLabel.setText(t);
setGraphic(vbox);
} else {
setGraphic(null); // you will have weird bugs without this: don't omit it
}
}
};
callButton.setOnAction(e -> System.out.println("action: "+cell.getItem()));
callButton.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> System.out.println("entered "+ cell.getItem()));
callButton.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("clicked "+ cell.getItem()));
return cell;
}
});
Note that this "workaround" is really the preferred approach anyway, and the one that was intended by the designers of the "virtualized" controls like ListView, TableView, etc. The point is that updateItem(...) is potentially called very frequently by the application, whereas cells are created very rarely. By creating new controls in the updateItem(...) method you potentially introduce performance issues. Create them once for the cell, and then just configure them in updateItem(...). Note also how I just registered the event handlers once, and had the handlers refer to cell.getItem() to see which item is currently represented by the cell.
One last thing: you have a bug in your code (which I fixed). Since cells can be reused, including for the case where a cell displaying an item is reused for an empty cell, it's important that you always handle the case where the item is null (typically by setting text and/or graphic to null).
Could you add the code of getIconAndResizeTo16( String s ). I guess the node you return there consumes mouse clicks.
Here is a runnable example that demonstrates the issue. It is just a guess though.
import javafx.application.Application;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class App extends Application {
public static void main(String[] args) { launch(args); }
#Override
public void start(Stage stage) throws Exception {
Button callButton = new Button("", getIconAndResizeTo16("Phone"));
callButton.setOnAction(e -> System.out.println("clicked1")); // not working
callButton.addEventHandler(MouseEvent.MOUSE_ENTERED, e -> System.out.println("entered"));
callButton.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> System.out.println("clicked")); // not working
Button chatButton = new Button("", getIconAndResizeTo16("Chat") );
chatButton.setOnAction(e -> System.out.println("clicked2")); // not working
HBox callIconBox = new HBox(3);
callIconBox.setAlignment(Pos.CENTER_RIGHT);
callIconBox.getChildren().addAll(callButton, chatButton);
stage.setScene(new Scene(callIconBox));
stage.show();
}
private Node getIconAndResizeTo16(String s) {
Label l = new Label("Consumes " + s + " Events");
l.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> { e.consume(); });
l.addEventHandler(MouseEvent.MOUSE_CLICKED, e -> { e.consume(); });
return l;
}
}