Search code examples
javafxpositionalert

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


I'm showing an alert message on button click at specific position.

I've done but position is changing to default position as I'm pressing a button for second time.

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);
    }

}

Same behavior if I use setX and setY where I'm showing an alert inside method

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.

How to set fixed position of Alert?


Solution

  • Short Answer

    If you assign your Alert an owner Window the problem goes away. And you should not be using:

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

    To set the x and y properties. Use the following instead:

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

    Note: See the documentation of Dialog, the superclass of Alert, for more information.

    For example:

    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();
    }
    

    Another option, if you don't want to assign an owner, is to use a new Alert instance each time rather than reusing a single instance. The answer by Ahmed Emad shows an example of this.


    Long Answer

    Note this all relates to implementation details.

    The reason the Alert or, more generally, Dialog is centered on the screen when you show it the second time is a quirk (possibly a bug?) of the implementation. The code responsible for the behavior is in the package-private javafx.scene.control.HeavyweightDialog class. When you show the dialog the following method is called:

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

    The exact same procedure is followed for showAndWait() except it calls stage.showAndWait() instead of stage.show(). As you can see, the centerOnScreen() method is invoked before the Stage is shown. However, the Stage used by the dialog is a custom anonymous class that overrides 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();
                }
            }
        }
    };
    

    You don't specify an owner so the else path is executed. This is where the quirk comes into play. Before the dialog has been displayed the calls to both getWidth() and getHeight() return NaN which is not considered to be greater than 0, which means the super implementation of centerOnScreen() is not invoked and the Stage uses the x and y values you explicitly set. But then the second time you display the dialog both getWidth() and getHeight() return numbers greater than 0 which means super.centerOnScreen() is invoked and that method overrides any explicitly set x or y values.

    If you do assign the dialog an owner, however, the super.centerOnScreen() method will never be invoked; instead, the positionStage() method is used. That method is used to center the dialog over its owner but, unlike centerOnScreen(), it respects any explicit values set for the x and y properties.

    This means one solution is to simply assign your Alert an owner and it will always be displayed in the position you want it to. Unfortunately, I don't believe you can solve the problem elegantly if your Alert has no owner. One option is to set x and y after it has been shown but that may or may not lead to the dialog being displayed in the center of the screen and then jumping to the position you set—not exactly an ideal experience for your end users. This is assuming you must continue using the same Alert instance. If using a new Alert instance each time is acceptable then that would fix the problem as well.

    Also you don't need to use:

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

    In order to set the x and y values. For one, the setX and setY methods are declared in the Window class and so the cast to Stage is unnecessary. But more importantly the Dialog class also declares x and y properties. The implementation even delegates to the FXDialog implementation which, in the case of HeavyweightDialog, simply sets the value on the internally used Stage; in other words, calling:

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

    Does the exact same thing (mostly, see below).

    On top of that, using getDialogPane().getScene().getWindow() is the cause of your NullPointerException. When you create a Dialog it has an initial DialogPane which is automatically added to a Scene which in turn is automatically added to the internal Stage. So the first time you don't get an NPE. However, when you close the dialog the following is called (from HeavyweightDialog again):

    @Override public void close() {
        if (stage.isShowing()) {
            stage.hide();
        }
    
        // Refer to RT-40687 for more context
        if (scene != null) {
            scene.setRoot(DUMMY_ROOT);
        }
    }
    

    As you can see, it replaces the root of the Scene which means the DialogPane no longer has a Scene and thus the NPE. This is another reason to use Dialog#setX and Dialog#setY instead. Note the DialogPane is made the root again in the show() and showAndWait() methods.

    Keep in mind the fact a Stage is used is an implementation detail. From 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.

    You should only use the API provided by Dialog and DialogPane and not assume anything about the implementation.