I'm currently trying to implements a very basic application with JavaFX just to do some tests. The final goal here is to implement an interface separated in severals parts, and each part will have its own .fxm and controller.
For the beginning, I've tried to develop a basic application with this architecture :
I have a main VueGlobale.fxml file which include another .fxml file clavier.fxml :
VueGlobale.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="529.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
<children>
<fx:include source="clavier.fxml" fx:id="clavier" />
</children>
</VBox>
clavier.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1" fx:controller="controllers.ClavierController">
<children>
<Button layoutX="274.0" layoutY="188.0" mnemonicParsing="false" onAction="#ajouterDo" text="Button" />
</children>
</AnchorPane>
An here's the ClavierController.fxml : package controllers;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import model.Model;
public class ClavierController {
private Model model;
public ClavierController(Model m) {
this.model = m;
}
@FXML
public void ajouterDo(ActionEvent e){
System.out.println("Click !");
this.model.doSomething();
}
}
Model package model; public class Model {
public void doSomething() {
System.out.println("Model !");
}
}
Main
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("views/VueGlobale.fxml"));
Parent root = loader.load();
primaryStage.setTitle("Test");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The problem is : I don't know what to do in my main so that I can give to my clavier.fxml a ClavierController(Model m). (It's working with a controller without parameters, but what if I need to precise parameters ?)
Here's the StackTrace if it could help you :
Exception in Application start method
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at com.sun.javafx.application.LauncherImpl.launchApplicationWithArgs(LauncherImpl.java:389)
at com.sun.javafx.application.LauncherImpl.launchApplication(LauncherImpl.java:328)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
at sun.launcher.LauncherHelper$FXHelper.main(Unknown Source)
Caused by: java.lang.RuntimeException: Exception in Application start method
at com.sun.javafx.application.LauncherImpl.launchApplication1(LauncherImpl.java:917)
at com.sun.javafx.application.LauncherImpl.lambda$launchApplication$155(LauncherImpl.java:182)
at java.lang.Thread.run(Unknown Source)
Caused by: javafx.fxml.LoadException:
/J:/Programming/Telecom%20Nancy/S3/Test/bin/views/clavier.fxml:6
/J:/Programming/Telecom%20Nancy/S3/Test/bin/views/VueGlobale.fxml:9
at javafx.fxml.FXMLLoader.constructLoadException(FXMLLoader.java:2601)
at javafx.fxml.FXMLLoader.access$700(FXMLLoader.java:103)
at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:932)
at javafx.fxml.FXMLLoader$InstanceDeclarationElement.processAttribute(FXMLLoader.java:971)
at javafx.fxml.FXMLLoader$Element.processStartElement(FXMLLoader.java:220)
at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:744)
at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2707)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2527)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2441)
at javafx.fxml.FXMLLoader.access$2700(FXMLLoader.java:103)
at javafx.fxml.FXMLLoader$IncludeElement.constructValue(FXMLLoader.java:1143)
at javafx.fxml.FXMLLoader$ValueElement.processStartElement(FXMLLoader.java:746)
at javafx.fxml.FXMLLoader.processStartElement(FXMLLoader.java:2707)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2527)
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:2441)
at javafx.fxml.FXMLLoader.load(FXMLLoader.java:2409)
at Main.start(Main.java:14)
at com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$162(LauncherImpl.java:863)
at com.sun.javafx.application.PlatformImpl.lambda$runAndWait$175(PlatformImpl.java:326)
at com.sun.javafx.application.PlatformImpl.lambda$null$173(PlatformImpl.java:295)
at java.security.AccessController.doPrivileged(Native Method)
at com.sun.javafx.application.PlatformImpl.lambda$runLater$174(PlatformImpl.java:294)
at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at com.sun.glass.ui.win.WinApplication.lambda$null$148(WinApplication.java:191)
... 1 more
Caused by: java.lang.InstantiationException: controllers.ClavierController
at java.lang.Class.newInstance(Unknown Source)
at sun.reflect.misc.ReflectUtil.newInstance(Unknown Source)
at javafx.fxml.FXMLLoader$ValueElement.processAttribute(FXMLLoader.java:927)
... 23 more
Caused by: java.lang.NoSuchMethodException: controllers.ClavierController.<init>()
at java.lang.Class.getConstructor0(Unknown Source)
... 26 more
Exception running application Main
Thank you in advance for your help and for your time, have a good day.
EDIT :
By including several fxml, i mean this :
VueGlobale.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="529.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
<children>
<fx:include source="viewtop.fxml" fx:id="top" />
</children>
<children>
<fx:include source="clavier.fxml" fx:id="clavier" />
</children>
<children>
<fx:include source="viewbottom.fxml" fx:id="bottom" />
</children>
</VBox>
So I have three .fxml file included in my VueGlobale.fxml. Let us supposed that all this .fxml have their own controller (ClavierController.java, TopController.java, BottomController.java). All these controllers needs the model. What should I do in my main factory ? Something like this doesn't work :
import controllers.ClavierController;
import controllers.TopController;
import controllers.BottomController;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import model.Model;
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Model m = new Model();
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("views/VueGlobale.fxml"));
loader.setControllerFactory(ic -> new ClavierController(m));
loader.setControllerFactory(ic -> new TopController(m));
loader.setControllerFactory(ic -> new BottomController(m));
Parent root = loader.load();
primaryStage.setTitle("Test");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
The controllerFactory
is a property like any other property. If you set its value, it has that value, so it makes no sense to set its value to three different things in three consecutive lines of code.
You can create a controller factory that simply checks the parameter and returns the appropriate controller:
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception{
Model m = new Model();
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("views/VueGlobale.fxml"));
loader.setControllerFactory(ic -> {
if (ic == ClavierController.class) {
return new ClavierController(m);
} else if (ic == TopController.class) {
return new TopController(m);
} else if (ic == BottomController.class) {
return new BottomController(m) ;
}
throw new IllegalArgumentException("Unexpected controller type: "+ic.getName());
});
Parent root = loader.load();
primaryStage.setTitle("Test");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You can also use reflection to implement logic like "If the supplied controller class has a constructor taking a model, call that constructor and pass the model in, otherwise call the default constructor":
loader.setControllerFactory(ic -> {
try {
for (Constructor<?> c : ic.getConstructors()) {
if (c.getParameterCount() == 1 && c.getParameterTypes()[0]==Model.class) {
return c.newInstance(m);
}
}
return ic.newInstance();
} catch (Exception e) {
// fatal...
throw new RuntimeException(e);
}
});
This is all a bit heavy-handed for what you are trying to achieve, though. You are merely trying to pass a model to the nested controllers (the controllers for the FXML files loaded via <fx:include>
). The documentation explicitly provides an injection-based approach for this.
Specifically, if your "main" FXML adds fx:id
s to the <fx:include>
s, then the controllers can be injected into the main controller. So you can do something like:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox
maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="529.0" xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1">
<children>
<fx:include source="viewtop.fxml" fx:id="top" />
</children>
<children>
<fx:include source="clavier.fxml" fx:id="clavier" />
</children>
<children>
<fx:include source="viewbottom.fxml" fx:id="bottom" />
</children>
</VBox>
Now define a "main" controller for this FXML file:
public class MainController {
@FXML
private Parent top ;
@FXML
private Parent clavier ;
@FXML
private Parent bottom ;
@FXML
private TopController topController ;
@FXML
private ClavierController clavierController ;
@FXML
private BottomController bottomController ;
private final Model model ;
public MainController(Model model) {
this.model = model ;
}
@FXML
public void initialize() {
topController.setModel(model);
clavierController.setModel(model);
bottomController.setModel(model);
}
// ...
}
Just define the "nested" controllers with the appropriate methods for setting the model:
public class TopController {
private Model model ;
public void setModel(Model model) {
this.model = model ;
}
@FXML
private void someHandlerMethod(ActionEvent event) {
model.doSomething();
}
}
And finally load your main fxml with:
Model model = new Model();
FXMLLoader loader = new FXMLLoader(getClass().getResource("/path/to/fxml"));
loader.setController(new MainController(model));
Parent root = loader.load();
and it should all be good to go.