I currently have an application that utilizes multiple FXML controllers. In my root controller (CustomController.java) I have loaded an FXML file that calls MainMenuController. I have another controller/fxml-file that is another "view" of the application.
My question is: What would be the best approach to changing the current "view"? My current method is calling the CustomController.swapOut() when a button is clicked in the mainmenu.fxml. But this is causing an error, as it is creating a MainMenuController object and gets stuck in an infinite loop until memory runs out.
Note: I am currently trying to clear the viewableContent pane first, before attempting to load in a new "view"
Application structure => tester.java -> CustomController.java
|
V
customController.fxml -> MainMenuController.java
|
V
mainmenu.fxml
tester.java
package task01;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class tester extends Application
{
#Override
public void start(Stage stage) throws Exception
{
CustomController customController = new CustomController();
customController.getStylesheets().add("/task01/stylize.css");
stage.setScene(new Scene(customController,1920,1080));
stage.setTitle("Seneca ATM Program");
stage.setWidth(1920);
stage.setHeight(1080);
stage.show();
}
public static void main(String[] args)
{
Application.launch(args);
}
}
CustomController.java
package task01;
import javafx.event.ActionEvent;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.fxml.*;
import javafx.scene.layout.Pane;
import java.io.IOException;
public class CustomController extends GridPane
{
#FXML
private Pane viewableContent;
public CustomController()
{
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("customController.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try
{
fxmlLoader.load();
} catch (IOException exception)
{
throw new RuntimeException(exception);
}
}
public void swapOut()
{
viewableContent.getChildren().clear();
}
}
customController.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.Pane?>
<?import task01.MainMenuController?>
<fx:root type="javafx.scene.layout.GridPane" xmlns:fx="http://javafx.com/fxml" alignment="CENTER">
<ImageView fitWidth="229.67" fitHeight="149.67" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.halignment="CENTER">
<Image url="/task01/logo.png"/>
</ImageView>
<Pane fx:id="viewableContent" GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.halignment="CENTER">
<MainMenuController/>
</Pane>
</fx:root>
MainMenuController.java
package task01;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Button;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.io.IOException;
public class MainMenuController extends GridPane
{
private CustomController customController = new CustomController();
#FXML
private VBox buttonSet;
#FXML
private HBox buttonSetOne;
#FXML
private HBox buttonSetTwo;
#FXML
private Button changePinButton;
#FXML
private Button accountInquiryButton;
#FXML
private Button withdrawMoneyButton;
#FXML
private Button depositMoneyButton;
#FXML
private Button balanceInquiryButton;
#FXML
private Button createAccountButton;
#FXML
private GridPane gridpane;
#FXML
public void initialize()
{
createAccountButton.setOnAction(event ->
{
customController.swapOut();
});
}
public MainMenuController()
{
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("mainmenu.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try
{
fxmlLoader.load();
} catch (IOException exception)
{
throw new RuntimeException(exception);
}
}
}
mainmenu.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.geometry.Insets?>
<fx:root type="javafx.scene.layout.GridPane" alignment="CENTER" vgap="50" hgap="50" xmlns:fx="http://javafx.com/fxml">
<padding><Insets top="10" bottom="10" left="10" right="10"/></padding>
<VBox fx:id="buttonSet" spacing="25" GridPane.columnIndex="0" GridPane.rowIndex="1">
<HBox fx:id="buttonSetOne" spacing="25">
<Button styleClass="menuButton" fx:id="createAccountButton">Create account</Button>
<Button styleClass="menuButton" fx:id="changePinButton">Change PIN</Button>
<Button styleClass="menuButton" fx:id="accountInquiryButton">Account Inquiry</Button>
</HBox>
<HBox fx:id="buttonSetTwo" spacing="25">
<Button styleClass="menuButton" fx:id="withdrawMoneyButton">Withdraw Money</Button>
<Button styleClass="menuButton" fx:id="depositMoneyButton">Deposit Money</Button>
<Button styleClass="menuButton" fx:id="balanceInquiryButton">Balance Inquiry</Button>
</HBox>
</VBox>
</fx:root>
Basically you need to pass the "parent" controller to the child controller instead of creating a instance in the child controller. (The instance you initialize your field with would be different to the parent instance anyways which would result in non-working code, even if you prevent the stackoverflow somehow.)
In this case you could use the FXMLLoader to pass the value like this:
<fx:root fx:id="rootController" type="javafx.scene.layout.GridPane" xmlns:fx="http://javafx.com/fxml" alignment="CENTER">
<ImageView fitWidth="229.67" fitHeight="149.67" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.halignment="CENTER">
<Image url="/task01/logo.png"/>
</ImageView>
<Pane fx:id="viewableContent" GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.halignment="CENTER">
<MainMenuController customController="$rootController"/> <!-- pass object to setter using fx:id -->
</Pane>
</fx:root>
MainMenuController
private CustomController customController; //= new CustomController();
public void setCustomController(CustomController customController) {
this.customController = customController;
}
// required for fxmlloader to identify the type
public CustomController getCustomController() {
return customController;
}
Related
my program is really simple, it's just two scenes, the second scene just says "this is the second scene" and in the first scene you have to select a folder which will then appear in an un-editable TextField. you can go back and forth between these two scenes using a back and next button. The problem is, when I go to the second scene then go back to the first scene, the TextField is empty, when it should really be displaying the path of the folder I selected before, but when I reselect the folder it shows again, but every time I change the scene, it disappears. The FX:id of the TextField is textfield. I tried making it so that it again sets the text to the path of the folder but this time in the "back" method for the back button. But this never works if it's not in the OpenFolder method for some reason, it doesn't work anywhere else. I want to know why it only works in the OpenFolder method and not anywhere else. This is my controller class:
package sample;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import java.io.File;
import java.io.IOException;
import javafx.event.ActionEvent;
public class Controller {
private Stage stage;
private Scene scene;
private Parent root;
public Controller() {
}
#FXML
public void Next(ActionEvent event) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("scene2.fxml"));
stage = ((Stage)((Node)event.getSource()).getScene().getWindow());
scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
#FXML
public void Back(ActionEvent event) throws IOException{
Parent root = FXMLLoader.load(getClass().getResource("scene1.fxml"));
stage = ((Stage)((Node)event.getSource()).getScene().getWindow());
scene = new Scene(root);
stage.setScene(scene);
stage.show();
}
#FXML
private TextField textfield = new TextField();
#FXML
private AnchorPane anchorid1;
#FXML
public static File thefolder;
#FXML
DirectoryChooser SongsOpener;
#FXML
public File OpenFolder(){
final DirectoryChooser SongsOpener = new DirectoryChooser();
stage = (Stage) anchorid1.getScene().getWindow();
thefolder = SongsOpener.showDialog(stage);
if (thefolder != null){
textfield.setText(thefolder.getAbsolutePath());
}
return thefolder;
}
}
This is my FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>
<AnchorPane fx:id="anchorid1" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="411.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
<children>
<Text layoutX="326.0" layoutY="76.0" strokeType="OUTSIDE" strokeWidth="0.0" text="This is scene 1">
<font>
<Font size="33.0" />
</font>
</Text>
<Button layoutX="939.0" layoutY="373.0" mnemonicParsing="false" onAction="#Next" text="Next" />
<Label layoutX="63.0" layoutY="117.0" prefHeight="18.0" prefWidth="122.0" text="Songs folder :">
<font>
<Font size="20.0" />
</font></Label>
<Button layoutX="888.0" layoutY="121.0" mnemonicParsing="false" onAction="#OpenFolder">
<graphic>
<ImageView fitHeight="18.0" fitWidth="17.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="#../toppng.com-folder-icon-png-transparent-black-and-white-folder-ico-1589x1366.png" />
</image>
</ImageView>
</graphic>
</Button>
<TextField fx:id="textfield" layoutX="190.0" layoutY="119.0" prefHeight="23.0" prefWidth="685.0" />
</children>
</AnchorPane>
Here it shows the path in the TextField after I selected the folder image
But when I go to the next scene and come back, it disappears : image
Every time you load a FXML file, the FXMLLoader creates a new set of controls corresponding to the elements in the FXML file. So if you reload the first view, you get a new TextField, which obviously does not contain the same text as the previous TextField unless you explicitly set the text.
So you can either avoid reloading the FXML files every time (first solution below), or create a mechanism by which you update the text field with the correct value every time you load the FXML (second solution below, using a model and binding to update the text field).
Note also that there's absolutely no reason to create new Scenes every time you change the view. Just use a single scene and replace its root.
Solution loading each view once
So one way to fix this is to just load each FXML once, and then arrange to switch between the two views in the controllers:
App.java:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class App extends Application {
#Override
public void start(Stage stage) throws IOException {
FXMLLoader loader1 = new FXMLLoader(getClass().getResource("View1.fxml"));
Parent view1 = loader1.load();
FXMLLoader loader2 = new FXMLLoader(getClass().getResource("View2.fxml"));
Parent view2 = loader2.load();
Scene scene = new Scene(view1);
View1Controller controller1 = loader1.getController();
View2Controller controller2 = loader2.getController();
controller1.setOnNext(() -> scene.setRoot(view2));
controller2.setOnBack(() -> scene.setRoot(view1));
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
View1Controller.java:
import java.io.File;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
public class View1Controller {
private Runnable onNext = () -> {};
#FXML
private TextField textfield ;
public void setOnNext(Runnable onNext) {
this.onNext = onNext ;
}
#FXML
private void next() {
onNext.run();
}
#FXML
public void openFolder(){
final DirectoryChooser songsOpener = new DirectoryChooser();
Stage stage = (Stage) textfield.getScene().getWindow();
File thefolder = songsOpener.showDialog(stage);
if (thefolder != null){
textfield.setText(thefolder.getAbsolutePath());
}
}
}
View2Controller.java
import javafx.fxml.FXML;
public class View2Controller {
private Runnable onBack = () -> {} ;
public void setOnBack(Runnable onBack) {
this.onBack = onBack ;
}
#FXML
private void back() {
onBack.run();
}
}
View1.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<?import javafx.scene.text.Text?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="411.0" prefWidth="1000.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.jamesd.examples.switchviews.View1Controller">
<children>
<Text layoutX="326.0" layoutY="76.0" strokeType="OUTSIDE" strokeWidth="0.0" text="This is scene 1">
<font>
<Font size="33.0" />
</font>
</Text>
<Button layoutX="939.0" layoutY="373.0" mnemonicParsing="false" onAction="#next" text="Next" />
<Label layoutX="63.0" layoutY="117.0" prefHeight="18.0" prefWidth="122.0" text="Songs folder :">
<font>
<Font size="20.0" />
</font></Label>
<Button layoutX="888.0" layoutY="121.0" mnemonicParsing="false" onAction="#openFolder" text="Open" />
<TextField fx:id="textfield" layoutX="190.0" layoutY="119.0" prefHeight="23.0" prefWidth="685.0" />
</children>
</AnchorPane>
View2.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>
<HBox alignment="CENTER" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.jamesd.examples.switchviews.View2Controller">
<children>
<Button mnemonicParsing="false" onAction="#back" text="Back" />
</children>
</HBox>
Solution using a model to store the state
Another way is to create a model containing the data you need, and bind, for example, the text in the text field to an appropriate property in the model. Then just pass the same model instance to each controller.
For example:
ViewState.java:
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class ViewState {
private final StringProperty selectedFolder = new SimpleStringProperty();
public final StringProperty selectedFolderProperty() {
return this.selectedFolder;
}
public final String getSelectedFolder() {
return this.selectedFolderProperty().get();
}
public final void setSelectedFolder(final String selectedFolder) {
this.selectedFolderProperty().set(selectedFolder);
}
}
Then in App.java:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class App extends Application {
#Override
public void start(Stage stage) throws IOException {
FXMLLoader loader1 = new FXMLLoader(getClass().getResource("View1.fxml"));
Parent view1 = loader1.load();
ViewState viewState = new ViewState();
View1Controller controller = loader1.getController();
controller.setViewState(viewState);
Scene scene = new Scene(view1);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
View1Controller.java:
import java.io.File;
import java.io.IOException;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.control.TextField;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
public class View1Controller {
#FXML
private TextField textfield ;
private ViewState viewState ;
public void setViewState(ViewState viewState) {
this.viewState = viewState ;
textfield.textProperty().bindBidirectional(viewState.selectedFolderProperty());
}
#FXML
private void next() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("View2.fxml"));
Parent root = loader.load();
textfield.getScene().setRoot(root);
View2Controller controller = loader.getController();
controller.setViewState(viewState);
}
#FXML
public void openFolder(){
final DirectoryChooser songsOpener = new DirectoryChooser();
Stage stage = (Stage) textfield.getScene().getWindow();
File thefolder = songsOpener.showDialog(stage);
if (thefolder != null){
textfield.setText(thefolder.getAbsolutePath());
}
}
}
View2Controller:
import java.io.IOException;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
public class View2Controller {
private ViewState viewState ;
#FXML
private Parent root ;
public void setViewState(ViewState viewState) {
this.viewState = viewState;
}
#FXML
private void back() throws IOException {
FXMLLoader loader = new FXMLLoader(getClass().getResource("View1.fxml"));
Parent newRoot = loader.load();
this.root.getScene().setRoot(newRoot);
View1Controller controller = loader.getController();
controller.setViewState(viewState);
}
}
The only change to the FXML files is adding a fx:id to the root element in the second view:
View2.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>
<HBox fx:id="root" alignment="CENTER" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.jamesd.examples.switchviews.View2Controller">
<children>
<Button mnemonicParsing="false" onAction="#back" text="Back" />
</children>
</HBox>
if i run the Main Class my JavaFX is not opening. A Java symbol appears in my menubar but no window appears. I don't get any error in the console. I am on a Mac machine and I am using eclipse, together with e(fx)clipes and Gluon Scene Builder. The code is actually 1 by 1 written down from a tutorial. In the tutorial the run configuration JRE is set to JavaSE-1.8. - however, I tested every available JRE, without any success. What do I need to adjust?
Main Class:
package application;
import java.io.IOException;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.layout.AnchorPane;
public class Main extends Application {
private Stage primaryStage;
#Override
public void start(Stage primaryStage) {
this.primaryStage = primaryStage;
}
public void mainWindow() {
try {
FXMLLoader loader = new FXMLLoader(Main.class.getResource("MainWindow.fxml"));
AnchorPane pane = loader.load();
primaryStage.setMinHeight(400.00);
primaryStage.setMinWidth(500.00);
MainWindowController mainWindowController = loader.getController();
mainWindowController.setMain(this);
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Controller class:
package application;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
public class MainWindowController {
public Main main;
#FXML private Label label;
#FXML private TextField field;
#FXML private Button clear;
#FXML private Button changeText;
public void setMain(Main main) {
// TODO Auto-generated method stub
this.main = main;
}
#FXML
public void handleChangeText() {
String text = field.getText();
label.setText(text);
}
#FXML
public void handleClear() {
field.clear();
}
}
FXML file:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<AnchorPane prefHeight="400.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainWindowController">
<children>
<VBox alignment="CENTER" layoutY="83.0" spacing="30.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<children>
<Label fx:id="label" text="Label">
<font>
<Font size="24.0" />
</font></Label>
<HBox alignment="CENTER">
<children>
<TextField fx:id="field" prefWidth="220.0" />
</children>
</HBox>
<HBox alignment="CENTER" spacing="20.0">
<children>
<Button fx:id="changeText" mnemonicParsing="false" onAction="#handleChangeText" text="Change Text" />
<Button fx:id="clear" mnemonicParsing="false" onAction="#handleClear" text="Clear" />
</children>
</HBox>
</children>
</VBox>
</children>
</AnchorPane>
You didn't do anything in you start method. All you did was set your primaryStage variable, you should call your mainWindow method after that.
In my application I have declared a custom component like this:
#DefaultProperty("todoItems")
public class TodoItemsVBox extends VBox {
private ObservableList<TodoItem> todoItems;
// Setter/Getter omitted
}
and now somewhere in the fxml I want to use the TodoItemsVBox component like this:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<BorderPane prefHeight="600" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.todolist.controller.TodoListController"
stylesheets="#../css/app.css">
<top>
<HBox spacing="10.0">
<TextField fx:id="input" layoutX="35.0" layoutY="64.0" prefWidth="431.0" promptText="Enter todo task" HBox.hgrow="ALWAYS" onAction="#addTask"/>
<Button layoutX="216.0" layoutY="107.0" mnemonicParsing="false" onAction="#addTask" prefHeight="27.0" prefWidth="70.0" text="Add" HBox.hgrow="ALWAYS" />
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</HBox>
</top>
<center>
<ScrollPane fitToHeight="true" fitToWidth="true" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<TodoItemsVBox fx:id="todoItemsVBox" todoItems="${todoTasks}"/>
</ScrollPane>
</center>
... so as we can see the fxml has it's controller TodoListController
public class TodoListController implements {
private final ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList(/*Fill in the collection somehow - for now doesn't matter*/);
#FXML
private TodoItemsVBox todoItemsVBox;
// Setter/Getter omitted
}
So, here what I want to do: pass todoTasks into TodoItemsVBox defined in the FXML via such construction: todoItems="${todoTasks}" ---- unfortunately this doesn't work as I expected, because fxml files load before controllers initialised so todoTasks is always null. I also tried #NamedArg with one arg constructor in TodoItemsVBox - it even fails with exception: "Cannot bind to untyped object.”.
Could some one suggest a solution how to pass a collection of objects, defined in a controller, into a custom component via it's parameters?
There are two issues with the code as you have it:
For FXML expression binding, you need to expose properties from your class, not just the values themselves. This applies to ObservableLists as well as regular values. So your TodoItemsVBox class needs to expose a ListProperty todoItemsProperty()
FXML expression bindings (i.e. ${todoTasks}) reference the FXMLLoader's namespace, not the controller. The controller is automatically injected into the namespace (with key "controller"), so, given that the task list is stored in your controller (which is not necessarily a good idea) you can use ${controller.todoTasks} here.
Here's a minimal, complete version of your app which works.
A basic TodoItem.java:
public class TodoItem {
private final String name ;
public TodoItem(String name) {
this.name = name ;
}
public String getName() {
return name ;
}
}
A TodoItemsVBox that exposes the list as a property:
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.ListChangeListener.Change;
import javafx.collections.ObservableList;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
public class TodoItemsVBox extends VBox {
private ListProperty<TodoItem> todoItems = new SimpleListProperty<>();
public TodoItemsVBox() {
// not efficient but works for demo:
todoItems.addListener((Change<? extends TodoItem> c) -> rebuildView());
}
private void rebuildView() {
getChildren().clear();
todoItems.stream()
.map(TodoItem::getName)
.map(Label::new)
.forEach(getChildren()::add);
}
public ListProperty<TodoItem> todoItemsProperty() {
return todoItems ;
}
public ObservableList<TodoItem> getTodoItems() {
return todoItemsProperty().get() ;
}
}
A simple controller:
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
public class TodoListController {
private ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList();
// not actually needed...
#FXML
private TodoItemsVBox todoItemsVBox;
#FXML
private TextField input ;
public ObservableList<TodoItem> getTodoTasks() {
return todoTasks;
}
#FXML
private void addTask() {
todoTasks.add(new TodoItem(input.getText()));
}
}
The FXML file (TodoList.fxml):
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<?import org.jamesd.examples.TodoItemsVBox ?>
<BorderPane prefHeight="600" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="com.todolist.controller.TodoListController"
>
<top>
<HBox spacing="10.0">
<TextField fx:id="input" promptText="Enter todo task" HBox.hgrow="ALWAYS" onAction="#addTask"/>
<Button onAction="#addTask" text="Add" HBox.hgrow="ALWAYS" />
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
</HBox>
</top>
<center>
<ScrollPane fitToWidth="true" prefHeight="200.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<TodoItemsVBox fx:id="todoItemsVBox" todoItems="${controller.todoTasks}"/>
</ScrollPane>
</center>
</BorderPane>
And finally the application class:
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class TodoApp extends Application {
#Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("TodoList.fxml"));
Scene scene = new Scene(loader.load());
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
}
Really, a controller is no place to be storing data; you should have a separate model class to do that, which is shared between the controller and view. This is reasonably straightforward to do here; you just need to do a little more work with the FXMLLoader (namely putting the model in the namespace, and manually creating and setting the controller).
For example:
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class TodoModel {
private ObservableList<TodoItem> todoTasks = FXCollections.observableArrayList();
public ObservableList<TodoItem> getTodoTasks() {
return todoTasks;
}
}
Then your controller becomes:
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
public class TodoListController {
// not actually needed...
#FXML
private TodoItemsVBox todoItemsVBox;
#FXML
private TextField input ;
private TodoModel model ;
public TodoModel getModel() {
return model;
}
public void setModel(TodoModel model) {
this.model = model;
}
#FXML
private void addTask() {
model.getTodoTasks().add(new TodoItem(input.getText()));
}
}
Modify the FXML to use
<TodoItemsVBox fx:id="todoItemsVBox" todoItems="${model.todoTasks}"/>
And finally assemble the application with
public void start(Stage primaryStage) throws Exception {
TodoModel model = new TodoModel();
FXMLLoader loader = new FXMLLoader(getClass().getResource("TodoList.fxml"));
loader.getNamespace().put("model", model);
Scene scene = new Scene(loader.load());
TodoListController controller = loader.getController();
controller.setModel(model);
primaryStage.setScene(scene);
primaryStage.show();
}
The advantage to this approach is that your data are now separated from the UI (both view and controller), which becomes essential if you want to access the same data in another part of the UI (which would use another FXML and another controller).
This is what my view looks like:
Explanation
The entire window itself runs on one controller, called the CartWindowController, and the product list itself is a JavaFX Custom Control called CartItemComponent, which has it's own controller. Each item in the list, therefore has it's own instance of the controller as I programmatically make an instance and populate a VBox.
Problem
I cannot figure out how to notify the CartWindowController when the user clicks on the "X" button, which is handled by the "CartItemComponent" Controller. I would highly appreciate it if anyone could give me a heads up on how to tackle this problem.
Here's what my FXML looks like for the entire Window:
CartWindow.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.VBox?>
<AnchorPane id="AnchorPane" fx:id="parent" prefHeight="400.0" prefWidth="600.0" styleClass="pane" stylesheets="#../assets/userwindow.css" xmlns="http://javafx.com/javafx/8.0.111" xmlns:fx="http://javafx.com/fxml/1" fx:controller="ordermanagementsystem.controllers.CartWindowController">
<children>
<Button layoutX="25.0" layoutY="25.0" mnemonicParsing="false" onAction="#exitWindow" prefHeight="35.0" prefWidth="20.0" styleClass="back-button" />
<Label layoutX="262.0" layoutY="17.0" styleClass="heading" text="Cart" />
<ScrollPane hbarPolicy="NEVER" layoutY="97.0" prefHeight="315.0" prefWidth="600.0" styleClass="no-padding">
<content>
<AnchorPane prefWidth="600.0" styleClass="no-padding">
<children>
<VBox fx:id="productList" layoutX="25.0" prefWidth="350.0" />
<AnchorPane layoutX="425.0" layoutY="25.0" prefWidth="150.0" styleClass="no-padding">
<children>
<Label layoutX="18.0" styleClass="heading-sub" text="Summary" />
<Label layoutX="1.0" layoutY="55.0" text="Gross:" />
<Label fx:id="grossTotal" alignment="CENTER_RIGHT" layoutX="47.0" layoutY="55.0" prefHeight="17.0" prefWidth="103.0" text="RM0.00" />
<Label layoutX="1.0" layoutY="75.0" text="Packaging:" />
<Label fx:id="packagingTotal" alignment="CENTER_RIGHT" layoutX="73.0" layoutY="75.0" prefHeight="17.0" prefWidth="77.0" text="RM0.00" />
<Label layoutX="1.0" layoutY="95.0" text="Total:" />
<Label fx:id="total" alignment="CENTER_RIGHT" layoutX="40.0" layoutY="95.0" prefHeight="17.0" prefWidth="110.0" styleClass="green-text" text="RM0.00" />
<Button layoutY="125.0" mnemonicParsing="false" onAction="#checkout" prefHeight="39.0" prefWidth="150.0" styleClass="action-button" text="Check Out" />
</children>
</AnchorPane>
</children>
</AnchorPane>
</content>
</ScrollPane>
</children>
</AnchorPane>
CartWindowController.java
package ordermanagementsystem.controllers;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Parent;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import ordermanagementsystem.cart.CartState;
import ordermanagementsystem.orders.models.OrderItem;
import ordermanagementsystem.viewcomponents.CartItemComponent;
public class CartWindowController extends ViewController implements Initializable {
#FXML
public Parent parent;
#FXML
private VBox productList;
#FXML
private Label grossTotal;
#FXML
private Label packagingTotal;
#FXML
private Label total;
private CartState cartState;
#FXML
private void exitWindow(ActionEvent event) {
try {
this.openPage(this.parent, "MainWindow.fxml");
} catch (IOException ex) {
ex.printStackTrace();
}
}
#FXML
private void checkout(ActionEvent event) {
}
#Override
public void initialize(URL url, ResourceBundle rb) {
this.cartState = CartState.getInstance();
for (OrderItem item : this.cartState.getItems()) {
this.productList.getChildren().add(new CartItemComponent(item));
}
}
}
CartItemComponent.java:
package ordermanagementsystem.viewcomponents;
import java.io.IOException;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import ordermanagementsystem.DialogBox;
import ordermanagementsystem.cart.CartState;
import ordermanagementsystem.orders.models.OrderItem;
public class CartItemComponent extends AnchorPane {
#FXML
private AnchorPane frame;
#FXML
private TextField quantity;
#FXML
private Label total;
#FXML
private Label productName;
private OrderItem item;
private CartState cartState;
public CartItemComponent(OrderItem item) {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/ordermanagementsystem/views/components/CartItemComponent.fxml"));
fxmlLoader.setRoot(this);
fxmlLoader.setController(this);
try {
fxmlLoader.load();
} catch (IOException exception) {
exception.getStackTrace();
}
this.cartState = CartState.getInstance();
this.item = item;
this.setQuantity(1);
this.quantity.setEditable(false);
this.productName.setText(this.item.getProduct().getName());
}
private void setQuantity(int quantity) {
this.quantity.setText(String.valueOf(quantity));
this.item.setQuantity(quantity);
this.cartState.updateItem(this.item);
this.total.setText("RM" + String.format("%.2f", item.getProduct().getPrice() * quantity));
}
private int getQuantity() {
return Integer.parseInt(this.quantity.getText());
}
private void updateSummary() {
}
#FXML
private void add(ActionEvent event) {
int quantity = this.getQuantity();
if (quantity == 99) {
DialogBox.showValidationDialog("The quantity cannot be over 99.");
} else {
this.setQuantity(quantity + 1);
}
}
#FXML
private void substract(ActionEvent event) {
int quantity = this.getQuantity();
if (quantity == 1) {
DialogBox.showValidationDialog("The quantity cannot be below 1.");
} else {
this.setQuantity(quantity - 1);
}
}
}
Something you could try is to pass the CartWindowController into the CartItemComponent constructor, then when you need to notify the CartWindowController, you call a method or set a flag or trigger an event, your choice! All you would need to do is add a parameter of type CartWindowController to you CartItemComponent and just save the reference.
It's about JavaFX. When i want to inject fx:id in Scene Builder, i get this warning: No injectable field found in FXML Controller class for the id 'something'. I wanted to ignore it, and created a function, but it didn't work either. I created mainController class and added it into my FXML file. Here are my codes...
mainController.java
package main;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressBar;
import javafx.scene.control.Slider;
public class mainController implements Initializable {
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
// TODO Auto-generated method stub
}
#FXML
private ProgressBar pb;
#FXML
private Slider sl;
#FXML
private Label label;
public void changed(ActionEvent event){
}
}
Main.java
package main;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ProgressBar;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
Parent root = FXMLLoader.load(getClass().getResource("/fxml/main.fxml"));
Scene scene = new Scene(root, 600, 400);
scene.getStylesheets().add("/fxml/styles/main.css");
ProgressBar pb1 = new ProgressBar();
ProgressBar pb2 = new ProgressBar();
//pb1.
//primaryStage.setResizable(false);
primaryStage.setScene(scene);
primaryStage.setTitle("Something");
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
main.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane id="AnchorPane" prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8" fx:controller="main.mainController">
<children>
<BorderPane layoutX="14.0" layoutY="14.0" prefHeight="200.0" prefWidth="200.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<top>
<ProgressBar fx:id="pb" prefWidth="200.0" progress="0.0" BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets bottom="30.0" left="30.0" right="30.0" top="30.0" />
</BorderPane.margin>
</ProgressBar>
</top>
<bottom>
<Slider fx:id="sl" onDragDetected="#changed" BorderPane.alignment="CENTER">
<BorderPane.margin>
<Insets bottom="30.0" left="30.0" right="30.0" top="30.0" />
</BorderPane.margin>
</Slider>
</bottom>
</BorderPane>
</children>
</AnchorPane>
I did same things in my old projects, and they work like a charm. But this one seems not to obey me. Thanks in advance...
In Scene Builder, if the FXML file is associated to a controller class, you know that what makes the connection between the variable in the controller class (pb) and the object in the FXML file (<ProgressBar ... />) is the value of the object's fx:id.
So when you set an fx:id on an object, Scene Builder tries to parse the controller class trying to find a variable of that name.
If it doesn't find any, it displays this warning. It's just a reminder that you may want to add such a variable to the controller class, or that you have to choose other valid name from a list.
Since you have label defined on your controller, if you try to add a Label on Scene Builder, you can get its fx:id from the list:
But if you assing another name, you will get the warning:
On a side note, you don't need to instantiate the progress bar on the main class, since it will be instantiated in the controller. And ìf you try to link change(ActionEvent event) to a method in Scene Builder (#change), you have to annotate it with #FXML. Anyway, don't use onDragDetected with the slider.
don't forget that Netbeans will Auto-Generate the code in the controller class.
Goto your FXMLDocument.fxml in your Netbeans project, and right click and select: "Make Controller".
Also, remember to save your changes first to your FXMLDocument.fxml, in Scene builder.