Issue
SUMMARY: I am trying to design an application where the user inputs into TextFields, and upon clicking a button ONCE:
- The values are retrieved
- The scene switches to one containing a lineChart
- The number axes' bounds are set corresponding to the values entered before.
However I'm finding it very hard to understand how to make the program flow correctly for this to work. My main issues are with switching the scene then accessing x_axis and y_axis, where for some reason "this.x_axis" is null.
Main Class:
public class Main extends Application {
@Override
public void start(Stage stage) throws IOException {
Parent root = FXMLLoader.load(getClass().getResource("menuScene.fxml"));
Scene scene = new Scene(root);
stage.setTitle("FT_Tool");
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
Controller: (calculate is called after clicking button)
public class MenuController {
private Stage stage;
private Scene scene;
private Parent root;
@FXML
private TextField funcBox;
@FXML
private TextField sampBox;
@FXML
private TextField limitBox;
Expression func;
double sampFreq;
double limit;
@FXML
public void calculate(ActionEvent event) throws IOException{
func = new ExpressionBuilder(funcBox.getText()).variables("x").build();
sampFreq = Double.parseDouble(sampBox.getText());
limit = Double.parseDouble(limitBox.getText());
//=== SWITCHING SCENES ===//
root = FXMLLoader.load(getClass().getResource("graphScene.fxml"));
stage = (Stage) ((Node) event.getSource()).getScene().getWindow();
scene = new Scene(root);
stage.setScene(scene);
stage.show();
graphing();
}
@FXML
private LineChart sampleGraph;
@FXML
private NumberAxis x_axis;
@FXML
private NumberAxis y_axis;
public void graphing(){
x_axis.setUpperBound(limit);
}
}
2nd Scene:
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/16" xmlns:fx="http://javafx.com/fxml/1">
<children>
<LineChart fx:id="sampleGraph" layoutX="24.0" layoutY="16.0" prefHeight="368.0" prefWidth="552.0" title="Samples taken">
<xAxis>
<NumberAxis fx:id="x_axis" autoRanging="false" label="Time (s)" tickUnit="1.0"/>
</xAxis>
<yAxis>
<NumberAxis fx:id="y_axis" autoRanging="false" label="Amplitude (m)"/>
</yAxis>
</LineChart>
</children>
</AnchorPane>
When I run it, application runs fine and after clicking button the scene switches and linechart is shown but without the right bounds.
This line of code: x_axis.setUpperBound(limit)
causes the following error:
Caused by: java.lang.NullPointerException: Cannot invoke "javafx.scene.chart.NumberAxis.setUpperBound(double)" because "this.x_axis" is null
Solution
Despite not having the fx:controller
attribute defined in your FXML file, it looks like—based on your FXML controller class—you're trying to use the same FXML controller class with multiple FXML files. This is strongly discouraged. By default, a new controller instance is created every time the FXML file is loaded. Consequently, it quickly becomes difficult to reason about which fields should have been injected (i.e., non-null) at any given time. An FXML file should have a one-to-one mapping with an FXML controller class (or possibly no controller).
You have two views, which suggests two FXML files and thus two FXML controller classes. When you load the second view, you should communicate to its controller the needed information. For trivial applications you can do this directly, but for more complex applications it would be better to communicate via a model shared between the two controllers.
Example
Here's an example (based on your code but simplified for demonstration). It only asks for the upper bounds of the X and Y axes and then graphs random data within those bounds. It also makes use of TextFormatter
to make it easier to grab the values out of the TextField
s, and to ensure the user only enters positive integers.
FXML files are assumed to be located in the com.example
package.
Main.java:
package com.example;
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 {
var root = FXMLLoader.<Parent>load(Main.class.getResource("Form.fxml"));
primaryStage.setScene(new Scene(root, 1000, 650));
primaryStage.show();
}
}
Form.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.layout.GridPane?>
<GridPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.FormController" alignment="CENTER" vgap="20" hgap="15">
<Label text="Upper X Bound:" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<TextField fx:id="xField" GridPane.rowIndex="0" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
<Label text="Upper Y Bound:" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<TextField fx:id="yField" GridPane.rowIndex="1" GridPane.columnIndex="1" GridPane.columnSpan="2"/>
<Button text="Graph" onAction="#handleOnGraph" defaultButton="true" GridPane.rowIndex="2"
GridPane.columnIndex="2" GridPane.halignment="RIGHT"/>
</GridPane>
FormController.java:
package com.example;
import java.io.IOException;
import java.util.function.UnaryOperator;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.util.converter.IntegerStringConverter;
public class FormController {
@FXML private TextField xField;
@FXML private TextField yField;
private TextFormatter<Integer> xFieldFormatter;
private TextFormatter<Integer> yFieldFormatter;
@FXML
private void initialize() {
UnaryOperator<TextFormatter.Change> filter =
change -> {
if (change.getControlNewText().matches("\\d*")) {
return change;
}
return null;
};
xFieldFormatter = new TextFormatter<>(new IntegerStringConverter(), 50, filter);
xField.setTextFormatter(xFieldFormatter);
yFieldFormatter = new TextFormatter<>(new IntegerStringConverter(), 50, filter);
yField.setTextFormatter(yFieldFormatter);
}
@FXML
private void handleOnGraph(ActionEvent event) throws IOException {
event.consume();
int upperX = xFieldFormatter.getValue();
int upperY = yFieldFormatter.getValue();
var loader = new FXMLLoader(FormController.class.getResource("Chart.fxml"));
xField.getScene().setRoot(loader.load());
loader.<ChartController>getController().graph(upperX, upperY);
}
}
Chart.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.chart.LineChart?>
<?import javafx.scene.chart.NumberAxis?>
<LineChart fx:id="chart" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.ChartController">
<xAxis>
<NumberAxis fx:id="xAxis" upperBound="50" autoRanging="false"/>
</xAxis>
<yAxis>
<NumberAxis fx:id="yAxis" upperBound="50" autoRanging="false"/>
</yAxis>
</LineChart>
ChartController.java:
package com.example;
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
public class ChartController {
@FXML private LineChart<Number, Number> chart;
@FXML private NumberAxis xAxis;
@FXML private NumberAxis yAxis;
void graph(int upperX, int upperY) {
xAxis.setUpperBound(upperX);
yAxis.setUpperBound(upperY);
generateAndGraphRandomData();
}
private void generateAndGraphRandomData() {
var data = FXCollections.<XYChart.Data<Number, Number>>observableArrayList();
for (int x = 0; x <= xAxis.getUpperBound(); x++) {
int y = (int) (Math.random() * yAxis.getUpperBound());
data.add(new XYChart.Data<>(x, y));
}
var series = new XYChart.Series<>("Random Demo Data", data);
chart.getData().add(series);
}
}
Answered By - Slaw
Answer Checked By - David Goodson (JavaFixing Volunteer)