I'm currently teaching myself JavaFX, and I've taken a simple example program that's hardcoded the view and am turning it into one that uses FXML (mostly so I can use SceneBuilder for building UIs). Rather than writing a separate controller class, I'm using the application class (so there's 1 Java file and 1 FXML file). I'm not using an initialize() method as it's a linear flow (display the UI, populate the fields, wait for input). The view pops up, but then the app errors out as none of the controls are mapped to the appropriate variables (so for #FXML TableView<...> table, table is null).
However, I put in an initialize() method for debugging, the controls are injected while in initialize(), and then return to null when initialize() exits.
So the question is, does JavaFX instantiate a new instance of the application class as a separate controller class? This would explain why the variable are going out of scope. Or is it something else (e.g. the controls are injected only when being called back from JavaFX actions)?
The default behavior of the FXMLLoader is to create a new instance of the controller class and use that instance as the controller.
Specifically, the FXMLLoader does something like:
Read the root FXML element.
If the root FXML element has a fx:controller attribute, then
If a controller already exists, throw an exception, otherwise create an instance of the specified class1 and set that as the controller
Continue parsing the FXML file. If elements have a fx:id attribute, and a controller exists (by any mechanism), inject those fields into the controller. Similarly register event handlers as calls to methods in the controller instance.
Invoke initialize() on the controller, if a controller exists and it has such a method.
So, the question you asked:
Can application class be the controller class
Yes, but it's probably a terrible idea. If you simply specify the Application subclass as the controller class using fx:controller, then a second instance of the Application subclass is created, #FXML-annotated fields are injected on that second instance, and the initialize() method is invoked on that second instance. Obviously, the #FXML-fields are never initialized on the instance on which start(...) is invoked, and the initialize() method is never invoked on that instance.
The question you probably meant is:
Can the application class instance created at launch be used as the controller?
The answer to this is also yes, and, aside from very small demo programs you intend to immediately discard, it's also probably a very bad idea. You would do this by
public class MyApp extends Application {
#FXML
private Node someNode ;
public void initialize() {
// do something with someNode
}
#Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/fxml/file.fxml"));
loader.setController(this);
Parent root = loader.load();
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
}
Note that to use this code, your FXML file must not have a fx:controller attribute.
The problem with this is that you have no separation and no flexibility. (E.g. if you create a second instance of the view defined in your FXML file somewhere, you end up with a second Application subclass instance, which is at best counterintuitive (one application with two Application instances...).)
So I would advocate using a separate class for the controller in basically every case. The Application subclass should contain minimal code and should be used only for starting the application.
1 This step is actually a little more complex. If a class is specified in the fx:controller attribute, and no controller already exists, the FXMLLoader checks for a controllerFactory. If one exists, then the controller is set as the result of passing the specified Class to the controllerFactory's call() method, otherwise it is created by calling newInstance() on the specified class (effectively calling its no-argument constructor).
If you have defined your application class to be the controller in the FXML file, JavaFX will, if I remember correctly, create a new instance of your application class and use the new instance as a controller. Thus, your existing application class still has null for the table.
You can however define the controller programmatically in your application class to use your own instance:
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("example.fxml"));
fxmlLoader.setController(this);
Parent root = (Parent)fxmlLoader.load();
Related
I am currently quite puzzled by the behaviour of JavaFX with regards to nested FXMLs and associated Controllers and would greatly appreciate your help on this one;
This is how the code is structured:
Stage
--> Scene loaded from "Main" FXML with BorderPane as root Node and AnchorPane as 'center' Node
----> in center-AnchorPane, <fx:include> of nested "Login" FXML file (with VBox as top-Node and including a "Login" button)
------> Login controller, injected with top-Node of Login FXML file (a VBox in the Login FXML)
Then in the controller, here is what happens from my understanding:
Construction (nothing there)
FXML injection
initialize()
NOTHING?
event-driven method on login button-click
What I am trying to do:
Accessing the Login FXML's top-Node's Parent from the Login's FXML Controller via: injected_topNode_Login_VBox.getParent().blahblahblah
What doesn't work:
During the Login FXML nested controller's initialize() method, executing the above statement returns 'null'.
During the Login FXML nested controller's initialize() method, accessing the same Parent node, but from the top-down (I have a Scene-Handling class from which I can fetch the VBox's Parent node). The interesting part here is that at that time - even though it is accessed in a static way via SceneHandlerClassName.getRootNode() where getRootNode() is a static method - the Root Node element (of the outter FXML) is also returned as being 'null'..
What works:
During the Login FXML nested controller's event-driven method loginButtonClick(), executing the exact same statement returns the AnchorPane from the Main FXML scene, as expected.
In short:
Construction
FXML injection
initialize() --> Parent of Login VBox not reachable through VBox.getParent()
NOTHING?
event-driven method --> Parent of Login VBox reachable through VBox.getParent()
From my understanding, all properties should already be available at the initialize() method's runtime execution, and I don't see why getParent() would return null then...
Has someone got an answer for me, please?
Any help would be greatly appreciated! :) :)
Cheers,
Peter
PS: I did not enclose the original code as it is quite simple in nature really, and I think my problem is most probably about my conception of how JavaFX's initialize() method is working.
Edit1: Ok, I have a new finding after #Slaw 's comment:
I have tried, still in the initialize() method of the child FXML's controller, to access the parent of the top-level node in a different way (now documented in "What doesn't work").
I looked at the stackoverflow questiions that seem similar to my problem, but none were of any help.
Here is my problem:
For a project, I am making a JavaFX app that is in pure Java without FXML. I have two classes, a controller Controller class and a class containing gui stuff GUI.
Controller has a member variable of type GUI and I am trying to assign an event handler to one of the buttons in GUI but it doesn't seem to work. It only works when I try implementing the handler inside the GUI class, but I need it to work in Comtroller.
In the constructor of Controller is as follows:
this.view = view;
view.addSimpleHandler(new SimpleHandler());
view is of type GUI and addSimpleHandler is a member function of view
SimpleHandler is an inner class of Controller that implements EventHandler and overrides the handle() function
public void addSimpleHandler(EventHandler<ActionEvent> e) {
simpleButton.setOnAction(e);
}
here is my main method and class signature for GUI
public class GUI extends Application {
//member variables for the GUI design including simpleButton
private Button simpleButton;
public static void main(String[] args) {
GUI view = new GUI();
Controller controller = new Controller(view);
Application.launch(view.getClass(), args);
}
public GUI() {
simpleButton = new Button("Simple button");
//rest of code is setting up GUI into my panes
}
public void start(Stage primaryStage) throws Exception {
Scene scene = new Scene(mainPane, sceneWidth, sceneHeight);
//mainPane is a pane that contains simpleButton with a screenwidth and screenHeight
primaryStage.setTitle("Simple");
primaryStage.setScene(scene);
primaryStage.show();
}
Don't instantiate your application class manually. To understand why, see the documentation regarding the JavaFX life-cycle (emphasis mine):
The entry point for JavaFX applications is the Application class. The JavaFX runtime does the following, in order, whenever an application is launched:
Starts the JavaFX runtime, if not already started (see Platform.startup(Runnable) for more information)
Constructs an instance of the specified Application class
Calls the init() method
Calls the start(javafx.stage.Stage) method
Waits for the application to finish, which happens when either of the following occur:
the application calls Platform.exit()
the last window has been closed and the implicitExit attribute on Platform is true
Calls the stop() method
As you can see, JavaFX itself will instantiate the application class and it's that instance which has its life-cycle methods invoked—those methods being init(), start(Stage), and stop(). However, in your code you have the following:
public static void main(String[] args) {
GUI view = new GUI(); // created your own instance
Controller controller = new Controller(view); // gave controller that instance
// Launches JavaFX which starts the life-cycle documented above
Application.launch(view.getClass(), args);
}
You create your own instance of GUI. This instance is not managed by JavaFX which means its start(Stage) method is never invoked. When you create the Controller instance and pass it your instance of GUI you're adding the EventHandler to a node which is never displayed. The window you see displayed is from the GUI instance created as part of the call to Application#launch and that GUI instance is never associated with a Controller.
For JavaFX applications you should consider the init() and start(Stage) methods as the entry points1. In other words, create the Controller in one of those aforementioned life-cycle methods. Although I suppose you could do the same in the constructor instead2. Whichever you choose, take note of which thread invoke each method, which is documented in the same place as the life-cycle. Here are the essentials:
The application class is loaded, initialized, and constructed on the JavaFX Application Thread.
The init() method is invoked by the JavaFX-Launcher thread.
The start(Stage) and stop() methods are invoked by the JavaFX Application Thread.
Remember that certain actions can only be executed on the JavaFX Application Thread.
1. You can still execute code before the call to Application#launch within the main method if needed. The only constraint is that code should not be directly related to JavaFX.
2. Your constructor is currently public and has zero parameters—keep it that way. JavaFX requires the application class to have a public, no-argument constructor in order to construct an instance via reflection.
I'm a newcomer when it comes to JavaFX and I recently encountered a problem which really confuses me alot. I'm using a class called "MainController" which controlls an FXML-File containing a TabPane. Each tab is controlled by another controller. But there is one situation in which a tab needs to be deleted, so I need access to the MainController instance to remove the currently active tab from the pane.
Whenever I'm using this code to get an instance of the currently running MainController, I instead get a completely new instance with all of its components set to their default values.
The code is:
FXMLLoader loader = new FXMLLoader(getClass().getResource("Main.fxml"));
loader.load();
MainController controller = loader.getController();
controller.closeCurrentTab();
protected void closeCurrentTab() {
tabPane.getTabs().remove(tabPane.getSelectionModel().getSelectedIndex());
}
I'm currently using a static reference to the controller to access it since it is the only solution that works for me. But I know that this is highly unprofessional and I really want to avoid that.
I hope somebody knows what's wrong here.
You should ensure that you have a reference for your main controller at the point where you want to use it. I guess it is one of the "child" controllers (most probably the controller of the current tab).
Therefore if you would have a property in this class that stores the reference for your main controller, your problem would be solved.
I guess you initialize this "child" controller from the main controller like:
FXMLLoader loader = new FXMLLoader(getClass().getResource("TabController1.fxml"));
loader.load();
So here you could do:
TabController controller = loader.getController();
controller.mainControllerProperty.set(this);
Where mainControllerProperty is defined in TabController like:
ObjectProperty<MainController> mainControllerProperty = new SimpleObjectProperty();
I've searched several times here for answer but didn't get my solution.
In my case:
I want to take input from user and check validity. If everything is fine I will grab users ID from database and send that ID to another FXML and then run a select query there using that ID and display the results into a tableView.
In 2nd FXML (controller) I am using initialize() method to set data into tableView and a setId() method to receive user ID from previous FXML. But, initialize() method get called before setId() method and doesn't provide my required result as the ID is null.
Used Passing Parameters JavaFX FXML this method form passing data between FXML.
What will be the best solution for this?
FYI: Currently I'm using an extra class with static variable to store ID.
You could use a controller factory that initializes the id before it returns the controller instance:
FXMLLoader loader = new FXMLLoader(url);
loader.setControllerFactory(c -> {
MyController controller = new MyController();
controller.setId(userId);
return controller;
});
...
Node n = loader.load();
This way you could also use classes as controllers, that don't provide a default constructor. A more complex controller factory could be used to connect model and presenter (see MVP).
An alternative would be to modify the scene's contents in the setId method instead of the initialize method, which would be simpler than using a controller factory.
What the best solution is depends on your needs and personal preference. However, using a static member to pass data should be avoided, if possible.
I have a JavaFX control that is basically an amalgamation of several other JavaFX controls.
I want it such that the .jar file can be imported into Scene Builder so that it can be used like any other control. The closest analogy I can think of is when you make a custom control in C# and use it several times throughout several projects.
When I try to import the FXML file, it doesn't work. The control isn't treated as a single entity, and instead is basically just all of it's parts strung out in the FXML file.
What do I need to do with the FXML file, or the controller.java file so that the Scene Builder will be able to import the .jar, see the control(s), and allow me to import and use each custom control as a single entity? I've looked several places and even asked on Stack Overflow once before (though the answer I got was not the one for which I was looking, and have received no responses since), but nothing I've seen comes close to handling my issue.
The closest I've come has to do with this line in the FXML file:
<?scenebuilder-classpath-element /path/to/something?>
but I don't know what goes in /path/to/something
I know I can, in the initialization, simply add the control to the scene, but that is sub-optimal and something which I am desperately trying to avoid.
I was finally able to resolve the issue. After much trial and error and following the sample code that came from here, I discovered my problem was that I needed 2 classes for each FXML control group.
One to be the actual controller of the group, and another to be the object that would house the controller. I followed the code in the Unlock example and it was a godsend for helping me.
Basically it comes down to two files:
The object (which extends the type of the root node, in my case):
public class <InsertObjectClassNameHere> extends <RootContainerTypeHere>{
}
After that you need the controller class. This is with what I am most familiar, however I was still doing it wrong. This is what needs to implement initializable:
public class <InsertControllerClassNameHere> implements Initializable{
}
So for me the Object class looks like this:
public class DGCSDefiner extends GridPane {
private final DGCSDefinerController Controller;
public DGCSDefiner(){
this.Controller = this.Load();
}
private DGCSDefinerController Load(){
final FXMLLoader loader = new FXMLLoader();
loader.setRoot(this);
loader.setClassLoader(this.getClass().getClassLoader());
loader.setLocation(this.getClass().getResource("DGCSDefiner.fxml"));
try{
final Object root = loader.load();
assert root == this;
} catch(IOException ex){
throw new IllegalStateException(ex);
}
final DGCSDefinerController cntrlr = loader.getController();
assert cntrlr != null;
return cntrlr;
}
/**
* Get the settings defined by the controller.
* #return controller defined settings.
*/
public ColorSettings getColorSettings(){
return this.Controller.getColorSettings();
}
/**
* Set the controllers color settings.
* #param CS Defined color settings.
*/
public void setColorSettings(ColorSettings CS){
this.Controller.setColorSettings(CS);
}
}
and then there is the actual Controller class.
So for a straight-forward answer,
you need to have a class that will be loading your controller, and you need to pass down from your controller to that class that with which you will be working (Or, you can simply keep the controller public and access it directly).