How to reuse JavaFX GUI? Can I change controllers dynamically? - java

I have designed a scene using JavaFX Scene Builder/FXML and I want to create many instances of that scene, but each scene with different behavior. Is there a way to change the controller of a scene/FXML dynamically?
What I want is to design one scene and reuse it, but with different behaviors for each instance.
Currently I am loading the FXML and its controller like this:
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxmlFile));
Parent root = (Parent) fxmlLoader.load();
Scene scene = new Scene(root);
Controller controller = fxmlLoader.getController();

There is the possibility to use a custom Controllerfactory with the FXML loader.
The setControllerFactory method expects a Callback type, which is an interface with only one callable function: call which the factory uses. The function (called by the FXMLLoader class) expects to get a Class object as an input parameter and provides an instantiated object based on the type. Above Java8 lambdas can be used to provide the factory:
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxmlFile));
loader.setControllerFactory((Class<?> controllerType) ->{
if(controllerType == Controller.class){
return new Controller();
}
});
Parent root = (Parent) fxmlLoader.load();
The argument controllerType in the above code is the the type provided by the fxml fx:controller attribute, which is determined by the Java Class loader. When the Controllerfactory is called, nothing is instantiated yet, that's why even abstract classes can be given in the fxml. To achieve different behavior, inheritance can be used.
An example would be:
class Controller{...}
class FirstController extends Controller{...}
class SecondController extends Controller{...}
And the factory can be used like so:
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxmlFile));
final int[] instantiatedClasses = {0};
loader.setControllerFactory((Class<?> controllerType) ->{
if(controllerType == Controller.class){
if(0 == instantiatedClasses[0]){
++instantiatedClasses[0];
return new FirstController();
} else return new SecondController();
}
});
Parent root = (Parent) fxmlLoader.load();
Please Note, that this way different arguments can also be supplied to the controller, so inheritance might be an overkill.
For example the primaryStage can be provided to the controller, e.g. for eliminating the need for different setters in the controllers.
class Controller{
public Controller(int behaviorParam){...}
}
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource(fxmlFile));
final int[] instantiatedClasses = {-1}; /* so it would start at 0 */
loader.setControllerFactory((Class<?> controllerType) ->{
if(controllerType == Controller.class){
++instantiatedClasses[0];
return new Controller(instantiatedClasses[0]);
}
});
Parent root = (Parent) fxmlLoader.load();
The challenges of this method, however is that it is still not straightforward to differentiate between the different fxml instances of the same controller type. Counting the number of instantiated classes is one way that works, but it doesn't give much control, compared to anything identifier based.

Related

Passing an object or String to controller before initialiation

I have a class that contains information based on which the title text in the next scene will be modified. So I need to pass the Object or at least a String to the new scene, using which I'll update the text in the next scene during Initialization.
void sceneSwitch(Event event, String fxmlName, Class child){
String fxmlPath = "gui/resources/fxml/";
Node node = (Node) event.getSource();
Stage stage = (Stage) node.getScene().getWindow();
FXMLLoader sceneLoader = fxmlLoadErrorHandler(fxmlPath + fxmlName, child); //This sets up the FXMLLoader
Parent newScene = fxmlLoadErrorHandler(sceneLoader);
/* The last line above is basically the .load() function, but this already
launches the Initialize function, in which I need to use 'session' Object
which I only yet set below via setActive */
CoreController controller = sceneLoader.getController();
controller.setActive(session);
Scene scene = null;
if(newScene != null) {
scene = new Scene(newScene);
}
stage.setScene(scene);
stage.show();
}
So is there any way for me to do what I'm trying to here? It would be sufficient to just send a String to it. I thought of defining a function in the CoreController that I'll run as soon as I set Session before the scene shows, but CoreController is an abstract Class and I don't have access to modifying the objects in the subclass.
Answer by Slaw in a comment in response to the question. Thanks! I feel embarassed I didn't think of this.
"You could expose a method in the abstract class that sub-classes would override; the overrides would call the super implementation as well as do their own thing. Another option is to not use fx:controller and instead call FXMLLoader.setController before loading. A third option is to use a controller factory."

How to make Combobox retain its selected value in JavaFx while calling from another class? [duplicate]

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.

call methods inside controller from a different class javafx [duplicate]

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.

TableView.setItems throw null Exception in java

im trying to add some items to my View Table . im creating a ObservableList and pass it to the setItem method but it throws null Exception .
Here is the latest attempt :
javafx.scene.control.TableView table = controller.friendsList;
/*
the controller.friendList is defined as Below :
#FXML
public javafx.scene.control.TableView friendsList;
*/
ObservableList friendData = FXCollections.observableArrayList();
for(Client friend : friends )
{
friendData.add(friend.getUsername());
}
//The friendData is fine ( according to the debugging ) it includes 2 Strings
table.setItems(friendData);
table.setEditable(false);
table.setVisible(false);
table.setVisible(true);
}
whats wrong with the code ?
EDIT:
now i get the controller from the FXMLLoader as the answere says . but it's still the same . all the field with the #FXML anotation are still null .
here is how i get the controller :
public userSceneController controller;
FXMLLoader fxmlLoader = new FXMLLoader();
Parent root = fxmlLoader.load(getClass().getResource("userScene.fxml"));
controller = (userSceneController) fxmlLoader.getController();
Since your TableView is a #FXML-annotated field in the controller, it is initialized in the controller instance created by the FXMLLoader. In other words, presumably at some point in the code you didn't actually show, you create an FXMLLoader and call load() on it. As part of that load process, the FXMLLoader creates an instance of the controller and initializes the #FXML-annotated fields with references to the controls displayed in the UI.
You are presumably intending to configure the table view that is displayed in the UI; in order to do that, you need to reference the controller that the FXMLLoader created for you.
However, what you actually do in your code is to create a new controller instance:
Controller controller = new Controller();
Obviously since this in not the controller created as part of the FXMLLoader.load() process, it never had its friendsList field initialized.
Instead, you need to retrieve the controller instance from your FXML loader. In order to do this, you must create an FXMLLoader with a location properly set, and then use the instance method FXMLLoader.load(), not the static method FXMLLoader.load(URL):
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("userScene.fxml"));
Parent root = (Parent) fxmlLoader.load();
controller = (userSceneController) fxmlLoader.getController();

JavaFX class controller scene reference

Is there any way of getting the Scene object of an FXML loaded file from the associated class controller.
I'm doing something like this:
#FXML
private AnchorPane anchor;
Scene scene = anchor.getScene();
but i'd like a solution that does not reference the AnchorPane control.
Why not? Controller is an abstract class, he's not aware about UI unless you deliberately make him know.
Nodes (inlcuding AnchorPane) are another story, they hardly exists outside for scenegraph. So it's perfectly fine to ask Node about his parent or scene.
If you still want to handle that separately there are next approaches:
you can create a custom controller and set scene after loader. Just note that at the time initialize() called it wouldn't yet initialized.
public class MyController {
private void Scene scene;
public void setScene(Scene scene) { this.scene = scene; }
}
// loading code
FXMLLoader fxmlLoader = new FXMLLoader();
AnchorPane root = (AnchorPane) fxmlLoader.load(getClass().getResource("MyApp.fxml"));
MyController myController = (MyController) fxmlLoader.getController();
myController.setScene(scene);
You can create a custom fxml control which will incorporate controller and he can just call getScene() for itself. See an example here: https://stackoverflow.com/a/10718683/1054140
I tried your answer, but it did not work, I found the reason here:
JavaFX: How to get stage from controller during initialization?
after the comment:
// loading code
don't use the static load method
AnchorPane root=(AnchorPane) FXMLLoader.load(getClass().getResource("MyApp.fxml"));
but instead use instantiated loader's method
AnchorPane root=(AnchorPane) fxmlLoaded.load(getClass().getResource("MyApp.fxml"));

Categories