Below is a small Application that illustrates the problem:
ButtonPanel.fxml
<ScrollPane fx:controller="ButtonPanelController">
<VBox>
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
ButtonPanelController.java
public class ButtonPanelController {
#FXML
Button myButton;
boolean isRed = false;
public void buttonClickedAction(ActionEvent event) {
if(isRed) {
myButton.setStyle("");
} else {
myButton.setStyle("-fx-background-color: red");
}
isRed = !isRed;
}
}
TestApp.java
public class TestApp extends Application {
ButtonPanelController buttonController;
#Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
Parent root = (Parent) loader.load();
// Grab the instance of ButtonPanelController
buttonController = loader.getController();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
/* Override the ControllerFactory callback to use
* the stored instance of ButtonPanelController
* instead of creating a new one.
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return buttonController;
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}
Basically, I have a single FXML that I am using for two separate scenes that may or may not be active on the screen at the same time. A practical example of this would be having content docked to a side panel plus a button that opens the same content in a separate window that can be dragged/resized/etc.
The goal I am trying to achieve is to keep the views in sync (changes to one of the views affects the other one).
I am able to point both views to the same controller via a callback however the issue I am running into now is that the UI changes are only reflected on the 2nd scene. Both views talk to the controller but the controller only talks back to the 2nd scene. I'm assuming something with JavaFX's implementation of MVC or IOC is linking the controller to the view in some 1:1 relationship when it is loaded via the FXMLLoader.
I am well aware that trying to link two views to 1 controller is bad MVC practice, however I would like to avoid having to implement a separate FXML and Controller that are practically identical.
Is it possible to achieve this kind of synchronization that I listed above?
If I need to create a separate Controller, what's the best way to ensure that both UI's are in sync (even down to sidebar movements)?
Thanks in Advance!
-Steve
The reason your code doesn't work is that the FXMLLoader injects references to elements in the FXML with fx:id attributes into fields in the controller with matching names. So when you load the FXML file the first time, the FXMLLoader sets the field myButton to be a reference to the button it creates when it loads the FXML. Since you use the exact same controller instance the second time you load the FXML, the FXMLLoader now sets that same field (in the same controller instance) to be a reference to the button it creates when the FXML file is loaded again. In other words, buttonController.myButton now refers to the second button created, not the first. So when you call myButton.setStyle(...) it updates the style of the second button.
Basically, you always want one controller instance per view instance. What you need is for both controllers to access the same shared state.
Create a model class that stores the data. In a MVC architecture, the View observes the model and updates when the data in the model changes. The controller reacts to user interaction with the view and updates the model.
(Arguably, FXML gives you more of a MVP architecture, which is similar. There are variants of this too, but generally the presenter will observe the model and update the view when data in the model changes, as well as update the model in response to user interaction.)
So your model might look like:
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
public class Model {
private final BooleanProperty red = new SimpleBooleanProperty();
public final BooleanProperty redProperty() {
return this.red;
}
public final boolean isRed() {
return this.redProperty().get();
}
public final void setRed(final boolean red) {
this.redProperty().set(red);
}
public void toggleRed() {
setRed(! isRed() );
}
}
Your ButtonPanel.fxml doesn't change:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<ScrollPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="ButtonPanelController">
<VBox >
<Button fx:id="myButton" text="Click Me" onAction="#buttonClickedAction" />
</VBox>
</ScrollPane>
Your controller has a reference to the model. It can use bindings or listeners on the model properties to update the UI, and the handler methods just update the model:
import javafx.beans.binding.Bindings;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
public class ButtonPanelController {
#FXML
Button myButton;
boolean isRed = false;
private Model model ;
public ButtonPanelController(Model model) {
this.model = model ;
}
public void initialize() {
myButton.styleProperty().bind(Bindings.
when(model.redProperty()).
then("-fx-background-color: red;").
otherwise("")
);
}
public void buttonClickedAction(ActionEvent event) {
model.toggleRed();
}
}
Finally, you keep everything synchronized because the views are views of the same model. In other words you just create one model and hand its reference to both controllers. Since I made the model a constructor parameter in the controller (which is nice, because you know you have a model as soon as the instance is created), we need a controller factory to create the controller instances:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TestApp extends Application {
#Override
public void start(Stage stage) throws Exception {
// 1st Stage
stage.setTitle("1st Stage");
stage.setWidth(200);
stage.setHeight(200);
stage.setResizable(false);
// The one and only model we will use for both views and controllers:
Model model = new Model();
/* Override the ControllerFactory callback to create
* the controller using the model:
*/
Callback<Class<?>, Object> controllerFactory = type -> {
if(type == ButtonPanelController.class) {
return new ButtonPanelController(model);
} else {
try {
return type.newInstance();
} catch(Exception e) {
throw new RuntimeException(e);
}
}
};
// Load FXML
FXMLLoader loader = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
loader.setControllerFactory(controllerFactory);
Parent root = (Parent) loader.load();
// Show 1st Scene
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
// 2nd Stage
Stage stage2 = new Stage();
stage2.setTitle("2nd Stage");
stage2.setWidth(200);
stage2.setHeight(200);
stage2.setResizable(false);
// Load FXML
FXMLLoader loader2 = new FXMLLoader(
ButtonPanelController.class.getResource("ButtonPanel.fxml"));
// Set the ControllerFactory before the load takes place
loader2.setControllerFactory(controllerFactory);
Parent root2 = (Parent) loader2.load();
// Show 2nd Scene
Scene scene2 = new Scene(root2);
stage2.setScene(scene2);
stage2.show();
}
public static void main(String[] args) {
launch(args);
}
}
Related
I have an app, which has HomeScene.fxml file with headers and menu. HomeScene has also dashboardPane, which should be changed dynamically after menu button is being pressed. Dashboard pane content should be loaded from another fxml file, lets say 'FinancesPane.fxml' or 'SettingsPane.fxml'.
Im trying to replace content of dashboardPane in HomeController:
#FXML
public void handleFinancesButtonAction() {
FinancesPaneFactory paneFactory = new FinancesPaneFactory();
dashBoardPane.getChildren().clear();
dashBoardPane.getChildren().add(paneFactory.createPane());
}
My FinancesPaneFactory looks like this:
public class FinancesPaneFactory extends PaneFactory {
private static final String PANE_TEMPLATE_PATH = "/sceneTemplates/FinancesPane.fxml";
public FinancesPaneFactory() {
super(PANE_TEMPLATE_PATH );
}
#Override
protected Pane generatePane(FXMLLoader loader) {
try {
return (Pane) loader.load();
} catch (IOException e) {
throw new FatBirdRuntimeException("Unable to load FinancesPane", e);
}
}
}
To be more clear, this is how HomeScene looks like: HomeScene .
This empty space is a dashboardPane, and should be replaced with another content when user press the left menu button.
How to inject this content dynamically?
Yes, you should do this to keep scene graph low and you will benefit from better performance , what i do is create dynamic container :
#FXML
private ScrollPane dynamicNode;
Scroll pane is a good choice.
This is put to MainController.
I have main controller different from others , main controller is actually the only one i initialize, so in your main program class whatever you call it :
private static MainViewController mainViewController;
...
private static BorderPane loadMainPane() throws IOException {
FXMLLoader loader = new FXMLLoader();
loader.setController(mainViewController);
BorderPane mainPane = (BorderPane) loader.load(
CsgoRr.class
.getResourceAsStream(Info.Resource.FXML_FILE_MAIN));
mainPane.getStylesheets().add(CsgoRr.class.getResource("path...style.css").toString());
return mainPane;
}
Dont forget to create static accessor, other controllers that i have are usually not created this way , i use fx:controller in fxml to specify what controller should be for which fxml , its usually handy to have mainController accessable.
So to change your views create in your main controller methods that are connected to your menu with whose you change views
#FXML
private void setViewPreferences() {
setView(Info.Resource.FXML_FILE_PREFERENCES);
}
#FXML
private void setViewProductPage() {
setView(Info.Resource.FXML_FILE_PRODUCT_PAGE);
}
Currently in dynamicNode is helper to see what exactly is the current selected, its
private String currentlyInDynamicPane;//not important
Here is setView
public void setView(String fxmlPath) {
dynamicNode.setContent(getView(fxmlPath));
currentlyInDynamicPane = fxmlPath;
}
public Node getView(String fxmlPath) {
try {
return new FXMLLoader(getClass().getResource(fxmlPath)).load();
} catch (IOException ex) {
ex.printStackTrace();
return null;
}
}
So when you click left menu you swap FXML files, you can make sure that you have some default FXML shown at the start or when nothing in menu is selected as well.
This is the way i do it, roughly.
So think about YOUR DASHBOARD as DynamicPane,
I have written a controller for two windows /stages.
The first window is opened in the MainClass. The second in the Controller, if the user clicks onto a button.
How can I get the TextFields from second.fxml in the applyFor()-method?
Thanks.
#FXML
protected void requestNewAccount(ActionEvent event) {
try {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("second.fxml")); // TextFields in there
Parent root = (Parent) fxmlLoader.load();
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setTitle("Second Window");
Scene scene = new Scene(root);
String css = MainOnlineCustomer.class.getResource("/style.css").toExternalForm();
scene.getStylesheets().clear();
scene.getStylesheets().add(css);
stage.setScene(scene);
stage.show();
} catch (IOException e) {
logger.error(e);
}
}
/**
* closes the "second"-Window
* #param event
*/
#FXML
protected void cancel(ActionEvent event) {
final Node source = (Node) event.getSource();
final Stage stage = (Stage) source.getScene().getWindow();
stage.close();
}
#FXML
protected void applyFor(ActionEvent event) {
// get values from TextField in second.fxml here!!!
}
It's not good to share controllers between fxmls unless they serve the same purpose. Here both fxml seem to serve a different purpose (account management, login or something similar for one of them and creating a new account for the other). What is even worse is that those classes do not share the same controller instance, which means the small (and probably only) benefit you could get from using the same controller, is not used here. You should better use different controllers.
Since you use Modality.APPLICATION_MODAL as modality, I'd recommend using showAndWait instead of show to open the new stage. This will enter a nested event loop, which allows the UI to remain responsive and continues after the invocation of showAndWait once the stage is closed.
Furthermore add a method to the controller of second.fxml that allows you to retrieve the result.
Example
This creates a Person object with given name and family name.
"primary window (opening the "inner" stage)
FXMLLoader loader = new FXMLLoader(getClass().getResource("second.fxml"));
Stage subStage = new Stage();
subStage.initModality(Modality.APPLICATION_MODAL);
subStage.setTitle("Second Window");
Scene scene = new Scene(loader.load());
subStage.setScene(scene);
subStage.showAndWait();
Optional<Person> result = loader.<Supplier<Optional<Person>>>getController().get();
if (result.isPresent()) {
// do something with the result
}
controller for "inner" content
public class SecondController implements Supplier<Optional<Person>> {
#FXML
private TextField givenName;
#FXML
private TextField familyName;
private boolean submitted = false;
// handler for submit action
#FXML
private void submit() {
submitted = true;
givenName.getScene().getWindow().hide();
}
// handler for cancel action
#FXML
private void cancel() {
givenName.getScene().getWindow().hide();
}
#Override
public Optional<Person> get() {
return submitted ? Optional.of(new Person(givenName.getText(), familyName.getText())) : Optional.empty();
}
}
Note that you can gain access to any data available to the controller this way. I wouldn't recommend accessing any nodes (like TextFields) directly though, since this makes changing the UI harder.
Using the Supplier interface here is not necessary, but I chose to do this to achieve a loose coupling between SecondController and the main window.
I would like to communicate with a FXML controller class at any time, to update information on the screen from the main application or other stages.
Is this possible? I havent found any way to do it.
Static functions could be a way, but they don't have access to the form's controls.
Any ideas?
You can get the controller from the FXMLLoader
FXMLLoader fxmlLoader = new FXMLLoader();
Pane p = fxmlLoader.load(getClass().getResource("foo.fxml").openStream());
FooController fooController = (FooController) fxmlLoader.getController();
store it in your main stage and provide getFooController() getter method.
From other classes or stages, every time when you need to refresh the loaded "foo.fxml" page, ask it from its controller:
getFooController().updatePage(strData);
updatePage() can be something like:
// ...
#FXML private Label lblData;
// ...
public void updatePage(String data){
lblData.setText(data);
}
// ...
in the FooController class.
This way other page users do not bother about page's internal structure like what and where Label lblData is.
Also look the https://stackoverflow.com/a/10718683/682495. In JavaFX 2.2 FXMLLoader is improved.
Just to help clarify the accepted answer and maybe save a bit of time for others that are new to JavaFX:
For a JavaFX FXML Application, NetBeans will auto-generate your start method in the main class as follows:
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
Now, all we need to do to have access to the controller class is to change the FXMLLoader load() method from the static implementation to an instantiated implementation and then we can use the instance's method to get the controller, like this:
//Static global variable for the controller (where MyController is the name of your controller class
static MyController myControllerHandle;
#Override
public void start(Stage stage) throws Exception {
//Set up instance instead of using static load() method
FXMLLoader loader = new FXMLLoader(getClass().getResource("FXMLDocument.fxml"));
Parent root = loader.load();
//Now we have access to getController() through the instance... don't forget the type cast
myControllerHandle = (MyController)loader.getController();
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
Another solution is to set the controller from your controller class, like so...
public class Controller implements javafx.fxml.Initializable {
#Override
public void initialize(URL location, ResourceBundle resources) {
// Implementing the Initializable interface means that this method
// will be called when the controller instance is created
App.setController(this);
}
}
This is the solution I prefer to use since the code is somewhat messy to create a fully functional FXMLLoader instance which properly handles local resources etc
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("/sample.fxml"));
}
versus
#Override
public void start(Stage stage) throws Exception {
URL location = getClass().getResource("/sample.fxml");
FXMLLoader loader = createFXMLLoader(location);
Parent root = loader.load(location.openStream());
}
public FXMLLoader createFXMLLoader(URL location) {
return new FXMLLoader(location, null, new JavaFXBuilderFactory(), null, Charset.forName(FXMLLoader.DEFAULT_CHARSET_NAME));
}
On the object's loading from the Main screen, one way to pass data that I have found and works is to use lookup and then set the data inside an invisible label that I can retrieve later from the controller class. Like this:
Parent root = FXMLLoader.load(me.getClass().getResource("Form.fxml"));
Label lblData = (Label) root.lookup("#lblData");
if (lblData!=null) lblData.setText(strData);
This works, but there must be a better way.
I would like to communicate with a FXML controller class at any time, to update information on the screen from the main application or other stages.
Is this possible? I havent found any way to do it.
Static functions could be a way, but they don't have access to the form's controls.
Any ideas?
You can get the controller from the FXMLLoader
FXMLLoader fxmlLoader = new FXMLLoader();
Pane p = fxmlLoader.load(getClass().getResource("foo.fxml").openStream());
FooController fooController = (FooController) fxmlLoader.getController();
store it in your main stage and provide getFooController() getter method.
From other classes or stages, every time when you need to refresh the loaded "foo.fxml" page, ask it from its controller:
getFooController().updatePage(strData);
updatePage() can be something like:
// ...
#FXML private Label lblData;
// ...
public void updatePage(String data){
lblData.setText(data);
}
// ...
in the FooController class.
This way other page users do not bother about page's internal structure like what and where Label lblData is.
Also look the https://stackoverflow.com/a/10718683/682495. In JavaFX 2.2 FXMLLoader is improved.
Just to help clarify the accepted answer and maybe save a bit of time for others that are new to JavaFX:
For a JavaFX FXML Application, NetBeans will auto-generate your start method in the main class as follows:
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("FXMLDocument.fxml"));
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
Now, all we need to do to have access to the controller class is to change the FXMLLoader load() method from the static implementation to an instantiated implementation and then we can use the instance's method to get the controller, like this:
//Static global variable for the controller (where MyController is the name of your controller class
static MyController myControllerHandle;
#Override
public void start(Stage stage) throws Exception {
//Set up instance instead of using static load() method
FXMLLoader loader = new FXMLLoader(getClass().getResource("FXMLDocument.fxml"));
Parent root = loader.load();
//Now we have access to getController() through the instance... don't forget the type cast
myControllerHandle = (MyController)loader.getController();
Scene scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
Another solution is to set the controller from your controller class, like so...
public class Controller implements javafx.fxml.Initializable {
#Override
public void initialize(URL location, ResourceBundle resources) {
// Implementing the Initializable interface means that this method
// will be called when the controller instance is created
App.setController(this);
}
}
This is the solution I prefer to use since the code is somewhat messy to create a fully functional FXMLLoader instance which properly handles local resources etc
#Override
public void start(Stage stage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("/sample.fxml"));
}
versus
#Override
public void start(Stage stage) throws Exception {
URL location = getClass().getResource("/sample.fxml");
FXMLLoader loader = createFXMLLoader(location);
Parent root = loader.load(location.openStream());
}
public FXMLLoader createFXMLLoader(URL location) {
return new FXMLLoader(location, null, new JavaFXBuilderFactory(), null, Charset.forName(FXMLLoader.DEFAULT_CHARSET_NAME));
}
On the object's loading from the Main screen, one way to pass data that I have found and works is to use lookup and then set the data inside an invisible label that I can retrieve later from the controller class. Like this:
Parent root = FXMLLoader.load(me.getClass().getResource("Form.fxml"));
Label lblData = (Label) root.lookup("#lblData");
if (lblData!=null) lblData.setText(strData);
This works, but there must be a better way.
So I made a FXML file for a little game I'm making for school and it has some buttons and labels in it, and it has it's own controller. Now I made a group of rectangles and want to add it to the same scene as the fxml file.
button.getParent().getChildren().add(group);
The code I wrote here doesn't work. Anybody an idea on how to add the group in the fxml file or just render it on the scene?
Rendering the fxml and the group in 2 diffrent scenes does work, so there are no errors.
EDIT:
Application class:
package retris;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
/**
*
* #author Arno Vandersmissen, Casper Vranken, Rani Vanhoudt
*/
public class Retris extends Application {
private Stage stage;
#Override
public void start(Stage stage) throws Exception {
this.stage = stage;
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("FXMLRetris.fxml"));
Parent root = loader.load();
FXMLRetrisController controller = loader.getController();
controller.playMusic();
stage.setOnCloseRequest(e -> {
e.consume();
FXMLConfirmController confirm= new FXMLConfirmController();
if(confirm.close("Close?")){
Platform.exit();
}
});
Scene scene = new Scene(root);
stage.setTitle("Retris");
stage.setScene(scene);
stage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
A Scene can only display ONE Parent at a time. Whatever you want to display in your GUI would be contained in that Parent. Assuming, as you suggested in the comments, that you want to update that parent at runtime, you need to have a reference to whatever child of the parent that should contain your group of rectangles.
let's say the root element of your fxml file is AnchorPane, and you also want to add the group of rectangles to that root. In your .fxml file you need a fx:id tag <AnchorPane fx:id="myRoot"> this allows you to inject the element to your controller class by use of the #FXML annotation.
public class MyController {
#FXML private AnchorPane myRoot;
#FXML private void createAndAddRectangles {
/**myRoot is already instantiated. you can simply add nodes to it at runtime
by using onAction="createAndAddRectangles" tag on a button in your .fxml file.**/
}
}