JavaFX 中的 ContextMenu 不断获取相同 MenuItem 的副本

ContextMenu in JavaFX keeps getting duplicates of the same MenuItem

我使用这个 class 为 TreeItem 提供一个用于编辑的文本字段(与问题无关)并在 TreeItem 上设置上下文菜单:

package domain;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;

public final class TextFieldTreeCellImpl extends TreeCell<MyNode> {

    private TextField textField;
    private ContextMenu cm = new ContextMenu();

    String oldItem = "";

    private Connection connection;
    String url = "jdbc:sqlserver://localhost:1433;databaseName=HOGENT1415_11";
    String user = "sa";
    String password = "root";
    Statement statement;

    public TextFieldTreeCellImpl() throws SQLException {
        connection = DriverManager.getConnection(url, user, password);
        statement = connection.createStatement();
    }

    @Override
    public void startEdit() {
        super.startEdit();

        if (textField == null) {
            createTextField();
        }
        setText(null);
        setGraphic(textField);
        textField.selectAll();
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText((String) getItem().value);
        setGraphic(getTreeItem().getGraphic());
    }

    @Override
    public void updateItem(MyNode item, boolean empty) {

        super.updateItem(item, empty);

        if (empty) {
            setText(null);
            setGraphic(null);
        } else {
            if (isEditing()) {
                if (textField != null) {
                    textField.setText(getString());
                }
                setText(null);
                setGraphic(textField);
            } else {
                setText(getString());
                setGraphic(getTreeItem().getGraphic());
                
                MenuItem cmItem1 = new MenuItem("Add continent");
                cmItem1.setOnAction(new EventHandler<ActionEvent>() {
                    @Override
                    public void handle(ActionEvent e) {
                        System.out.println("Geklikt!");
                    }
                });
                cm.getItems().add(cmItem1);
                setContextMenu(cm);
            }
        }

    }

    private void createTextField() {
        textField = new TextField(getString());
        textField.setOnKeyReleased(new EventHandler<KeyEvent>() {

            @Override
            public void handle(KeyEvent t) {
                if (t.getCode() == KeyCode.ENTER) {

                    String sql = "UPDATE Continents SET Name='" + textField.getText() + "' WHERE ContinentID=" + getItemId();
                    if (getItem().isCountry()) {
                        sql = "UPDATE Countries SET Name='" + textField.getText() + "' WHERE CountryID=" + getItemId();
                    }
                    if (getItem().isClimateChart()) {
                        sql = "UPDATE ClimateCharts SET Location='" + textField.getText() + "' WHERE ClimateChartID=" + getItemId();
                    }

                    try {
                        ResultSet result = statement.executeQuery(sql);
                    } catch (SQLException ex) {
                        Logger.getLogger(TextFieldTreeCellImpl.class.getName()).log(Level.SEVERE, null, ex);
                    }
                    commitEdit(new MyNode(textField.getText(), getType(), getItemId()));
                } else if (t.getCode() == KeyCode.ESCAPE) {
                    cancelEdit();
                }
            }
        });
    }

    private String getString() {
        return getItem() == null ? "" : getItem().toString();
    }

    private String getType() {
        return getItem() == null ? "" : getItem().type;
    }

    private int getItemId() {
        return getItem() == null ? null : getItem().id;
    }
}

我使用以下代码在我的控制器 class 中实例化此 class:

selectionTreeView.setEditable(true);
        selectionTreeView.setCellFactory(new Callback<TreeView<MyNode>, TreeCell<MyNode>>() {
            @Override
            public TreeCell<MyNode> call(TreeView<MyNode> p) {
                try {
                    return new TextFieldTreeCellImpl();
                } catch (SQLException ex) {
                    Logger.getLogger(MainPanel.class.getName()).log(Level.SEVERE, null, ex);
                }
                return null;
            }

        });

然而,当我 运行 程序并右键单击项目时,一切正常,但如果我继续单击其他项目,则会继续在上下文菜单中获取项目。

为了缩小范围,每次我双击一个项目时都会发生这种情况。

查看截图:

我知道这是因为 ipdateItem 不断被调用,但我该如何防止这种情况?

每次调用 updateItem(...) 时,您都会再次添加菜单项(并且永远不会删除它)。因此,每次重复使用单元格时,它都会获得菜单项的另一个副本。

最有效的方法是在构造函数中创建菜单项并将其传递给那里的上下文菜单。请注意,事件处理程序可以轻松访问当前项目:

public TextFieldTreeCellImpl() throws SQLException {
    connection = DriverManager.getConnection(url, user, password);
    statement = connection.createStatement();

    MenuItem cmItem1 = new MenuItem("Add continent");
    cmItem1.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent e) {
            MyNode item = getItem();
            // ...
            System.out.println("Geklikt!");
        }
    });
    cm.getItems().add(cmItem1);

}

您可能还想删除空单元格的上下文菜单:

@Override
public void updateItem(MyNode item, boolean empty) {

    super.updateItem(item, empty);
    if (empty) {
        setText(null);
        setGraphic(null);
        setContextMenu(null);
    } else {
        if (isEditing()) {
            if (textField != null) {
                textField.setText(getString());
            }
            setText(null);
            setGraphic(textField);
        } else {
            setText(getString());
            setGraphic(getTreeItem().getGraphic());

            setContextMenu(cm);
        }
    }
}

如果需要,您还可以在 updateItem(...) 方法中进一步配置菜单项,方法是将它们声明为字段,例如

public final class TextFieldTreeCellImpl extends TreeCell<MyNode> {

    private TextField textField;
    private ContextMenu cm = new ContextMenu();

    private MenuItem cmItem1 ;

    // ...

    @Override
    public void updateItem(MyNode item, boolean empty) {
        super.updateItem(item, empty);

        // ...

        cmItem1.setText(...);

    }
}

最后,如果您真的需要在项目更改时完全重构上下文菜单,那么您可以这样做

    @Override
    public void updateItem(MyNode item, boolean empty) {
        super.updateItem(item, empty);

        // ...

        cm.getItems().clear();
        // Now create all menu items from scratch and add to the context menu

    }