Issue
I have a simple JavaFX application with main controller. In this main controller I dynamically add this component :
Added component:
FXML code of this component (paramValue.fxml) :
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?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.HBox?>
<HBox alignment="CENTER" prefHeight="40.0" prefWidth="833.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.demoSpreadsSheet.ParamValueController">
<children>
<Label prefHeight="95.0" prefWidth="83.0" text="Paramètre : " />
<TextField fx:id="paramName" prefHeight="32.0" prefWidth="300.0" />
<Label prefHeight="95.0" prefWidth="83.0" text="Valeur : ">
<HBox.margin>
<Insets left="40.0" />
</HBox.margin>
</Label>
<TextField fx:id="paramValue" prefHeight="32.0" prefWidth="300.0" />
<Button mnemonicParsing="false" prefHeight="25.0" prefWidth="12.0">
<HBox.margin>
<Insets left="20.0" />
</HBox.margin>
<graphic>
<ImageView fitHeight="26.0" fitWidth="21.0" onMouseClicked="#openSpreadSheet" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../Images/spreadsheet.png" />
</image>
</ImageView>
</graphic>
</Button>
<Button mnemonicParsing="false" onAction="#removeParamValue" prefHeight="25.0" prefWidth="12.0">
<graphic>
<ImageView fitHeight="26.0" fitWidth="21.0" onMouseClicked="#openSpreadSheet" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@../../Images/remove.png" />
</image>
</ImageView>
</graphic>
<HBox.margin>
<Insets left="10.0" />
</HBox.margin>
</Button>
</children>
</HBox>
Controller of the component :
package com.example.demoSpreadsSheet;
import javafx.fxml.FXML;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
public class ParamValueController extends HBox {
@FXML TextField paramName, paramValue;
public ParamValueController() {}
@FXML
public void openSpreadSheet(){
}
@FXML
private void removeParamValue(){
//I don't know if it's possible to remove it here
}
}
The controller where I add my custom component :
package com.example.demoSpreadsSheet;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import java.io.IOException;
public class DoublonController extends BorderPane {
@FXML VBox paramValueList;
public DoublonController(){}
@FXML
public void initialize() throws IOException {
paramValueList.getChildren().addAll(createParamValueComponent(), createParamValueComponent());
}
private HBox createParamValueComponent() throws IOException {
FXMLLoader paramValueLoader = new FXMLLoader(DoublonController.class.getResource("paramValue.fxml"));
return paramValueLoader.load();
}
@FXML
private void addParamValue() throws IOException {
this.paramValueList.getChildren().add(createParamValueComponent());
}
}
I want to remove this component when the user click on the red cross. The problem is that the redcross calls the function "removeParamValue()" within the class that I want to remove which i think is impossible..
Do you have a workaround ?
Thanks in advance.
Solution
There are at least two approaches you can use.
Get the Parent
As mentioned by @kleopatra in a question comment, you can get the parent of the root FXML node from within the "child" controller, check if it is an instance of Pane
and, if it is, then modify the parent's children list.
For example:
@FXML
private void removeParamValue() {
var parent = container.getParent();
if (parent instanceof Pane pane) {
pane.getChildren().remove(container);
}
}
This requires you to inject the root FXML element into the controller. I made the name of the field container
, whose type would be HBox
, but you can name it anything you'd like (just make sure add the corresponding fx:id
attribute to the FXML file). I also made use of pattern matching for instanceof
, which was finalized in Java 16. If you are not using at least Java 16, then you'll need to manually cast parent
to a Pane
.
Problem With Your Code
Note you can't use this.getParent()
, because the controller itself has never been added to the scene graph. In fact, with how your code is currently written, your controller class should not be extending HBox
at all (and I assume a similar problem with your other controller class). Perhaps you meant to use fx:root
, but that would involve changing both the root element of your FXML file and how you load the FXML file. Read the linked documentation for more information.
In other words, unless you want to use fx:root
, remove the extends HBox
and extends BorderPane
from your controller classes.
Use a "Callback"
The other approach is to use a callback. Add a method to your ParamValueController
which accepts some functional interface (e.g., Runnable
):
public void setOnRemove(Runnable action) {
// also declare a field to store the Runnable in
this.action = action;
}
Then your "remove param value" method would look like:
@FXML
private void removeParamValue() {
if (action != null) {
action.run();
}
}
And you'd modify the code that loads the "child" FXML like so:
private HBox createParamValueComponent() throws IOException {
FXMLLoader paramValueLoader = new FXMLLoader(DoublonController.class.getResource("paramValue.fxml"));
HBox root = paramValueLoader.load();
// invoked *after* you load the FXML file
ParamValueController controller = paramValueLoader.getController();
controller.setOnRemove(() -> paramValueList.getChildren().remove(root));
return root;
}
Personally, I prefer this approach as it's safer, more flexible, and in my opinion clearer. But it is also more work than the first approach.
Answered By - Slaw
Answer Checked By - Clifford M. (JavaFixing Volunteer)