在JFX TableView中如何添加一个按钮?

huangapple go评论66阅读模式
英文:

How do I add a button into a JFX TableView?

问题

To resolve the issue and make the buttons work in your JavaFX table, you should make some modifications to your code. The problem you're encountering is due to the casting of the TableColumn in your ActionButtonTableCell, which results in the ClassCastException.

Here are the steps to fix it:

  1. Update the ActionButtonTableCell class to accept the type of the data item, not the button:

    public class ActionButtonTableCell<S> extends TableCell<S, Void> {
        private final Button actionButton;
    
        public ActionButtonTableCell(String label, Function<S, Void> 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 getItem();
        }
    
        public static <S> Callback<TableColumn<S, Void>, TableCell<S, Void>> forTableColumn(String label, Function<S, Void> function) {
            return param -> new ActionButtonTableCell<>(label, function);
        }
    
        @Override
        public void updateItem(Void item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setGraphic(null);
            } else {
                setGraphic(actionButton);
            }
        }
    }
    
  2. Modify the viewCol and deleteCol TableColumn definitions in your CustomerList class to match the updated ActionButtonTableCell:

    TableColumn<Customer, Void> viewCol = new TableColumn<>("View/Edit");
    TableColumn<Customer, Void> deleteCol = new TableColumn<>("Delete");
    
  3. Update the setCellValueFactory for viewCol and deleteCol to return null since you don't need to bind these columns to a specific property:

    viewCol.setCellValueFactory(param -> new SimpleObjectProperty<>(null));
    deleteCol.setCellValueFactory(param -> new SimpleObjectProperty<>(null));
    

These changes should resolve the ClassCastException issue and allow the buttons to be displayed in your table. Remember to update your ActionButtonTableCell usage in other parts of your code to match the modified class definition.

英文:

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; https://stackoverflow.com/questions/29489366/how-to-add-button-in-javafx-table-view).

Here is the stack trace:

Exception in thread &quot;JavaFX Application Thread&quot; 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 &#39;app&#39;)
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.&lt;init&gt;(CustomerList.java:79)
at app.jfx.CustomerList.&lt;init&gt;(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&lt;S&gt; extends TableCell&lt;S, Button&gt; {
private final Button actionButton;
public ActionButtonTableCell(String label, Function&lt; S, S&gt; function) {
this.getStyleClass().add(&quot;action-button-table-cell&quot;);
this.actionButton = new Button(label);
this.actionButton.setOnAction((ActionEvent e) -&gt; {
function.apply(getCurrentItem());        });
this.actionButton.setMaxWidth(Double.MAX_VALUE);
}
public S getCurrentItem() {
return (S) getTableView().getItems().get(getIndex());
}
public static &lt;S&gt; Callback&lt;TableColumn&lt;S, Button&gt;, TableCell&lt;S, Button&gt;&gt; forTableColumn(String label, Function&lt; S, S&gt; function) {
return param -&gt; new ActionButtonTableCell&lt;&gt;(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(&quot;Customer Records&quot;);
TableView tableView = new TableView();
Button back = new Button(&quot;Back&quot;);
back.setOnAction(new EventHandler&lt;ActionEvent&gt;() {
@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&lt;Customer&gt; customers = JDBC.getCustomers();
customers.forEach( customer -&gt; tableView.getItems().add(customer));
//define columns
TableColumn&lt;Customer, Integer&gt; customerId = new TableColumn&lt;&gt;(&quot;Customer ID&quot;);
TableColumn&lt;Customer, String&gt; customerName = new TableColumn&lt;&gt;(&quot;Customer Name&quot;);
TableColumn&lt;Customer, String&gt; address = new TableColumn&lt;&gt;(&quot;Address&quot;);
TableColumn&lt;Customer, String&gt; postalCode = new TableColumn&lt;&gt;(&quot;Postal Code&quot;);
TableColumn&lt;Customer, String&gt; phone = new TableColumn&lt;&gt;(&quot;Phone&quot;);
TableColumn&lt;Customer, Integer&gt; divisionId = new TableColumn&lt;&gt;(&quot;Division ID&quot;);
TableColumn viewCol = new TableColumn(&quot;&quot;);
TableColumn deleteCol = new TableColumn(&quot;&quot;);
//set cell values
customerId.setCellValueFactory(new PropertyValueFactory&lt;&gt;(&quot;customerId&quot;));
customerName.setCellValueFactory(new PropertyValueFactory&lt;&gt;(&quot;customerName&quot;));
address.setCellValueFactory(new PropertyValueFactory&lt;&gt;(&quot;address&quot;));
postalCode.setCellValueFactory(new PropertyValueFactory&lt;&gt;(&quot;postalCode&quot;));
phone.setCellValueFactory(new PropertyValueFactory&lt;&gt;(&quot;phone&quot;));
divisionId.setCellValueFactory(new PropertyValueFactory&lt;&gt;(&quot;divisionId&quot;));
viewCol.setCellValueFactory(ActionButtonTableCell.&lt;Customer&gt;forTableColumn(&quot;View/Edit&quot;, (Customer c) -&gt; {
new CustomerEditView(stage, user, c.getCustomerId());
return c;
}));
deleteCol.setCellValueFactory(ActionButtonTableCell.&lt;Customer&gt;forTableColumn(&quot;Delete&quot;, (Customer c) -&gt; {
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, &quot;Are you sure you want to delete this customer?&quot;);
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&lt;Customer, Button&gt;, the compiler warns that setCellValueFactory requires a Callback&lt;CellDataFeatures&lt;Customer, Button&gt;, ObservableValue&lt;Button&gt;&gt; and I've provided a Callback&lt;TableColumn&lt;Customer, Button&gt;, TableCell&lt;Customer, Button&gt; 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?

答案1

得分: 4

I have translated the code-related content you provided into Chinese:

以下是要翻译的代码内容:

您的代码存在一些问题主要问题在于您似乎混淆了`cellFactory`(用于生成要显示在表列中的单元格`cellValueFactory`(用于提供单元格显示的值两者之间的区别在这种情况下单元格实际上不需要一个值它仅依赖于整行的值),所以不需要`cellValueFactory`(尽管您可以使用一个来返回整行的值)。

您也不应该在没有类型参数的情况下使用原始类型`TableView``TableColumn`)。

由于您的操作实现从不使用它们返回的值似乎更合理的做法是使用`Consumer&lt;S&gt;`来表示它们而不是`Function&lt;S, S&gt;`,后者似乎是随意返回作为参数传递的值

最后,`TableView`、`TableColumn`、`TableCell`等的类型应该是*数据*的类型而不是UI控件的类型所以您不应该有类似`TableCell&lt;S, Button&gt;`这样的代码。`Button`不是数据类型

您的表格单元格实现可以如下所示

```java
public class ActionButtonTableCell&lt;S, T&gt; extends TableCell&lt;S, T&gt; {
    private final Button actionButton;

    public ActionButtonTableCell(String label, Consumer&lt;S&gt; function) {
        this.getStyleClass().add("action-button-table-cell");
        this.actionButton = new Button(label);
        this.actionButton.setOnAction(e -&gt; function.accept(getCurrentItem()));
        this.actionButton.setMaxWidth(Double.MAX_VALUE);
    }
    public S getCurrentItem() {
        // 这里不需要转换:
        return getTableView().getItems().get(getIndex());
    }

    public static &lt;S, T&gt; Callback&lt;TableColumn&lt;S, T&gt;, TableCell&lt;S, T&gt;&gt; forTableColumn(String label, Consumer&lt;S&gt; function) {
        return param -&gt; new ActionButtonTableCell&lt;&gt;(label, function);
    }
    @Override
    public void updateItem(T item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setGraphic(null);
        } else {
            setGraphic(actionButton);
        }
    }
}

现在,您的应用程序代码应该如下所示:

TableView&lt;Customer&gt; tableView = new TableView&lt;&gt;();
// ...
TableColumn&lt;Customer, Void&gt; viewCol = new TableColumn&lt;&gt;("");
TableColumn deleteCol&lt;Customer, Void&gt; = new TableColumn&lt;&gt;("");

// ...

// 请注意使用setCellFactory,而不是setCellValueFactory:
viewCol.setCellFactory(ActionButtonTableCell.&lt;Customer, Void&gt;forTableColumn("View/Edit", c -&gt; 
    new CustomerEditView(stage, user, c.getCustomerId())));
deleteCol.setCellFactory(ActionButtonTableCell.&lt;Customer, Void&gt;forTableColumn("Delete", c -&gt; {
    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);
}));

一般来说,您可能考虑其他用户体验选项。如在原帖评论中提到的,更好的方法可能是在表格外部只有一个编辑按钮和一个删除按钮,它们仅操作选定的项。

如果您有多个操作要执行,我绝对建议不要为每个操作都创建一列。为了演示我上面提到的另一种方法(单元格值工厂返回行的值),下面是一个支持多个操作(按钮)的基于操作的表列的示例,它以更流畅的API风格编写:

import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.layout.HBox;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

public class ActionColumn&lt;S&gt; {

    private final List&lt;Action&lt;S&gt;&gt; actions = new ArrayList&lt;&gt;();
    private String title ;

    private ActionColumn() {}

    public static &lt;S&gt; ActionColumn&lt;S&gt; forType(Class&lt;S&gt; type) {
        return new ActionColumn&lt;&gt;();
    }

    public ActionColumn&lt;S&gt; withTitle(String title) {
        this.title = title ;
        return this;
    }

    public ActionColumn&lt;S&gt; withAction(String name, Consumer&lt;S&gt; action) {
        actions.add(new Action&lt;&gt;(name, action));
        return this;
    }

    public TableColumn&lt;S, ?&gt; build() {
        TableColumn&lt;S, S&gt; column = new TableColumn&lt;&gt;(title);
        column.setCellValueFactory(data -&gt; new SimpleObjectProperty&lt;&gt;(data.getValue()));
        column.setCellFactory(tc -&gt; new ActionColumnCell&lt;&gt;(actions));
        return column;
    }

    private record Action&lt;S&gt;(String name, Consumer&lt;S&gt; action) {}

    private static class ActionColumnCell&lt;S&gt; extends TableCell&lt;S, S&gt; {
        private final HBox buttons ;
        private ActionColumnCell(List&lt;Action&lt;S&gt;&gt; actions) {
            buttons = new HBox(2);
            actions.stream()
                    .map(this::createButton)
                    .forEach(buttons.getChildren()::add);
        }

        private Button createButton(Action&lt;S&gt; action) {
            Button button = new Button(action.name());
            button.setOnAction(e -&gt; action.action().accept(getItem()));
            return button;
        }

        @Override
        protected void updateItem(S item, boolean empty) {
            super.updateItem(item, empty);
            setGraphic(empty ? null : buttons);
        }
    }
}

用法类似:

TableColumn&lt;Item, ?&gt; actionColumn = ActionColumn.forType(Item.class)
    .withTitle("Action")
    .withAction("Edit", this::editItem)
    .withAction("Delete", table.getItems()::remove)
    .build();

这是一个完整的可工作示例:

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;


<details>
<summary>英文:</summary>

There are a number of issues with your code. The primary problem is that you seem to be confusing the `cellFactory` (which is used to generate cells to display in the table column) with the `cellValueFactory` (which is used to provide the value that is displayed by the cells). In this case, the cell doesn&#39;t really need a value (it only relies on the value for the entire row), so you don&#39;t need a `cellValueFactory` (though you could use one to return the value for the row). 

You should also never use raw types (such as `TableView` and `TableColumn`) without type parameters.

Since your action implementations never use the value they return, it seems to make more sense to use a `Consumer&lt;S&gt;` to represent them, instead of a `Function&lt;S,S&gt;` which seems to arbitrarily return the value passed as a parameter.

And finally, the types for `TableView`, `TableColumn`, `TableCell`, etc., should be types of *data*, not types of UI control. So you should never have, e.g., `TableCell&lt;S, Button&gt;`. `Button` is not a data type.

Your table cell implementation can look like

    public class ActionButtonTableCell&lt;S, T&gt; extends TableCell&lt;S, T&gt; {
        private final Button actionButton;
    
        public ActionButtonTableCell(String label, Consumer&lt;S&gt; function) {
            this.getStyleClass().add(&quot;action-button-table-cell&quot;);
            this.actionButton = new Button(label);
            this.actionButton.setOnAction(e -&gt; function.accept(getCurrentItem());
            this.actionButton.setMaxWidth(Double.MAX_VALUE);
        }
        public S getCurrentItem() {
            // No need for a cast here:
            return getTableView().getItems().get(getIndex());
        }
    
        public static &lt;S, T&gt; Callback&lt;TableColumn&lt;S, T&gt;, TableCell&lt;S, T&gt;&gt; forTableColumn(String label, Consumer&lt;S&gt; function) {
            return param -&gt; new ActionButtonTableCell&lt;&gt;(label, function);
        }
        @Override
        public void updateItem(T item, boolean empty) {
            super.updateItem(item, empty);
            if (empty) {
                setGraphic(null);
            } else {
                setGraphic(actionButton);
            }
        }
    }

And now your application code should be something like

        TableView&lt;Customer&gt; tableView = new TableView&lt;&gt;();
        // ...
        TableColumn&lt;Customer, Void&gt; viewCol = new TableColumn&lt;&gt;(&quot;&quot;);
        TableColumn deleteCol&lt;Customer, Void&gt; = new TableColumn&lt;&gt;(&quot;&quot;);

        // ...

        // note setCellFactory, not setCellValueFactory:
        viewCol.setCellFactory(ActionButtonTableCell.&lt;Customer, Void&gt;forTableColumn(&quot;View/Edit&quot;, c -&gt; 
            new CustomerEditView(stage, user, c.getCustomerId())));
        deleteCol.setCellFactory(ActionButtonTableCell.&lt;Customer, Void&gt;forTableColumn(&quot;Delete&quot;, c -&gt; {
            Alert confirm = new Alert(Alert.AlertType.CONFIRMATION, &quot;Are you sure you want to delete this customer?&quot;);
            confirm.show();
            JDBC.deleteCustomer(c.getCustomerId());
            new CustomerList(stage, user);
        }));

---

Generally, you might consider other user experience options here. As mentioned in a comment on the original post, a better approach might be to have a single edit button and a single delete button outside of the table which just operates on the selected item.

I would definitely suggest not having a column for every action if you have multiple actions to perform. Just for the sake of demonstrating the other approach I alluded to above (the cell value factory returning the value for the row), here is an example of an action-based table column which uses that approach and supports multiple actions (buttons) in the same column. This is written in more of a fluid-API style:

    import javafx.beans.property.SimpleObjectProperty;
    import javafx.scene.control.Button;
    import javafx.scene.control.TableCell;
    import javafx.scene.control.TableColumn;
    import javafx.scene.layout.HBox;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.function.Consumer;
    
    public class ActionColumn&lt;S&gt; {
    
        private final List&lt;Action&lt;S&gt;&gt; actions = new ArrayList&lt;&gt;();
        private String title ;
    
        private ActionColumn() {}
    
        public static &lt;S&gt; ActionColumn&lt;S&gt; forType(Class&lt;S&gt; type) {
            return new ActionColumn&lt;&gt;();
        }
    
        public ActionColumn&lt;S&gt; withTitle(String title) {
            this.title = title ;
            return this;
        }
    
        public ActionColumn&lt;S&gt; withAction(String name, Consumer&lt;S&gt; action) {
            actions.add(new Action&lt;&gt;(name, action));
            return this;
        }
    
    
        public TableColumn&lt;S, ?&gt; build() {
            TableColumn&lt;S, S&gt; column = new TableColumn&lt;&gt;(title);
            column.setCellValueFactory(data -&gt; new SimpleObjectProperty&lt;&gt;(data.getValue()));
            column.setCellFactory(tc -&gt; new ActionColumnCell&lt;&gt;(actions));
            return column;
        }
    
        private record Action&lt;S&gt;(String name, Consumer&lt;S&gt; action) {}
    
        private static class ActionColumnCell&lt;S&gt; extends TableCell&lt;S, S&gt; {
            private final HBox buttons ;
            private ActionColumnCell(List&lt;Action&lt;S&gt;&gt; actions) {
                buttons = new HBox(2);
                actions.stream()
                        .map(this::createButton)
                        .forEach(buttons.getChildren()::add);
            }
    
            private Button createButton(Action&lt;S&gt; action) {
                Button button = new Button(action.name());
                button.setOnAction(e -&gt; action.action().accept(getItem()));
                return button;
            }
    
            @Override
            protected void updateItem(S item, boolean empty) {
                super.updateItem(item, empty);
                setGraphic(empty ? null : buttons);
            }
        }
    }

Usage is something like:

    TableColumn&lt;Item, ?&gt; actionColumn = ActionColumn.forType(Item.class)
        .withTitle(&quot;Action&quot;)
        .withAction(&quot;Edit&quot;, this::editItem)
        .withAction(&quot;Delete&quot;, table.getItems()::remove)
        .build();

Here is a complete working example:

    import javafx.application.Application;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.Scene;
    import javafx.scene.control.*;
    import javafx.scene.layout.BorderPane;
    import javafx.stage.Stage;
    
    public class HelloApplication extends Application {
        @Override
        public void start(Stage stage) {
            ObservableList&lt;Item&gt; items = FXCollections.observableArrayList();
            for (int i = 1 ; i &lt;= 20 ; i++) items.add(new Item(&quot;Item &quot;+i));
            TableView&lt;Item&gt; table = new TableView&lt;&gt;(items);
    
            TableColumn&lt;Item, String&gt; itemColumn = new TableColumn&lt;&gt;(&quot;Item&quot;);
            itemColumn.setCellValueFactory(data -&gt; data.getValue().nameProperty());
            table.getColumns().add(itemColumn);
    
    
            table.getColumns().add(
                    ActionColumn.forType(Item.class).withTitle(&quot;Action&quot;)
                            .withAction(&quot;Edit&quot;, this::editItem)
                            .withAction(&quot;Delete&quot;, items::remove)
                            .build()
            );
    
            BorderPane root = new BorderPane(table);
            Scene scene = new Scene(root);
            stage.setScene(scene);
            stage.show();
        }
    
        private void editItem(Item item) {
            new TextInputDialog(item.getName()).showAndWait()
                .ifPresent(item::setName);
        }
    
        public static class Item {
            private final StringProperty name = new SimpleStringProperty();
    
            public String getName() {
                return name.get();
            }
    
            public StringProperty nameProperty() {
                return name;
            }
    
            public void setName(String name) {
                this.name.set(name);
            }
    
            public Item(String name) {
                setName(name);
            }
        }
    
        public static void main(String[] args) {
            launch();
        }
    }

</details>



huangapple
  • 本文由 发表于 2023年5月15日 01:23:56
  • 转载请务必保留本文链接:https://go.coder-hub.com/76248808.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定