Skip to content
Advertisement

How to listen for clicks on dynamically generated buttons in TableCell?

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?

Advertisement

Answer

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:

enter image description here

After pressing lots of “Remove” buttons:

enter image description here

Advertisement