JavaFX ContextMenu 如何获得被点击的对象?

JavaFX ContextMenu how do I get the clicked Object?

我正在学习javafx.scene.control.ContextMenu,现在遇到一个问题:

如何从 EventHandler 获取被点击的对象? event.source() 和 event.target() return MenuItem.

我举个例子解释一下: 我应该在函数句柄中写什么?

    TextField text = new TextField();
    Label label1 = new Label("hello");
    Label label2 = new Label("world");
    Label label3 = new Label("java");

    ContextMenu menu = new ContextMenu();
    MenuItem item = new MenuItem("copy to text field");
    menu.getItems().add(item);
    item.setOnAction(new EventHandler(){
        public void handle(Event event) {
            //I want to copy the text of the Label I clicked to TextField
            event.consume();
        }
    });

    label1.setContextMenu(menu);
    label2.setContextMenu(menu);
    label3.setContextMenu(menu);

编辑:我希望有一些简单的解决方案(一个衬里),但如果没有,那么有很多复杂的方法可以做到这一点。

您可以创建自己的 ContextMenu 实例并向其添加操作父级以供进一步参考:

public class Main extends Application {

    TextField text = new TextField();

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage primaryStage) {


        Label label1 = new Label("hello");
        Label label2 = new Label("world");
        Label label3 = new Label("java");

        label1.setContextMenu(new MyContextMenu(label1));
        label2.setContextMenu(new MyContextMenu(label2));
        label3.setContextMenu(new MyContextMenu(label3));

        HBox root = new HBox();

        root.getChildren().addAll(text, label1, label2, label3);

        Scene scene = new Scene(root, 300, 100);

        primaryStage.setScene(scene);
        primaryStage.show();

    }

    private class MyContextMenu extends ContextMenu {

        public MyContextMenu(Label label) {

            MenuItem item = new MenuItem("copy to text field");
            item.setOnAction(event -> {

                // I want to copy the text of the Label I clicked to TextField
                text.setText(label.getText());

                event.consume();
            });

            getItems().add(item);

        }

    }
}

只需为每个标签创建一个不同的 ContextMenu 实例:

TextField text = new TextField();
Label label1 = new Label("hello");
Label label2 = new Label("world");
Label label3 = new Label("java");

label1.setContextMenu(createContextMenu(label1, text));       
label2.setContextMenu(createContextMenu(label2, text));            
label3.setContextMenu(createContextMenu(label3, text));

// ...

private ContextMenu createContextMenu(Label label, TextField text) {
    ContextMenu menu = new ContextMenu();
    MenuItem item = new MenuItem("copy to text field");
    menu.getItems().add(item);
    item.setOnAction(new EventHandler(){
        public void handle(Event event) {
            text.setText(label.getText());
        }
    });
    return menu ;
}

我认为最简单的方法是将节点保存为上下文菜单的用户数据。

EventHandler<? super ContextMenuEvent> eventHandle = e->menu.setUseData(e.getSource());
label1.setOnContextMenuRequested(eventHandle );
label2.setOnContextMenuRequested(eventHandle );
label3.setOnContextMenuRequested(eventHandle );

并在行动:

EventHandler<ActionEvent> menuItemEvent = e->{
    Node node = (Node) ((MenuItem)e.getSource()).getParentPopup().getUserData();
   ...
};

总结基本要求:掌握为其打开上下文菜单的节点。根据 PopupWindow(ContextMenu 的祖父母)的 api 文档,这应该很容易实现

show(Node node, ...)

... The popup is associated with the specified owner node...

Node getOwnerNode()

The node which is the owner of this popup.

所以 MenuItem 操作的一般方法是

  • 获取项目的 parentPopup(即 contextMenu),如果有嵌套菜单,可能需要向上爬
  • 抓住它的ownerNode
  • 访问任何 属性 需要的东西

最后的示例只是在 copyText 中执行此操作并验证它是否按预期工作...前提是我们 使用控件的 contextMenuProperty。控件中 not-working 的原因是 ContextMenu 的方法合同违规(可能 introduced by a bug fix 围绕 textInputControls 中的 auto-hide 行为):它总是在设置后使用 show(Window w, ..)作为任何控件的上下文菜单(实现细节:Control.contextMenuProperty 设置一个标志 setShowRelativeToWindow(true) 触发 mis-behavior)

现在我们可以做什么来获得ownerNode?有几个选项,none 其中不错:

  • 就像在其他答案中所做的那样,以某种方式跟踪 ownerNode:通过使用工厂方法,通过存储在用户属性或任何其他 ad-hoc 意味着
  • 扩展 ContextMenu,覆盖 show(Node owner, ... ) 并将给定所有者保留在自定义 属性
  • 扩展 ContextMenu,覆盖 show(Node owner, ...) 变脏并反射性地将 super ownerNode 设置为给定的
  • 变脏并反射性地将有问题的 showRelativeToWindow 标志重置回 false 将菜单​​设置为任何控件后

前两个引入了额外的耦合,后者(除了肮脏的反射访问)可能 re-introduce auto-hide 的问题("fixed" 行为本身就是肮脏的..违反"keep-open-if-owner-clicked"保证)

最后,一个可以玩的例子:

public class ContextMenuOwnerSO extends Application {

    private Parent createContent() {

        TextField text = new TextField();
        // the general approach to grab a property from the Node
        // that the ContextMenu was opened on
        EventHandler<ActionEvent> copyText = e -> {
            MenuItem source = (MenuItem) e.getTarget();
            ContextMenu popup = source.getParentPopup();
            String ownerText = "<not available>";
            if (popup != null) {
                Node ownerNode = popup.getOwnerNode();
                if (ownerNode instanceof Labeled) {
                    ownerText = ((Label) ownerNode).getText();
                } else if (ownerNode instanceof Text) {
                    ownerText = ((Text) ownerNode).getText();
                }
            }
            text.setText(ownerText);
        };

        MenuItem printOwner = new MenuItem("copy to text field");
        printOwner.setOnAction(copyText);

        // verify with manual managing of contextMenu
        Text textNode = new Text("I DON'T HAVE a contextMenu property");
        Label textNode2 = new Label("I'm NOT USING the contextMenu property");
        ContextMenu nodeMenu = new ContextMenu();
        nodeMenu.getItems().addAll(printOwner);
        EventHandler<ContextMenuEvent> openRequest = e -> {
            nodeMenu.show((Node) e.getSource(), Side.BOTTOM, 0, 0);
            e.consume();
        };

        textNode.setOnContextMenuRequested(openRequest);
        textNode2.setOnContextMenuRequested(openRequest);

        Label label1 = new Label("I'm USING the contextMenu property");

        ContextMenu menu = new ContextMenu() {

            // force menu to have an owner node: this being the case, it is not hidden 
            // on mouse events inside its owner
            //@Override
            //public void show(Node anchor, double screenX, double screenY) {
            //    ReadOnlyObjectWrapper<Node> owner = 
            //            (ReadOnlyObjectWrapper<Node>) 
            //            FXUtils.invokeGetFieldValue(PopupWindow.class, this, "ownerNode");
            //    owner.set(anchor);
            //    super.show(anchor, screenX, screenY);
            //}

        };
        MenuItem item = new MenuItem("copy to text field");
        menu.getItems().add(item);
        item.setOnAction(copyText);

        label1.setContextMenu(menu);
        // same effect as forcing the owner node 
        // has to be done after the last setting of contextMenuProperty 
        // setting to true was introduced as fix for
        // https://bugs.openjdk.java.net/browse/JDK-8114638
        //FXUtils.invokeGetMethodValue(ContextMenu.class, menu, "setShowRelativeToWindow", Boolean.TYPE, false);

        VBox content = new VBox(10, textNode, textNode2, text, label1);
        return content;

    }

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(new Scene(createContent(), 400, 200));
        stage.setTitle(FXUtils.version());
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    @SuppressWarnings("unused")
    private static final Logger LOG = Logger
            .getLogger(ContextMenuOwnerSO.class.getName());

}

我知道有人问这个问题已经有一段时间了,但是当我想用 JavaFX 上下文菜单解决我的类似问题时,我 运行 进入了这个线程,Oleksandr Potomkin 的回答给了我一个想法如何解决。

我想要实现的是一个正常运行的 ContextMenu(一个用于多个字段),当我单击 MenuItem 时,它可以让我访问打开上下文菜单(或已被 Accelerator 调用)的控件。

我在设置加速器时也遇到了问题 - 如果我专注于形式,它会起作用,但如果我专注于所需的控制,它就不会起作用。应该是反过来的...

我所做的是我创建了一个 class 来初始化一个 ContextMenu(在它的构造函数中)并共享一个方法 link 该上下文菜单到所需的控件:

public class FieldContextMenu {
    ContextMenu menu;
    MenuItem menuCopy;

    public FieldContextMenu() {
        menu = new ContextMenu();

        menuCopy = new MenuItem("Copy");
        menuCopy.setAccelerator(KeyCombination.keyCombination("Ctrl+C"));
        menuCopy.setOnAction(event -> System.out.println(((TextField) menu.getUserData()).getText()));

        menu.getItems().addAll(menuCopy);
    }

    public void link(Control ctrl) {
        ctrl.setContextMenu(menu);
        // onKeyPressed so KeyCombination work while focused on this control
        ctrl.setOnKeyPressed(event -> {
            if(event.isControlDown() && event.getCode() == KeyCode.C) {
                menu.setUserData(ctrl);
                menuCopy.fire();
            }
        });
        // setting this control in menus UserData when ContextMenu is activated in this control
        ctrl.setOnContextMenuRequested(e -> menu.setUserData(ctrl));
    }
}

下面是我在 FXML 控制器中的使用方式:

public class ExampleController {
    @FXML private AnchorPane rootPane;
    @FXML private TextField textField1;
    @FXML private TextField textField2;

    @FXML protected void initialize() {
        // consume roots keyPressed event so the accelerator wouldn't "run" when outside of the control
        rootPane.setOnKeyPressed(event -> {
            if(event.isControlDown()) event.consume();
        });

        FieldContextMenu contextMenu = new FieldContextMenu();
        contextMenu.link(textField1);
        contextMenu.link(textField2);
    }
}

我这样做的方式是 ContextMenu 只初始化一次 = 更少的内存使用量(如果我没想错的话)。