再次按下按钮后,JavaFx 中的警报位置更改为默认值

Alert position in JavaFx changing to default after I'm pressing button again

我在点击特定位置的按钮时显示警告消息。

我已经完成了,但是当我第二次按下 button 时,位置正在更改为默认位置。

MessageController.java

 public class MessageController implements Initializable {

    Alert alert = new Alert(Alert.AlertType.INFORMATION);

    @FXML
    private void handleButtonAction(ActionEvent event) {
        alert.setTitle("Message");
        alert.setHeaderText("You clicked button");
        alert.show();
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        ((Stage) alert.getDialogPane().getScene().getWindow()).setX(50);
        ((Stage) alert.getDialogPane().getScene().getWindow()).setY(50);
    }

Message.fxml

<AnchorPane id="AnchorPane" prefHeight="200" prefWidth="320" xmlns:fx="http://javafx.com/fxml/1" fx:controller="message.MessageController">
    <children>
        <Button layoutX="126" layoutY="90" text="Click Me!" onAction="#handleButtonAction" fx:id="button" />
    </children>
</AnchorPane>  

Message.java(main class)

public class Message extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("Message.fxml"));

        Scene scene = new Scene(root);

        stage.setScene(scene);
        stage.show();
    }

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

}

如果我在方法内部显示警报时使用 setX 和 setY,行为相同

private void handleButtonAction(ActionEvent event) {
    alert.setTitle("Message");
    alert.setHeaderText("You clicked button");
    ((Stage) alert.getDialogPane().getScene().getWindow()).setX(50);
    ((Stage) alert.getDialogPane().getScene().getWindow()).setY(50);
    alert.show();
}  

Above code also throws NullPointerException after clicked button for second time.

如何设置Alert的固定位置?

您可以通过在单击按钮时创建对话框来实现您想要的效果,而不是作为 class 成员,这样您的控制器将看起来像这样

public class MessageController implements Initializable {

    @FXML
    private void handleButtonAction(ActionEvent event) {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setTitle("Message");
        alert.setHeaderText("You clicked button");
        alert.setX(50);
        alert.setY(50);
        alert.show();
    }

    @Override
    public void initialize(URL url, ResourceBundle rb) {
    }
}

以及您之前获得 NullPointerException 的原因是因为您试图获得在按钮单击功能 alert.show() 之前不存在的对话框阶段

简答

如果您为 Alert 指定所有者 Window,问题就会消失。你不应该使用:

((Stage) alert.getDialogPane().getScene().getWindow()).setX(50);
((Stage) alert.getDialogPane().getScene().getWindow()).setY(50);

设置xy属性。请改用以下内容:

alert.setX(50);
alert.setY(50);

注:参见Dialog的文档,Alert的超class,了解更多信息。

例如:

private void handleButtonAction(ActionEvent event) {
    alert.initOwner(((Node) event.getSource()).getScene().getWindow());
    alert.setTitle("Message");
    alert.setHeaderText("You clicked button");
    alert.setX(50);
    alert.setY(50);
    alert.show();
}

如果您不想分配所有者,另一种选择是每次都使用一个新的 Alert 实例,而不是重复使用单个实例。 显示了一个例子。


长答案

请注意,这都与实施细节有关。

Alert 或更一般地说,Dialog 在您第二次显示时位于屏幕中心的原因是实现的一个怪癖(可能是一个错误?)。负责该行为的代码在 package-private javafx.scene.control.HeavyweightDialog class 中。当您显示对话框时,将调用以下方法:

@Override public void show() {
    scene.setRoot(dialogPane);
    stage.centerOnScreen();
    stage.show();
}

showAndWait() 遵循完全相同的过程,只是它调用 stage.showAndWait() 而不是 stage.show()。如您所见,centerOnScreen() 方法在显示 Stage 之前被调用。但是,对话框使用的 Stage 是自定义匿名 class,它覆盖了 centerOnScreen():

final Stage stage = new Stage() {
    @Override public void centerOnScreen() {
        Window owner = HeavyweightDialog.this.getOwner();
        if (owner != null) {
            positionStage();
        } else {
            if (getWidth() > 0 && getHeight() > 0) {
                super.centerOnScreen();
            }
        }
    }
};

您没有指定所有者,因此执行了 else 路径。这就是怪癖发挥作用的地方。在显示对话框之前调用 getWidth()getHeight() return NaN 不被认为大于 0,这意味着超级实现centerOnScreen() 未被调用,Stage 使用您明确设置的 xy 值。但是第二次显示对话框时 getWidth()getHeight() return 数字都大于 0 这意味着 super.centerOnScreen() 被调用并且该方法覆盖任何显式设置xy 个值。

但是,如果您确实为对话框指定了所有者,则永远不会调用 super.centerOnScreen() 方法;相反,使用 positionStage() 方法。该方法用于将对话框置于其所有者的中心,但与 centerOnScreen() 不同,它尊重为 xy 属性设置的任何显式值。

这意味着一种解决方案是简单地为您的 Alert 分配一个所有者,它将始终显示在您想要的位置。不幸的是,如果您的 Alert 没有所有者,我认为您无法优雅地解决问题。一种选择是在显示后设置 xy 但这可能会或可能不会导致对话框显示在屏幕中央,然后跳转到您设置的位置——不完全是为您的最终用户提供理想的体验。这是假设您必须继续使用相同的 Alert 实例。如果每次都使用新的 Alert 实例是可以接受的,那么这也可以解决问题。

另外你不需要使用:

((Stage) alert.getDialogPane().getScene().getWindow()).setX(50);
((Stage) alert.getDialogPane().getScene().getWindow()).setY(50);

为了设置xy值。其一,setXsetY 方法在 Window class 中声明,因此不需要强制转换为 Stage。但更重要的是 Dialog class 还声明了 xy 属性。该实现甚至委托给 FXDialog 实现,在 HeavyweightDialog 的情况下,它只是在内部使用的 Stage 上设置值;换句话说,调用:

alert.setX(50);
alert.setY(50);

做完全相同的事情(主要是,见下文)。

最重要的是,使用 getDialogPane().getScene().getWindow()NullPointerException 的原因。当你创建一个 Dialog 时,它有一个初始的 DialogPane,它会自动添加到 Scene,后者又会自动添加到内部 Stage。所以第一次你没有得到NPE。但是,当您关闭对话框时,将调用以下内容(再次来自 HeavyweightDialog):

@Override public void close() {
    if (stage.isShowing()) {
        stage.hide();
    }

    // Refer to RT-40687 for more context
    if (scene != null) {
        scene.setRoot(DUMMY_ROOT);
    }
}

如您所见,它取代了 Scene 的根,这意味着 DialogPane 不再有 Scene,因此 NPE。这是使用 Dialog#setXDialog#setY 的另一个原因。请注意 DialogPaneshow()showAndWait() 方法中再次成为根。

请记住,使用 Stage 是一个实现细节。来自 the documentation:

A Dialog in JavaFX wraps a DialogPane and provides the necessary API to present it to end users. In JavaFX 8u40, this essentially means that the DialogPane is shown to users inside a Stage, but future releases may offer alternative options (such as 'lightweight' or 'internal' dialogs). This API therefore is intentionally ignorant of the underlying implementation, and attempts to present a common API for all possible implementations.

您应该只使用 DialogDialogPane 提供的 API,不要对实现做任何假设。