我遇到过在 Linux 上运行的 JavaFX 应用程序的奇怪行为。 应用程序是一个简单的图形编辑器。它的主要区域由带有两个滚动条的 ScrollPane 和滚动窗格的内容 AnchorPane 表示,其子级是两层 - 图形层(放置所有对象的位置)和选择层(用于实现选择框)。 图形层还有上下文菜单。
选择框是通过检测拖动事件来实现的。按下鼠标按钮并创建选择框,然后移动鼠标并根据鼠标 X 和 Y 坐标重新绘制矩形。当您释放按钮时,矩形内的所有对象都将被选中,然后矩形将被删除。此行为的关键时刻是,每当我将鼠标拖动到舞台外时,仍然会检测到鼠标拖动事件,因此矩形的大小会增加,滚动窗格的滚动条也会相应地减小,这表明整体图形区域大小增加了。这是正常行为。
问题是,打开上下文菜单后,当我尝试使用选择框选择对象时,选择框无法将其自身增加到滚动窗格的可见区域之外,导致在舞台之外不再检测到拖动事件。检测到鼠标按下,仅在滚动窗格的可见区域内检测到鼠标拖动。但是,如果我打开上下文菜单,然后在图形层上按鼠标按钮,然后使用选择框,则一切正常。另一种恢复正常行为的方法是按 ESC 键。
此问题仅存在于 Linux 上,在 Windows 上一切正常。从带有内置 JavaFX 的 Java 8 迁移到带有 JavaFX 21 的 Java 17 后,也出现了此问题。
我想出了几个解决这个问题的方法,例如使用Robot API在隐藏上下文菜单时按ESC键,但我想以更干净、更合适的方式解决这个问题。
我将不胜感激任何帮助。
最小的、可重复的示例
package org.example;
import javafx.application.Application;
import javafx.geometry.Point2D;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.ScrollPane;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
public static void main(String[] args) {
launch(args);
}
public void start(Stage primaryStage) {
AnchorPane mainPane = new AnchorPane();
//Creating scroll pane
ScrollPane scroll = new ScrollPane();
scroll.setContent(mainPane);
scroll.setHbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
scroll.setVbarPolicy(ScrollPane.ScrollBarPolicy.ALWAYS);
scroll.setPrefWidth(1024);
scroll.setPrefHeight(768);
//Creating selection layer to place selection frame
AnchorPane selectionLayer = new AnchorPane();
selectionLayer.setMouseTransparent(true);
//Creating graphic layer
BorderPane graphicLayer = new BorderPane();
graphicLayer.setMinHeight(768);
graphicLayer.setMinWidth(1024);
graphicLayer.setFocusTraversable(false);
//Creating context menu
ContextMenu contextMenu = new ContextMenu();
contextMenu.getItems().add(new MenuItem("test1"));
contextMenu.getItems().add(new MenuItem("test2"));
graphicLayer.setOnContextMenuRequested(e -> contextMenu.show(graphicLayer, e.getScreenX(), e.getScreenY()));
SelectionController controller = new SelectionController(selectionLayer);
graphicLayer.setOnMousePressed(event -> {
System.err.println("PRESSED");
if (contextMenu.isShowing()) {
contextMenu.hide();
}
controller.startSelection(event);
});
graphicLayer.setOnMouseDragged(event -> {
System.err.println("DRAGGED");
controller.setPosition(event);
});
graphicLayer.setOnMouseReleased(event -> {
System.err.println("RELEASED");
controller.finishSelection(event);
});
mainPane.getChildren().addAll(graphicLayer, selectionLayer);
Scene scene = new Scene(scroll);
primaryStage.setScene(scene);
primaryStage.show();
}
class SelectionController {
AnchorPane selectionLayer;
Point2D startingPoint;
Rectangle selectionFrame;
SelectionController(AnchorPane selectionLayer) {
this.selectionLayer = selectionLayer;
}
void startSelection(MouseEvent event) {
startingPoint = new Point2D(event.getX(), event.getY());
selectionFrame = new Rectangle();
selectionFrame.setId("selection_frame");
selectionFrame.setFill(Color.TRANSPARENT);
selectionFrame.getStrokeDashArray().add(10.0);
selectionFrame.setStrokeWidth(2.0);
selectionFrame.setStroke(Color.LIGHTSKYBLUE);
selectionFrame.xProperty().set(startingPoint.getX());
selectionFrame.yProperty().set(startingPoint.getY());
selectionLayer.getChildren().add(selectionFrame);
}
void setPosition(MouseEvent event) {
double x = event.getX();
double y = event.getY();
if (x < startingPoint.getX()) {
selectionFrame.xProperty().set(x);
selectionFrame.widthProperty().set(startingPoint.getX() - x);
} else {
selectionFrame.widthProperty().set(x - startingPoint.getX());
}
if (y < startingPoint.getY()) {
selectionFrame.yProperty().set(y);
selectionFrame.heightProperty().set(startingPoint.getY() - y);
} else {
selectionFrame.heightProperty().set(y - startingPoint.getY());
}
}
void finishSelection(MouseEvent event) {
// To some logic to filter objects on graphic layer inside selection frame
selectionLayer.getChildren().clear();
}
}
}
重现动作:
在尝试了很多不同的选项之后,我找到了一个可以接受的解决方法。 ContextMenu 的自动隐藏机制似乎对底层窗格有一些影响,因此:
contextMenu.setAutoHide(false);
解决问题。
当然,现在我必须手动处理菜单隐藏,但无论如何我会坚持使用这个解决方案。