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
?
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.
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 theDialogPane
is shown to users inside aStage
, 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.