I am trying to add a button into a GUI app in JFX for dealing with a database of customers and appointments. I want to put two buttons in each row of the table that will open other views when clocked.
Below is what I currently have (which does not work). The compiler doesn't complain about this code but I get the following ClassCastException
at runtime (which did not seem to be an issue in the place I saw ActionButtonTableCell
originally used for a case analogous to this one; How to add button in JavaFX table view).
Here is the stack trace:
Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: class javafx.scene.control.TableColumn$CellDataFeatures cannot be cast to class javafx.scene.control.TableColumn (javafx.scene.control.TableColumn$CellDataFeatures and javafx.scene.control.TableColumn are in module javafx.controls of loader 'app')
at javafx.controls/javafx.scene.control.TableColumn.getCellObservableValue(TableColumn.java:593)
at javafx.controls/javafx.scene.control.TableColumn.getCellObservableValue(TableColumn.java:578)
at javafx.controls/javafx.scene.control.TableCell.updateItem(TableCell.java:646)
at javafx.controls/javafx.scene.control.TableCell.indexChanged(TableCell.java:469)
at javafx.controls/javafx.scene.control.IndexedCell.updateIndex(IndexedCell.java:120)
at javafx.controls/javafx.scene.control.skin.TableSkinUtils.resizeColumnToFitContent(TableSkinUtils.java:119)
at javafx.controls/javafx.scene.control.skin.TableSkinUtils.resizeColumnToFitContent(TableSkinUtils.java:86)
at javafx.controls/javafx.scene.control.skin.TableColumnHeader.doColumnAutoSize(TableColumnHeader.java:573)
at javafx.controls/javafx.scene.control.skin.TableColumnHeader.updateScene(TableColumnHeader.java:516)
at javafx.controls/javafx.scene.control.skin.TableColumnHeader.lambda$new$0(TableColumnHeader.java:159)
at javafx.controls/com.sun.javafx.scene.control.LambdaMultiplePropertyChangeListenerHandler.lambda$new$1(LambdaMultiplePropertyChangeListenerHandler.java:49)
at javafx.base/javafx.beans.value.WeakChangeListener.changed(WeakChangeListener.java:86)
at javafx.base/com.sun.javafx.binding.ExpressionHelper$SingleChange.fireValueChangedEvent(ExpressionHelper.java:181)
at javafx.base/com.sun.javafx.binding.ExpressionHelper.fireValueChangedEvent(ExpressionHelper.java:80)
at javafx.base/javafx.beans.property.ReadOnlyObjectPropertyBase.fireValueChangedEvent(ReadOnlyObjectPropertyBase.java:74)
at javafx.base/javafx.beans.property.ReadOnlyObjectWrapper.fireValueChangedEvent(ReadOnlyObjectWrapper.java:102)
at javafx.graphics/javafx.scene.Node$ReadOnlyObjectWrapperManualFire.fireSuperValueChangedEvent(Node.java:1054)
at javafx.graphics/javafx.scene.Node.invalidatedScenes(Node.java:1114)
at javafx.graphics/javafx.scene.Node.setScenes(Node.java:1152)
at javafx.graphics/javafx.scene.Parent$2.onChanged(Parent.java:369)
at javafx.base/com.sun.javafx.collections.TrackableObservableList.lambda$new$0(TrackableObservableList.java:45)
at javafx.base/com.sun.javafx.collections.ListListenerHelper$Generic.fireValueChangedEvent(ListListenerHelper.java:329)
at javafx.base/com.sun.javafx.collections.ListListenerHelper.fireValueChangedEvent(ListListenerHelper.java:73)
at javafx.base/javafx.collections.ObservableListBase.fireChange(ObservableListBase.java:233)
at javafx.base/javafx.collections.ListChangeBuilder.commit(ListChangeBuilder.java:482)
at javafx.base/javafx.collections.ListChangeBuilder.endChange(ListChangeBuilder.java:541)
at javafx.base/javafx.collections.ObservableListBase.endChange(ObservableListBase.java:205)
at javafx.base/javafx.collections.ModifiableObservableListBase.setAll(ModifiableObservableListBase.java:90)
at javafx.base/com.sun.javafx.collections.VetoableListDecorator.setAll(VetoableListDecorator.java:116)
at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.updateContent(NestedTableColumnHeader.java:578)
at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.updateTableColumnHeaders(NestedTableColumnHeader.java:504)
at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.checkState(NestedTableColumnHeader.java:638)
at javafx.controls/javafx.scene.control.skin.NestedTableColumnHeader.computePrefHeight(NestedTableColumnHeader.java:345)
at javafx.graphics/javafx.scene.Parent.prefHeight(Parent.java:1031)
at javafx.graphics/javafx.scene.layout.Region.prefHeight(Region.java:1559)
at javafx.controls/javafx.scene.control.skin.TableHeaderRow.computePrefHeight(TableHeaderRow.java:376)
at javafx.controls/javafx.scene.control.skin.TableHeaderRow.computeMinHeight(TableHeaderRow.java:369)
at javafx.graphics/javafx.scene.Parent.minHeight(Parent.java:1059)
at javafx.graphics/javafx.scene.layout.Region.minHeight(Region.java:1525)
at javafx.controls/javafx.scene.control.SkinBase.computeMinHeight(SkinBase.java:311)
at javafx.controls/javafx.scene.control.Control.computeMinHeight(Control.java:512)
at javafx.graphics/javafx.scene.Parent.minHeight(Parent.java:1059)
at javafx.graphics/javafx.scene.layout.Region.minHeight(Region.java:1525)
at javafx.graphics/javafx.scene.layout.Region.computeChildPrefAreaHeight(Region.java:1980)
at javafx.graphics/javafx.scene.layout.GridPane.computePrefHeights(GridPane.java:1436)
at javafx.graphics/javafx.scene.layout.GridPane.computePrefHeight(GridPane.java:1265)
at javafx.graphics/javafx.scene.Parent.prefHeight(Parent.java:1031)
at javafx.graphics/javafx.scene.layout.Region.prefHeight(Region.java:1559)
at javafx.graphics/javafx.scene.Scene.getPreferredHeight(Scene.java:1811)
at javafx.graphics/javafx.scene.Scene.resizeRootToPreferredSize(Scene.java:1776)
at javafx.graphics/javafx.scene.Scene.preferredSize(Scene.java:1747)
at javafx.graphics/javafx.scene.Scene$2.preferredSize(Scene.java:393)
at javafx.graphics/com.sun.javafx.scene.SceneHelper.preferredSize(SceneHelper.java:66)
at javafx.graphics/javafx.stage.Window$SceneModel.invalidated(Window.java:814)
at javafx.base/javafx.beans.property.ObjectPropertyBase.markInvalid(ObjectPropertyBase.java:112)
at javafx.base/javafx.beans.property.ObjectPropertyBase.set(ObjectPropertyBase.java:147)
at javafx.graphics/javafx.stage.Window.setScene(Window.java:770)
at javafx.graphics/javafx.stage.Stage.setScene(Stage.java:266)
at app.jfx.CustomerList.<init>(CustomerList.java:79)
at app.jfx.CustomerList.<init>(CustomerList.java:19)
at app.jfx.LoggedInView$1.handle(LoggedInView.java:44)
at app.jfx.LoggedInView$1.handle(LoggedInView.java:41)
at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:86)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:49)
at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
at javafx.graphics/javafx.scene.Node.fireEvent(Node.java:8879)
at javafx.controls/javafx.scene.control.Button.fire(Button.java:200)
at javafx.controls/com.sun.javafx.scene.control.behavior.ButtonBehavior.mouseReleased(ButtonBehavior.java:206)
at javafx.controls/com.sun.javafx.scene.control.inputmap.InputMap.handle(InputMap.java:274)
at javafx.base/com.sun.javafx.event.CompositeEventHandler$NormalEventHandlerRecord.handleBubblingEvent(CompositeEventHandler.java:218)
at javafx.base/com.sun.javafx.event.CompositeEventHandler.dispatchBubblingEvent(CompositeEventHandler.java:80)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:238)
at javafx.base/com.sun.javafx.event.EventHandlerManager.dispatchBubblingEvent(EventHandlerManager.java:191)
at javafx.base/com.sun.javafx.event.CompositeEventDispatcher.dispatchBubblingEvent(CompositeEventDispatcher.java:59)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:58)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.BasicEventDispatcher.dispatchEvent(BasicEventDispatcher.java:56)
at javafx.base/com.sun.javafx.event.EventDispatchChainImpl.dispatchEvent(EventDispatchChainImpl.java:114)
at javafx.base/com.sun.javafx.event.EventUtil.fireEventImpl(EventUtil.java:74)
at javafx.base/com.sun.javafx.event.EventUtil.fireEvent(EventUtil.java:54)
at javafx.base/javafx.event.Event.fireEvent(Event.java:198)
at javafx.graphics/javafx.scene.Scene$MouseHandler.process(Scene.java:3851)
at javafx.graphics/javafx.scene.Scene$MouseHandler.access$1200(Scene.java:3579)
at javafx.graphics/javafx.scene.Scene.processMouseEvent(Scene.java:1849)
at javafx.graphics/javafx.scene.Scene$ScenePeerListener.mouseEvent(Scene.java:2588)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:397)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler$MouseEventNotification.run(GlassViewEventHandler.java:295)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.lambda$handleMouseEvent$2(GlassViewEventHandler.java:434)
at javafx.graphics/com.sun.javafx.tk.quantum.QuantumToolkit.runWithoutRenderLock(QuantumToolkit.java:390)
at javafx.graphics/com.sun.javafx.tk.quantum.GlassViewEventHandler.handleMouseEvent(GlassViewEventHandler.java:433)
at javafx.graphics/com.sun.glass.ui.View.handleMouseEvent(View.java:556)
at javafx.graphics/com.sun.glass.ui.View.notifyMouse(View.java:942)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:174)
at java.base/java.lang.Thread.run(Thread.java:833)
I just want the buttons to display as part of the table, but the exception results in the entire table (apart from the column headers) failing to display any data.
Customer
class looks like this (Model
is an application-specific class that just reduces code duplication for several properties that have the same name across other classes):
public class Customer extends Model {
//primary key
private int customerId;
//customer data
private String customerName;
private String address;
private String postalCode;
private String phone;
//foreign key to FirstLevelDivision
private int divisionId;
//all-arg constructor
//getters and setters
}
The button is meant to be added with ActionButtonTableCell
, which looks like this:
package app.jfx;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonBase;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
import java.util.function.Function;
public class ActionButtonTableCell<S> extends TableCell<S, Button> {
private final Button actionButton;
public ActionButtonTableCell(String label, Function< S, S> function) {
this.getStyleClass().add("action-button-table-cell");
this.actionButton = new Button(label);
this.actionButton.setOnAction((ActionEvent e) -> {
function.apply(getCurrentItem()); });
this.actionButton.setMaxWidth(Double.MAX_VALUE);
}
public S getCurrentItem() {
return (S) getTableView().getItems().get(getIndex());
}
public static <S> Callback<TableColumn<S, Button>, TableCell<S, Button>> forTableColumn(String label, Function< S, S> function) {
return param -> new ActionButtonTableCell<>(label, function);
}
@Override
public void updateItem(Button item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(actionButton);
}
}
}
It appears in CustomerList
, which looks like this:
package app.jfx;
import app.JDBC;
import app.model.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;import javafx.stage.Stage;
import javafx.util.Callback;
import java.util.List;
import java.util.function.Function;
public class CustomerList extends Scene {
private User user;
public CustomerList(Stage stage) {
this(stage, null, new GridPane());
} public CustomerList(Stage stage, User user) {
this(stage, user, new GridPane());
}
private CustomerList(Stage stage, User user, GridPane gridPane) {
super(gridPane);
Label label = new Label("Customer Records");
TableView tableView = new TableView();
Button back = new Button("Back");
back.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent actionEvent) {
new LoggedInView(stage, user);
}
});
gridPane.add(label, 0, 0);
gridPane.add(tableView, 0, 1);
gridPane.add(back, 0, 2);
List<Customer> customers = JDBC.getCustomers();
customers.forEach( customer -> tableView.getItems().add(customer));
//define columns
TableColumn<Customer, Integer> customerId = new TableColumn<>("Customer ID");
TableColumn<Customer, String> customerName = new TableColumn<>("Customer Name");
TableColumn<Customer, String> address = new TableColumn<>("Address");
TableColumn<Customer, String> postalCode = new TableColumn<>("Postal Code");
TableColumn<Customer, String> phone = new TableColumn<>("Phone");
TableColumn<Customer, Integer> divisionId = new TableColumn<>("Division ID");
TableColumn viewCol = new TableColumn("");
TableColumn deleteCol = new TableColumn("");
//set cell values
customerId.setCellValueFactory(new PropertyValueFactory<>("customerId"));
customerName.setCellValueFactory(new PropertyValueFactory<>("customerName"));
address.setCellValueFactory(new PropertyValueFactory<>("address"));
postalCode.setCellValueFactory(new PropertyValueFactory<>("postalCode"));
phone.setCellValueFactory(new PropertyValueFactory<>("phone"));
divisionId.setCellValueFactory(new PropertyValueFactory<>("divisionId"));
viewCol.setCellValueFactory(ActionButtonTableCell.<Customer>forTableColumn("View/Edit", (Customer c) -> {
new CustomerEditView(stage, user, c.getCustomerId());
return c;
}));
deleteCol.setCellValueFactory(ActionButtonTableCell.<Customer>forTableColumn("Delete", (Customer c) -> {
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, "Are you sure you want to delete this customer?");
confirm.show();
JDBC.deleteCustomer(c.getCustomerId());
new CustomerList(stage, user);
return c; }));
//render table
tableView.getColumns().add(customerId);
tableView.getColumns().add(customerName);
tableView.getColumns().add(address);
tableView.getColumns().add(postalCode);
tableView.getColumns().add(phone);
tableView.getColumns().add(divisionId);
tableView.getColumns().add(viewCol);
tableView.getColumns().add(deleteCol);
stage.setScene(this); }}
Adding in deleteCol
is where the breaking started, yielding the exception above. If I specify viewCol
and deleteCol
as TableColumn<Customer, Button>
, the compiler warns that setCellValueFactory
requires a Callback<CellDataFeatures<Customer, Button>, ObservableValue<Button>>
and I've provided a Callback<TableColumn<Customer, Button>, TableCell<Customer, Button>
instead (but not if the types for TableColumn
are not specified). Altering the return type of the ActionButtonTableCell
method used breaks the @Override
. What can I do to make this cast work (or make it unnecessary)? Is there another way to add the buttons in?