Issue
I have a class Task that takes field values from the database and is displayed as a row in the table.
package com.example.javafxapp.models;
import javafx.scene.control.Button;
import javafx.scene.layout.HBox;
public class Task {
private String id;
private String task;
private Button done;
private Button cancel;
private Button remove;
private HBox edit;
public Task(String id, String task, HBox edit, Button done, Button cancel, Button remove) {
this.id = id;
this.task = task;
this.edit = edit;
this.done = done;
this.cancel = cancel;
this.remove = remove;
edit.getChildren().addAll(done, cancel, remove);
edit.setSpacing(5);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
public HBox getEdit() {
return edit;
}
public void setEdit(HBox edit) {
this.edit = edit;
}
public Button getDone() {
return done;
}
public void setDone(Button done) {
this.done = done;
}
public Button getCancel() {
return cancel;
}
public void setCancel(Button cancel) {
this.cancel = cancel;
}
public Button getRemove() {
return remove;
}
public void setRemove(Button remove) {
this.remove = remove;
}
}
There is a controller that fills the rows of the table with objects of the Task class.
package com.example.javafxapp.controllers;
import com.example.javafxapp.models.DatabaseHandler;
import com.example.javafxapp.models.Task;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Optional;
import java.util.ResourceBundle;
public class AppController {
@FXML
private ResourceBundle resources;
@FXML
private URL location;
@FXML
private AnchorPane appPane;
@FXML
private TableView<Task> currentTable;
@FXML
private Tab currentTab;
@FXML
private AnchorPane currentTabAnchorPane;
@FXML
private TableColumn<Task, String> currentIdColumn;
@FXML
private TableColumn<Task, String> currentTasksColumn;
@FXML
private TableColumn<Task, HBox> editColumn;
ObservableList<Task> currentTasks = FXCollections.observableArrayList();
@FXML
void initialize() {
getAllTasks();
initCols();
//Š’utton mouse click listener from Tableview cells.
}
private void initCols(){
currentIdColumn.setCellValueFactory(new PropertyValueFactory<Task, String>("id"));
currentTasksColumn.setCellValueFactory(new PropertyValueFactory<Task, String>("task"));
editColumn.setCellValueFactory(new PropertyValueFactory<Task, HBox>("edit"));
currentTable.getItems().clear();
currentTable.setItems(currentTasks);
}
private void getAllTasks() {
ResultSet rs = new DatabaseHandler().getTaskTableFromDb();
try {
while (rs.next()) {
if(rs.getString("state").equals("current")) {
currentTasks.add(new Task(rs.getString("idtask"),
rs.getString("task"),
rs.getString("state"),
new HBox(),
new Button("done"),
new Button("cancel"),
new Button("remove")));
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
I need to listen for clicks on buttons in cells. But those buttons are not present as fields in the controller. How can I do this without referring to a specific cell?
Solution
Your model class Task
should not have any Ui components in it. It should look something like
public class Task {
private String id;
private String task;
public Task(String id, String task) {
this.id = id;
this.task = task;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getTask() {
return task;
}
public void setTask(String task) {
this.task = task;
}
}
You should strongly consider implementing the model class with JavaFX Properties instead of implementing it as a plain Java bean.
To display UI controls in cells in the table, you should implement a custom TableCell
and generate instances of it in a cell factory. Probably the most convenient approach is to let the column containing those cells have the entire model as its value. That way the cell's getItem()
method will return the model for its specific row.
@FXML
private TableColumn<Task, Task> editColumn ;
@FXML
private void initialize() {
// ...
editColumn.setCellValueFactory(cellData -> new SimpleObjectProperty<>(cellData.getValue()));
editColumn.setCellFactory(col -> new TaskActionCell());
// ...
}
And the cell implementation (here this is an inner class in the controller) looks like
public class TaskActionCell extends TableCell<Task, Task> {
private final HBox graphic;
public TaskActionCell() {
Button done = new Button("Done");
Button cancel = new Button("Cancel");
Button remove = new Button("Remove");
graphic = new HBox(5, done, cancel remove);
remove.setOnAction(event -> {
Task task = getItem();
currentTasks.remove(task);
});
// other event handlers are similar
}
@Override
protected void updateItem(Task task, boolean empty) {
super.updateItem(task, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
}
Another approach would be to just let the cell values be null (e.g. using TableColumn<Task, Void> editColumn
, etc.), and to access the row value in the cell with getTableView().getItems().get(getIndex())
, but that seems less elegant.
Here is a complete working example, using the Task
class I defined above.
Table.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<BorderPane xmlns:fx="http://javafx.com/fxml"
fx:controller="org.jamesd.examples.actiontable.TableController">
<center>
<TableView fx:id="table">
<columns>
<TableColumn text="Id" fx:id="idColumn"/>
<TableColumn text="Task" fx:id="taskColumn"/>
<TableColumn text="Edit" fx:id="editColumn"/>
</columns>
</TableView>
</center>
</BorderPane>
TableController.java
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import java.util.ArrayList;
import java.util.List;
public class TableController {
@FXML
private TableView<Task> table ;
@FXML
private TableColumn<Task, Task> editColumn ;
@FXML
private TableColumn<Task, String> idColumn ;
@FXML
private TableColumn<Task, String> taskColumn ;
private ObservableList<Task> currentTasks ;
@FXML
private void initialize() {
idColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getId()));
taskColumn.setCellValueFactory(cellData -> new SimpleStringProperty(cellData.getValue().getTask()));
editColumn.setCellValueFactory(cellData -> new SimpleObjectProperty<>(cellData.getValue()));
editColumn.setCellFactory(col -> new TaskActionCell());
currentTasks = loadTasks();
table.setItems(currentTasks);
}
private ObservableList<Task> loadTasks() {
List<Task> tasks = new ArrayList<>();
for (int i = 1 ; i <= 20; i++) {
tasks.add(new Task(Integer.toString(i), "Task "+i));
}
return FXCollections.observableList(tasks);
}
public class TaskActionCell extends TableCell<Task, Task> {
private final HBox graphic;
public TaskActionCell() {
Button done = new Button("Done");
Button cancel = new Button("Cancel");
Button remove = new Button("Remove");
graphic = new HBox(5, done, cancel, remove);
remove.setOnAction(event -> {
Task task = getItem();
currentTasks.remove(task);
});
// other event handlers are similar
}
@Override
protected void updateItem(Task task, boolean empty) {
super.updateItem(task, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
}
}
Application class:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class App extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource("Table.fxml"));
Scene scene = new Scene(fxmlLoader.load());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Application running:
After pressing lots of "Remove" buttons:
Answered By - James_D
Answer Checked By - Dawn Plyler (JavaFixing Volunteer)