I am trying to make basic desktop app using Kotlin and JavaFX(TornadoFX), but I am stuck on setting up controller for FXML file.
I am following guide from edvin on git LINK to git.
The program should increment number in label whenewer is the button clicked.
The problem is..
When I try to use FXML file shown below, I can't use onAction="#increment" to "connect" that file with a controller.
This will compile, but there is no way to call increment function from controller.kt file. Also there is error saying "No controller specified for top level element" ..
Whenever I try to specify the controller by using fx:controller=view.Controller the code will not even compile, showing error:
javafx.fxml.LoadException: Controller value already specified.
Could someone please help me out?
Here is FXML file:
?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<BorderPane xmlns="http://javafx.com/javafx/null" xmlns:fx="http://javafx.com/fxml/1">
<padding>
<Insets top="20" right="20" bottom="20" left="20"/>
</padding>
<center>
<VBox alignment="CENTER" spacing="10">
<Label text="0">
<font>
<Font size="20"/>
</font>
</Label>
<Button text="Click to increment" onAction="#increment"/>
</VBox>
</center>
</BorderPane>
Here is MyApp.kt (main file):
package app
import javafx.stage.Stage
import tornadofx.*
import view.MainView
class MyApp: App(MainView::class){
override val primaryView = MainView::class
override fun start(stage: Stage) {
stage.minHeight = 400.0
stage.minWidth = 600.0
super.start(stage)
}
}
Here is MainView.kt (view):
package view
import javafx.scene.layout.BorderPane
import tornadofx.*
class MainView : View() {
override val root: BorderPane by fxml("/views/MainViewFXML.fxml" )
}
Here is Controller.kt (this should be used to control FXML file actions):
package view
class Controller {
fun increment(){
//code
}
}
In TornadoFX, the View IS the controller. Think of View subclasses as the View Controller. React to UI events in the View, and pass business logic off to a ViewModel or a Controller subclass.
Place your increment function in MainView and it will be called :) Remove the fx:controller=view.Controller attribute.
Related
This question already has an answer here:
JavaFX, Label null pointer exception
(1 answer)
Closed 1 year ago.
I'm working on a Game project with JavaFX and a MVC Architecture
Here are one of my views
package fr.arnoux23u.javano.mvc.views;
import fr.arnoux23u.javano.mvc.*;
import javafx.fxml.*;
import javafx.scene.control.TextArea;
import java.net.URL;
import java.util.ResourceBundle;
/**
* #author arnoux23u
*/
public class ServerView implements Initializable, Observer {
#FXML
private TextArea clientsList;
#Override
public synchronized void update(Model m) {
System.out.println("[UPDATE] "+Thread.currentThread().getName());
clientsList.setText("test");
}
#Override
public void initialize(URL url, ResourceBundle resourceBundle) {
System.out.println("[INITIALIZE] "+Thread.currentThread().getName());
clientsList.setText("Aucun joueur ici");
}
}
And my FXML File
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<VBox alignment="CENTER" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fr.arnoux23u.javano.mvc.views.ServerView">
<Label text="JavaNo - Server">
<font>
<Font size="23.0" />
</font>
</Label>
<Label alignment="CENTER" text="Connected Clients">
<font>
<Font size="23.0" />
</font>
</Label>
<TextArea fx:id="clientsList" editable="false" maxWidth="200.0" prefHeight="200.0" prefWidth="200.0" style="-fx-focus-color: -fx-control-inner-background;" text="
">
<VBox.margin>
<Insets top="20.0" />
</VBox.margin>
<font>
<Font size="18.0" />
</font></TextArea>
</VBox>
When I load the app, the text of the TextArea clientsList is correctly set to "Aucun joueur ici" but when I want to update the text (with MVC), I've a NullPointerException on clientsList
First, I thought it was a Thread Error because the Thread who runs the update method was not the "JavaFX Application Thread"
So in my MVC Controller, I use the Platform.runlater() method.
#Override
public void notifyObservers() {
observers.forEach(o -> {
System.out.println("[FOREACH] "+Thread.currentThread().getName());
Platform.runLater(() -> {
System.out.println("[PLATFORM] "+Thread.currentThread().getName());
o.update(this);
});
});
}
The output when I call the update is
[INITIALIZE] JavaFX Application Thread
[FOREACH] Thread-3
[PLATFORM] JavaFX Application Thread
[UPDATE] JavaFX Application Thread
But even with the runLater method, I've a NullPointerException
Here is the complete stack trace
Exception in thread "JavaFX Application Thread" java.lang.NullPointerException: Cannot invoke "javafx.scene.control.TextArea.setText(String)" because "this.clientsList" is null
at fr.arnoux23u.javano/fr.arnoux23u.javano.mvc.views.ServerView.update(ServerView.java:34)
at fr.arnoux23u.javano/fr.arnoux23u.javano.mvc.Game.lambda$notifyObservers$1(Game.java:54)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(PlatformImpl.java:457)
at java.base/java.security.AccessController.doPrivileged(AccessController.java:399)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$11(PlatformImpl.java:456)
at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:96)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(WinApplication.java:184)
at java.base/java.lang.Thread.run(Thread.java:833)
I hope someone can help me.
Thank's
See step 3 of this description of FXMLoader operation:
If there is a fx:controller attribute on the root element, the FXMLLoader creates an instance of the specified class.
So, if you create a new instance explicitly in your code, and the loader also creates a new instance, then only the loader instance will have fxml values injected.
I guess, with a high degree of certainty, that is what is happening with your code.
I make a program using FXML. In FXML I create 4 TextFields and Button. My problem is how send parameters from TextFields in FXML to Controller, when I click this button.
I'm make JavaFX application with FXML. I connect this application to database. I searched for the previous week unsuccessfully, but I found nothing special that can help me.
<children>
<BorderPane prefHeight="30.0" prefWidth="700.0">
<bottom>
<AnchorPane>
<children>
<Button text="Add car" fx:id="button" onAction="#AddCar"/>
</children>
</AnchorPane>
</bottom>
</BorderPane>
<TextField promptText="Brand" id="carBrand"/>
<TextField promptText="Model" id="carModel"/>
<TextField promptText="Mileage" id="carMileage"/>
</children>
I expect that I click Button in FXML - Java controller get parameters from FXML and write to the console. Actual I know how to write in console, but my problem is that I don't know how to get brand, model and mileage from FXML.
You need to inject your TextFields into your controller, then query their text properties. To do this, specify an fx:id for each element to be injected and add a field to your controller class with the same type and name. If the field is not public then you need to annotate it with #FXML.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.VBox?>
<VBox xmlns="http://javafx.com/javafx/12.0.1" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.example.Controller" spacing="10" alignment="center">
<TextField fx:id="carBrand" promptText="Brand"/>
<TextField fx:id="carModel" promptText="Model"/>
<TextField fx:id="carMileage" promptText="Mileage"/>
<Button text="Add car" onAction="#addCar"/>
</VBox>
package com.example;
import javafx.fxml.FXML;
import javafx.event.ActionEvent;
import javafx.scene.control.TextField;
public class Controller {
#FXML private TextField carBrand;
#FXML private TextField carModel;
#FXML private TextField carMileage;
#FXML
private void addCar(ActionEvent event) {
event.consume();
String brand = carBrand.getText();
String model = carModel.getText();
String mileage = carMileage.getText();
// do something with values...
}
}
Note: Following Java naming conventions, method names use camelCase. In other words, the button's action method's name should be addCar (like above) rather than AddCar.
I am trying to create a simple button, but I am getting this stupid error and it doesn't make any sense.
Here is my Admin Scene FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.Separator?>
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.text.Font?>
<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="437.0" prefWidth="582.0" stylesheets="#application.css" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="appController.AdminSceneController">
<children>
<Separator layoutX="-14.0" layoutY="101.0" prefHeight="10.0" prefWidth="601.0" />
<Label layoutX="14.0" layoutY="29.0" text="admin panel">
<font>
<Font name="Book Antiqua" size="28.0" />
</font>
</Label>
<Button layoutX="174.0" layoutY="32.0" mnemonicParsing="false" style="-fx-background-radius: 100px;" text="+" textFill="#369033" />
<Button fx:id="logoutButton" layoutX="14.0" layoutY="65.0" mnemonicParsing="false" onAction="#logout" prefHeight="3.0" prefWidth="81.0" styleClass="logout" stylesheets="#application.css" text="(logout)" textFill="#070707" />
<Button layoutX="387.0" layoutY="392.0" mnemonicParsing="false" prefHeight="31.0" prefWidth="158.0" text="Delete" />
<ListView layoutX="223.0" layoutY="106.0" prefHeight="327.0" prefWidth="128.0" />
</children>
</Pane>
And this is my AdminSceneController.java
package appController;
import appDesign.PhotoAlbum;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Button;
public class AdminSceneController {
public class MainSceneController {
#FXML
Button logoutButton;
#FXML
public void logout(ActionEvent event) throws Exception {
PhotoAlbum.primaryStage.show();
((Node)(event.getSource())).getScene().getWindow().hide();
}
}
}
I get a warning from Eclipse saying:
The controller 'AdminSceneController' has no event slot 'logout'
And when I run the program, I get the error:
javafx.fxml.LoadException: Error resolving onAction='#logout', either the event handler is not in the Namespace or there is an error in the script.
/C:/Users/Peter/Documents/GitHub/PhotoAlbum40/bin/appDesign/AdminPanelScene.fxml:19
Can anybody help?
Your AdminSceneController does not have a logout method, your class MainSceneController does though.
Remove the line
public class MainSceneController {
and the closing }and it should work.
You have to understand, that an inner class (MainSceneController) is not the same class as the enclosing class (AdminSceneController). By using fx:controller="appController.AdminSceneController" in the fxml a instance of AdminSceneController is created. This class however does not contain a single method or field. This causes the error.
Furthermore note that the FXMLLoader does not allow you to create non-static inner classes. If you want the FXMLLoader to create your controller instance, you have to make MainSceneController static and use fx:controller="appController.AdminSceneController$MainSceneController".
Ways around this would be specifying a controllerFactory or creating the controller instance yourself:
FXMLLoader loader = new FXMLLoader(getClass().getResource(...));
AdminSceneController enclosingInstance = new AdminSceneController(); // or any other way to get your hands on a instance of the enclosing class
// specify controller instance used yourself
loader.setController(enclosingInstance.new MainSceneController());
...
loader.load()
Which requires you to remove the fx:controller attribute from the fxml.
Of course you could also simply move the field / method to a top level class...
I have a custom JavaFx control that renders in my application. But, I can't get SceneBuilder to understand it.
I have CustomTextField.java / CustomTextField.fxml. CustomTextField inherits from UserControl, as defined here, but my scene builder problem happens with any custom control that I create.
First, I had to change my import statement to be a wildcard. From
<import sample.CustomTextField>
to
<import sample.*>
otherwise, scenebuilder threw an exception / showed a stacktrace indicating that the source file couldn't be found. I have no idea why this was necessary, but it seemed to work, so I kept moving.
I had read that you to specify a scenebuilder-classpath-element in the fxml file as well. So, i tried every combination I could think of:
<?scenebuilder-classpath-element ../../bin?>
<?scenebuilder-classpath-element ../../out?>
<?scenebuilder-classpath-element ../../../out?>
<?scenebuilder-classpath-element ./?>
<?scenebuilder-classpath-element ../../../../../target/classes?>
My issue is that the custom control does not render in Scene Builder. SElecting it in the hiearchy tree, it indicates "Selection contains unresolved reference". If I can't drag/drop the custom control around, that's acceptable. However, I really want to render this in Scene Builder and lay out other stuff.
I am using IntelliJ IDEA 14, and Scene Builder 2.0
sample.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import sample.*?>
<?scenebuilder-classpath-element ../../bin?>
<?scenebuilder-classpath-element ../../out?>
<?scenebuilder-classpath-element ../../../out?>
<?scenebuilder-classpath-element ./?>
<?scenebuilder-classpath-element ../../../../../target/classes?>
<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<children>
<Button fx:id="topButton" layoutY="2.0" mnemonicParsing="false" text="Button Top" />
<CustomTextField fx:id="myCustomTextField" layoutX="1.0" layoutY="40.0" />
<Button layoutX="1.0" layoutY="125.0" mnemonicParsing="false" text="Button Bot" />
</children>
</Pane>
I'm able to load custom classes. We got it to work by building a .jar with the custom classes, then in SceneBuilder, go to the settings icon next to searching the library and import custom library. Then find your .jar. Maybe you could do the same thing with .class files?
Here's the import statement in the .fxml:
<?import com.rlsciences.autoredact.view.ItemsTableView?>
Here's where I'm using it in the .fxml:
<ItemsTableView id="searchResultsTableView" fx:id="searchResultsTableView" fixedCellSize="20.0" styleClass="compact-table" VBox.vgrow="ALWAYS">
Here's the class definition:
package com.rlsciences.autoredact.view;
public class ItemsTableView<S> extends TableView<S>
I have the following .fxml file:
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="javafx.com/fxml">
<Pane VBox.vgrow="ALWAYS">
<!-- ... -->
</Pane>
</fx:root>
However, there is an error at VBox.vgrow="ALWAYS", since <fx:root ... is not exactly a VBox. How can I do this in FXML (no Java)?
Edit: The error in my IDE shows "Attribute VBox.vgrow is not allowed here", and the error the Java application gives is "VBox.vgrow is not a valid attribute."
I had neglected to display all my imports of the .fxml file (which only had javafx.scene.layout.Pane).
In order for that specific .fxml file to not give an error (see edit), the import javafx.scene.layout.VBox also had to be added, as VBox.* cannot be used on any element unless VBox is imported.
The correct .fxml file is:
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.layout.VBox?>
<fx:root type="javafx.scene.layout.VBox" xmlns:fx="javafx.com/fxml">
<Pane VBox.vgrow="ALWAYS">
<!-- ... -->
</Pane>
</fx:root>