I'm trying to make a TableView with an embedded ListView, but I'm not exactly sure what to do, I've been researching how to embed Buttons into a TableColumn and I've seen that I should create a custom class that extends TableColumn and overrides updateItem().
I have a:
#FXML
private TableColumn<FoodModel, ObservableSet<Food>> storeFood;
for the tableColumn, set by the FXML editor. It's value is set by this.storeFood.setCellValueFactory(val -> val.getValue().getFood); and this.storeFood.setCellFactory(value -> new ListViewCell<>()); upon initialization.
I've been encountering a problem in which the list on screen is not being populated. Can I have a checklist of things to do to embed a list into a TableColumn?
Cell:
private static final class ListViewCell<T, V> extends TableCell<T, V> {
private ListView<T> list;
ListViewCell() {
this.list = new ListView<>();
this.list.setPrefHeight(60);
}
#Override
protected void updateItem(V item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
setGraphic(this.list);
}
}
}
Related
I created a RadioButtonCell with this article but now i want to bind the selectedPropeties of my RadioButton with the properties contained in the ObservableList linked to this TableView. The observableList contains object type of "Risk", and the Model is containing:
final BooleanProperty isDefaultRiskProperty;
My own TableCell implementation is:
package utils;
import Model.databaseModels.Risk;
import controllers.risks.ModifyRisksAvailableController;
import javafx.beans.value.ObservableValue;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.ToggleGroup;
public class RadioButtonCell extends TableCell<Risk, Boolean> {
ToggleGroup toggleGroup;
ModifyRisksAvailableController modifyRisksAvailableController;
public RadioButtonCell(ToggleGroup group){
toggleGroup = group;
}
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
}
}
#Override
protected void updateItem(Boolean item, boolean empty){
super.updateItem(item, empty);
System.out.println(item);
if(!empty && item != null){
RadioButton radioButton = new RadioButton();
radioButton.setToggleGroup(this.toggleGroup);
radioButton.setSelected(item);
setGraphic(radioButton);
}else{
setGraphic(null);
}
}
}
My TableView contains 3 columns:
#FXML
TableColumn<Risk,Boolean> ColumnCheckBox;
#FXML
TableColumn<Risk,Number> ColumnRiskValue;
#FXML
TableColumn<Risk, Boolean> ColumnIsDefaultRisk;
And I initialize the TableView like this:
//Colonne -> Checbkox / sélection pour suppression
ColumnCheckBox.setCellValueFactory(cellData -> cellData.getValue().checkProperty());
ColumnCheckBox.setCellFactory(column -> new CheckBoxTableCell<>());
ColumnCheckBox.setEditable(true);
ColumnCheckBox.setVisible(false);
ColumnCheckBox.setPrefWidth(24.0);
//Colonne -> Checkbox / risque par défaut
ColumnIsDefaultRisk.setCellValueFactory(cellData -> cellData.getValue().isDefaultRiskProperty());
ColumnIsDefaultRisk.setCellFactory(column -> new RadioButtonCell(toggleGroup,this));
ColumnIsDefaultRisk.setEditable(true);
//Colonne -> TextField / % de risque
ColumnRiskValue.setCellValueFactory(cellData -> cellData.getValue().riskValueProperty());
ColumnRiskValue.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter()));
ColumnRiskValue.setEditable(true);
The property i want to bind with the radioButton is ".isDefaultRiskProperty()" of the "ColumnIsDefaultRisk" column. I giving my datas to the column with setCellValueFactory but i can't get the SimpleBooleanProperty in my CellFactory.
The param "item" that i get in the updateItem's method is a Boolean, (it converting BooleanProperty to Boolean), but i want a ObservableValue.
Thanks you very much.
The problem is that table cells aren’t guaranteed to exist all the time; they are for painting and editing only. So, a ToggleGroup isn’t really of any use here.
But you can do the work of a ToggleGroup yourself fairly easily:
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
RadioButton radioButton = new RadioButton();
radioButton.setToggleGroup(this.toggleGroup);
radioButton.setSelected(item);
setGraphic(radioButton);
radioButton.selectedProperty().addListener(
(o, old, selected) -> {
if (selected) {
Risk cellRisk = getTableRow().getItem();
for (Risk risk : getTableView().getItems()) {
risk.setDefaultRisk(risk == cellRisk);
}
}
});
} else {
setGraphic(null);
}
}
If you have a lot of table items (like, thousands), the for-loop could become a performance issue. So, alternatively, you can implement radio-button-like behavior by defining a Risk field or property independent of the RadioButtonCell class which keeps track of the previous selection:
ObjectProperty<Risk> previousDefaultRisk = new SimpleObjectProperty<>();
columnIsDefaultRisk.setCellFactory(
c -> new RadioButtonCell(previousDefaultRisk));
And your RadioButtonCell class would change to look like this:
public class RadioButtonCell extends TableCell<Risk, Boolean> {
private final ObjectProperty<Risk> previousDefaultRisk;
public RadioButtonCell(ObjectProperty<Risk> previousDefaultRisk) {
this.previousDefaultRisk = previousDefaultRisk;
}
#Override
protected void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
RadioButton radioButton = new RadioButton();
radioButton.setSelected(item);
setGraphic(radioButton);
radioButton.selectedProperty().addListener(
(o, old, selected) -> {
if (selected) {
if (previousDefaultRisk.get() != null) {
previousDefaultRisk.get().setDefaultRisk(false);
}
Risk risk = getTableRow().getItem();
risk.setIsDefaultRisk(true);
previousDefaultRisk.set(risk);
}
});
} else {
setGraphic(null);
}
}
}
Note: It is Java convention that non-static field names should always start with a lowercase letter. Following these conventions will make your code easier for others to read, including Stack Overflow readers.
In JavaFX I use ListView to display items added or removed to/from a Set. I have made an observableSet to use with the ListView to show the updates but the ListView is not updating properly when the Set changes. Here is my code with a work around. But why it's not working as expected?
public class FXMLDocumentController implements Initializable {
#FXML
private ListView listView;
ObservableSet<String> observableSet; //ObservableSet to prevent dublicates
Integer i = 3;
#Override
public void initialize(URL url, ResourceBundle rb) {
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
observableSet = FXCollections.observableSet();
observableSet.addAll(Arrays.asList("Item1", "Item2", "Item3"));
//Setting up ListView to the ObservableSet
listView.setItems(FXCollections.observableArrayList(observableSet));
}
#FXML
private void handleButtonAction(ActionEvent event) {
observableSet.add("Item" + i++);
//Setting up ListView to the ObservableSet otherwise it won't update
//My understanding that I don't have to do this with an observableSet
listView.setItems(FXCollections.observableArrayList(observableSet));
}
#FXML
private void handleRemoveAction(ActionEvent event) {
observableSet.removeAll(listView.getSelectionModel().getSelectedItems());
//Setting up ListView to the ObservableSet otherwise it won't update
listView.setItems(FXCollections.observableArrayList(observableSet));
}
}
The Issue:
I have to keep setting up the ListView as demonstrated above and below everytime the observableSet is updated. Otherwise the change won't show in the ListView.
listView.setItems(FXCollections.observableArrayList(observableSet));
Try calling
listview.refresh();
after adding and removing items from the list.
or you can add a change listener on the list and call refresh method from it.
but i prefer first method, because adding a listener sometimes will not update the list view.
This is the expected behavior. The method you are calling to create the observable list "Creates a new observable list and adds the contents of [the set] to it". So subsequent changes to the set will not change the list.
Another option is just to register a listener with the observable set, and update the list view's items by adding or removing an element appropriately:
public class FXMLDocumentController implements Initializable {
#FXML
private ListView listView;
private ObservableSet<String> observableSet; //ObservableSet to prevent dublicates
private int i = 3;
#Override
public void initialize(URL url, ResourceBundle rb) {
listView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
observableSet = FXCollections.observableSet();
observableSet.addListener((SetChangeListener.Change<? extends String> c) -> {
if (c.wasAdded()) {
listView.getItems().add(c.getElementAdded());
}
if (c.wasRemoved()) {
listView.getItems().remove(c.getElementRemoved());
}
});
observableSet.addAll(Arrays.asList("Item1", "Item2", "Item3"));
}
#FXML
private void handleButtonAction(ActionEvent event) {
observableSet.add("Item" + i++);
}
#FXML
private void handleRemoveAction(ActionEvent event) {
observableSet.removeAll(listView.getSelectionModel().getSelectedItems());
}
}
I have a TableView and a custom MyTableCell extends CheckBoxTreeTableCell<MyRow, Boolean>, In this cell is #Overridden the updateItem method:
#Override
public void updateItem(Boolean item, boolean empty) {
super.updateItem(item, empty);
if(!empty){
MyRow currentRow = geTableRow().getItem();
Boolean available = currentRow.isAvailable();
if (!available) {
setGraphic(null);
}else{
setGraphic(super.getGraphic())
}
} else {
setText(null);
setGraphic(null);
}
}
I have a ComboBox<String> where I have some items, and when I change the value of that combobox I want to set the visibility of the checkboxes depending on selected value. So I have a listener:
comboBox.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
if (newValue.equals("A") || newValue.equals("S")) {
data.stream().filter(row -> row.getName().startsWith(newValue)).forEach(row -> row.setAvailable(false));
}
});
The data is an ObservableList<MyRow>
This is just an example and a simplified version of my code
When I change the value in comboBox the table's the chekboxes don't disappear until I scroll or click on that cell. There is a "sollution" to call table.refresh(); but I don't want to refresh the whole table, when I want to refresh just one cell. So I tried to adding some listeners to trigger the updateItem, but I failed at every attempt. Do you have any idea how can I trigger the update mechanism for one cell, not for the whole table?
Bind the cell's graphic, instead of just setting it:
private Binding<Node> graphicBinding ;
#Override
protected void updateItem(Boolean item, boolean empty) {
graphicProperty().unbind();
super.updateItem(item, empty) ;
MyRow currentRow = getTableRow().getItem();
if (empty) {
graphicBinding = null ;
setGraphic(null);
} else {
graphicBinding = Bindings
.when(currentRow.availableProperty())
.then(super.getGraphic())
.otherwise((Node)null);
graphicProperty.bind(graphicBinding);
}
}
I really apologize if this question doesn't make much sense in your perspective but in my perspective, this is really important to write cleaner and more maintainable code.
I have A.fxml, AController.java (controller class for A.fxml). I have a TableView with custom cell factories defined. I prefer defining my cell factories in a separate class so that I can reuse them if needed. I prefer writing all my event handling code in my controller class. But if I use custom cell factories then I am forced to write the event handling in the cell factory class itself.
Is there a way i can handle the custom cell factory events in my controller class itself? or atleast just throw the event from the custom cell factory class to my controller class and handle?
Thanks in Advance.
You can pass a object to the factory that determines when an context menu should be opened and that prepares the menu.
Example:
public interface CellContextMenuProvider<S, T> {
/**
* Prepares the context menu for opening.
* #param cell the cell the menu was requested for
* #param menu the menu to prepare
*/
public void prepareContextMenu(TableCell<S, T> cell, ContextMenu menu);
/**
* Checks, if a cell continaing a certain item should have an active context
* menu.
* #param empty if the cell is empty
* #param item the item of the cell
* #return {#literal true} iff the context menu should be enabled.
*/
public boolean enableContextMenu(boolean empty, T item);
/**
* Prepares the intial menu. This menu must not be empty, otherwise it won't
* be shown when it's requested for the first time.
* #param menu the menu to prepare
*/
public void prepareInitialContextMenu(ContextMenu menu);
}
public class CellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private final CellContextMenuProvider<S, T> menuProvider;
private final ContextMenu contextMenu;
public CellFactory(#NamedArg("menuProvider") CellContextMenuProvider<S, T> menuProvider) {
this.menuProvider = menuProvider;
if (menuProvider == null) {
this.contextMenu = null;
} else {
this.contextMenu = new ContextMenu();
menuProvider.prepareInitialContextMenu(contextMenu);
}
this.menuEventHandler = evt -> {
if (this.contextMenu != null) {
TableCell<S, T> source = (TableCell<S, T>) evt.getSource();
this.menuProvider.prepareContextMenu(source, this.contextMenu);
}
};
}
public CellFactory() {
this(null);
}
private final EventHandler<ContextMenuEvent> menuEventHandler;
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
TableCell<S, T> result = new TableCell<S, T>() {
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
setText(Objects.toString(item, ""));
setContextMenu(menuProvider != null && menuProvider.enableContextMenu(empty, item) ? contextMenu : null);
}
};
result.setOnContextMenuRequested(menuEventHandler);
if (menuProvider != null && menuProvider.enableContextMenu(true, null)) {
result.setContextMenu(contextMenu);
}
return result;
}
}
public class AController {
#FXML
private TableView<Item<Integer>> table;
public void initialize() {
for (int i = 0; i < 100; i++) {
table.getItems().add(new Item<>(i));
}
}
public CellContextMenuProvider<Item<Integer>, Integer> getMenuProvider() {
return new CellContextMenuProvider<Item<Integer>, Integer>() {
private final MenuItem item = new MenuItem("Say Hello World");
{
item.setOnAction(evt -> System.out.println("Hello World"));
}
#Override
public void prepareContextMenu(TableCell<Item<Integer>, Integer> cell, ContextMenu menu) {
}
#Override
public void prepareInitialContextMenu(ContextMenu menu) {
menu.getItems().setAll(item);
}
#Override
public boolean enableContextMenu(boolean empty, Integer item) {
// only for odd items
return !empty && (item % 2) != 0;
}
};
}
}
A.fxml
<TableView fx:id="table" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.60" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fxml.table.AController">
<columns>
<TableColumn prefWidth="159.0" text="C1">
<cellValueFactory>
<PropertyValueFactory property="value" />
</cellValueFactory>
<cellFactory>
<CellFactory menuProvider="$controller.menuProvider"/>
</cellFactory>
</TableColumn>
</columns>
</TableView>
Note: if the context menu is always the same you can also add EventHandler properties to the factory and use them like e.g. onAction attributes for buttons, which would allow you to pass event handlers of the controller, wich would result in shorter/simpler code.
Wouldnt it be possible to give your cell factory a FunctionalInterface as parameter with your event handling?(not sure if a good idea tho)
I imagine your code as follow:
Controller:
myTableView.setCellFactory(new MyOwnCellFactory<>(() -> {
// event handling
}));
MyOwnCellFactory:
public MyOwnCellFactory(MyFunctionalInterface myInterface) {
functionalInterface = myInterface;
}
// something something
functionalInterface.handleEvent();
FunctionalInterface:
#FunctionalInterface
public interface MyFunctionalInterface {
public void handleEvent();
}
Not sure if I understand your idea correct. Didnt test the code, just wrote it out of my head.
I'm trying to add an image to a tableView and went through several questions and answers and it still doesn't work.
The other fields in the tableView like name are loaded correctly.
Intelij tells me that updateItem is never used, which is probably why it doesn't work, but I have no idea how to debug this...
Here's what I got so far
#FXML private TableColumn<PlayerManager, Image> tableColumnType;
#FXML private void initialize(){
tableColumnType.setCellFactory(param -> {
//Set up the ImageView
final ImageView imageview = new ImageView();
imageview.setFitHeight(10);
imageview.setFitWidth(10);
///imageview.setImage(imageComputer); //uncommenting this places the image on all cells, even empty ones
//Set up the Table
TableCell<PlayerManager, Image> cell = new TableCell<PlayerManager, Image>() {
public void updateItem(PlayerManager item, boolean empty) {
if (item != null) { // choice of image is based on values from item, but it doesn't matter now
imageview.setImage(imageComputer);
}
}
};
// Attach the imageview to the cell
cell.setGraphic(imageview);
return cell;
});
}
The questions I went through are:
How to add an Image into a JavaFx TableView column
Display image in table
Inserting images into TableView rows - JavaFX
The signature of the updateItem method is wrong: it should be
public void updateItem(Image item, boolean empty) { /* ... */ }
If the compiler rejects the #Override annotation, then you know you are not defining the correct method. So you should use #Override and if you get a compile error, it is a signal that something is not right.
So you should be able to do
#FXML private TableColumn<PlayerManager, Image> tableColumnType;
#FXML private void initialize(){
tableColumnType.setCellFactory(param -> {
//Set up the ImageView
final ImageView imageview = new ImageView();
imageview.setFitHeight(10);
imageview.setFitWidth(10);
///imageview.setImage(imageComputer); //uncommenting this places the image on all cells, even empty ones
//Set up the Table
TableCell<PlayerManager, Image> cell = new TableCell<PlayerManager, Image>() {
#Override
public void updateItem(Image item, boolean empty) {
if (item != null) { // choice of image is based on values from item, but it doesn't matter now
imageview.setImage(imageComputer);
}
}
};
// Attach the imageview to the cell
cell.setGraphic(imageview);
return cell;
});
}
If your table cell needs to access the actual PlayerManager object, then you need to make the table column a TableColumn<PlayerManager, PlayerManager> and update the cellValueFactory (which you haven't shown) accordingly.
Finally, note that your updateItem(...) method needs to deal with all cases, including empty cells for which the item is null.
So you may need something like
#FXML private TableColumn<PlayerManager, PlayerManager> tableColumnType;
#FXML private void initialize(){
tableColumnType.setCellValueFactory(cellData -> new SimpleObjectProperty<PlayerManager>(cellData.getValue());
tableColumnType.setCellFactory(param -> {
//Set up the ImageView
final ImageView imageview = new ImageView();
imageview.setFitHeight(10);
imageview.setFitWidth(10);
///imageview.setImage(imageComputer); //uncommenting this places the image on all cells, even empty ones
//Set up the Table
TableCell<PlayerManager, PlayerManager> cell = new TableCell<PlayerManager, PlayerManager>() {
#Override
public void updateItem(PlayerManager item, boolean empty) {
if (item != null) { // choice of image is based on values from item, but it doesn't matter now
imageview.setImage(imageComputer);
} else {
imageView.setImage(null);
}
}
};
// Attach the imageview to the cell
cell.setGraphic(imageview);
return cell;
});
}
The signature of the updateItem() method is wrong.
Try to use:
#Override
protected void updateItem(Image item, boolean empty){
//your code
}
Edit:
I think you can solve your problem by also setting a CellValueFactory for your TableColumn:
tableColumnType.setCellValueFactory(
new Callback<CellDataFeatures<PlayerManager, Image>, ObservableValue<Image>(){
#Override
public ObservableValue<Image> call(
CellDataFeatures<PlayerManager, Image> param) {
return param.getValue().exampleMethod; /* Method of your PlayerManager which returns an Image as ObservableValue. To do so you could wrap it in an `ObjectProperty<Image>`*/
}
}
);