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 文档,这应该很容易实现
... The popup is associated with the specified owner node...
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 只初始化一次 = 更少的内存使用量(如果我没想错的话)。
我正在学习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 文档,这应该很容易实现
... The popup is associated with the specified owner node...
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 只初始化一次 = 更少的内存使用量(如果我没想错的话)。