JavaFX TitledPane lookup(.title) returns null - java

I'm new to Java FX and am creating an application for fun. I'm trying to add a TitledPane dynamically and am getting Null Pointer Exceptions when attempting to lookup the title of the TitledPane about 70% of the time. I tried to create a simple demo of my issue, but was unable to reproduce the issue outside of my application, but I could solve my issue. I'm hoping someone could help me understand why my solution works and maybe point me in the direction of a better solution. I'm using an FXML file with a Controller. I'm attempting to lookup the title inside of Platform.runLater() because I'm manually editing the layout and elements of the title. Inside of the Controller's initialize function, I do the following to get null pointer exceptions:
Platform.runLater(new Runnable() {
#Override
public void run() {
titledpane.lookup(".title"); // will return null about 70% of the time
}
});
// Do more stuff
However, if I wrap that call in a timer and execute it in 500 ms, it doesn't seem to ever return Null.
new java.util.Timer().schedule(new java.util.TimerTask() {
#Override
public void run() {
Platform.runLater(new Runnable() {
#Override
public void run() {
titledpane.lookup(".title"); // doesn't seem to return null
}
});
}
}, 500);
One forum mentioned that CSS had to be applied to the element prior to calling a lookup on the title, so I tried manually applying CSS to the title, but titledpane.lookup(".title") still returned null. Can anyone help me understand what is happening here? Thanks in advance!

I had the same issue. I resolved it by calling applyCss() and layout() on the pane that contains the TitledPane:
Node loadedPane = paneLoader.load();
bodyPane.setCenter(loadedPane);
bodyPane.applyCss();
bodyPane.layout();

You should read the article Creating a Custom Control with FXML.
Here's an example about how you can load a TitledPane dynamically. A TitledPane is added each time you click the "Add Task" button.
Task.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.text.*?>
<?import javafx.scene.effect.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<fx:root collapsible="false" prefHeight="72.0" prefWidth="202.0" text="Task" type="TitledPane" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1">
<content>
<Pane prefHeight="43.0" prefWidth="200.0">
<children>
</children>
</Pane>
</content>
</fx:root>
Task.java
import java.io.IOException;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.Region;
public class Task extends Region {
TitledPane titledPane;
public Task( String title) {
final FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource( "Task.fxml"));
titledPane = new TitledPane();
fxmlLoader.setRoot( titledPane);
fxmlLoader.setController( this);
try {
fxmlLoader.load();
} catch( IOException exception) {
throw new RuntimeException( exception);
}
titledPane.setText(title);
getChildren().add( titledPane);
}
}
Demo.java
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;
public class Demo extends Application {
#Override
public void start(Stage primaryStage) {
Group root = new Group();
Button addTaskButton = new Button( "Add Task");
addTaskButton.setOnAction(new EventHandler<ActionEvent>() {
double x=0;
double y=0;
int count=0;
#Override
public void handle(ActionEvent event) {
// calculate new position
x+=50;
y+=50;
// task counter
count++;
Task task = new Task( "Task " + count);
task.relocate(x, y);
root.getChildren().add( task);
}
});
root.getChildren().add( addTaskButton);
Scene scene = new Scene( root, 1024, 768);
primaryStage.setScene( scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Screenshot:
There are various solutions in order to create a custom title. Here's one. Note: You need to provide an icon in the proper path or adapt the path. Alternatively you can just disable the ImageView node and instead use the Rectangle for demonstration purposes. It's just another node that's displayed.
public class Task extends Region {
TitledPane titledPane;
public Task( String title) {
final FXMLLoader fxmlLoader = new FXMLLoader( getClass().getResource( "Task.fxml"));
titledPane = new TitledPane();
fxmlLoader.setRoot( titledPane);
fxmlLoader.setController( this);
try {
fxmlLoader.load();
} catch( IOException exception) {
throw new RuntimeException( exception);
}
// create custom title with text left and icon right
AnchorPane anchorpane = new AnchorPane();
double offsetRight = 20; // just an arbitrary value. usually the offset from the left of the title
anchorpane.prefWidthProperty().bind(titledPane.widthProperty().subtract( offsetRight));
// create text for title
Label label = new Label( title);
// create icon for title
Image image = new Image( getClass().getResource( "title.png").toExternalForm());
ImageView icon = new ImageView();
icon.setImage(image);
// Rectangle icon = new Rectangle(16, 16);
// set text and icon positions
AnchorPane.setLeftAnchor(label, 0.0);
AnchorPane.setRightAnchor(icon, 0.0);
// add text and icon to custom title
anchorpane.getChildren().addAll( label, icon);
// set custom title
titledPane.setGraphic( anchorpane);
// show only our custom title, don't show the text of titlePane
titledPane.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
getChildren().add( titledPane);
}
}

Related

How to write conditions on button click in JavaFX

I want to make it so that when a button is pressed, different windows are displayed, and for this I need conditions. I don't want to create many methods for each button
This code doesn't work:
#Override
public void buttonOnAction(ActionEvent event){
if(btnReaders.isPressed()){
btnReaders.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent e) {
Parent parent = null;
try {
parent = FXMLLoader.load(getClass().getResource("readersMenu.fxml"));
} catch (IOException ex) {
ex.printStackTrace();
}
Scene scene = new Scene(parent);
Stage window = (Stage) ((Node)event.getSource()).getScene().getWindow();
window.setScene(scene);
window.show();
}
});
}
else if(btnDashboard.isPressed()){
btnDashboard.setOnAction(new EventHandler<ActionEvent>() {
#Override public void handle(ActionEvent e) {
Parent parent = null;
try {
parent = FXMLLoader.load(getClass().getResource("librarianMenu.fxml"));
} catch (IOException ex) {
ex.printStackTrace();
}
Scene scene = new Scene(parent);
Stage window = (Stage) ((Node)event.getSource()).getScene().getWindow();
window.setScene(scene);
window.show();
}
});
}
}
Here is an example of a parameterized event handler that will open the selected FXML in a new scene that will be set for the same stage containing the source node of the event.
When the event handler is created, the application stores, in the event handler, the name of the FXML resource to be loaded.
The event handler is assigned to a button action.
When the button is actioned, the event handler loads a new FXML into a new scene and attaches that scene to the window that the button is defined in.
Example App
For this example, FXML files should be in the same location as the package containing the SceneSelector application.
SceneSelector.java
import javafx.application.Application;
import javafx.event.*;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.*;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.Objects;
public class SceneSelector extends Application {
#Override
public void start(Stage stage) {
Button sceneAButton = new Button("Scene A");
sceneAButton.setOnAction(
new SceneChangeEventHandler(
"sceneA.fxml"
)
);
Button sceneBButton = new Button("Scene B");
sceneBButton.setOnAction(
new SceneChangeEventHandler(
"sceneB.fxml"
)
);
Pane layout = new HBox(10,
sceneAButton,
sceneBButton
);
layout.setPadding(new Insets(10));
layout.setPrefSize(200, 150);
stage.setScene(
new Scene(layout)
);
stage.show();
}
class SceneChangeEventHandler implements EventHandler<ActionEvent> {
private final String fxmlResourceName;
public SceneChangeEventHandler(String fxmlResourceName) {
this.fxmlResourceName = fxmlResourceName;
}
#Override
public void handle(ActionEvent event) {
try {
Stage stage = (Stage) ((Node) event.getSource())
.getScene()
.getWindow();
changeScene(stage, fxmlResourceName);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void changeScene(
Stage stage,
String fxmlResourceName
) throws IOException {
Parent parent = FXMLLoader.load(
Objects.requireNonNull(
getClass().getResource(
fxmlResourceName
)
)
);
Scene scene = new Scene(parent);
stage.setScene(scene);
stage.setTitle(fxmlResourceName);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
sceneA.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<StackPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
prefHeight="150.0" prefWidth="200.0" style="-fx-background-color: lemonchiffon;"/>
sceneB.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<StackPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
prefHeight="150.0" prefWidth="200.0" style="-fx-background-color: azure;"/>

JavaFx: Scroll reset after TitledPane is collapsed

I am using TitledPanes ScrollPanes and TableViews and I have the problem, when I collapse a titledPane, the horizontal ScrollBar of the TableView resets.
Here is a code example where you can verify it:
import javafx.collections.FXCollections;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TitledPane;
import javafx.scene.layout.AnchorPane;
import java.net.URL;
import java.util.ResourceBundle;
public class Controller implements Initializable {
#FXML
private AnchorPane content;
#FXML
private TitledPane titledPane;
#FXML
private TableView<Object> tableView;
#Override
public void initialize(URL location, ResourceBundle resources) {
titledPane.prefHeightProperty().bind(content.heightProperty());
tableView.prefWidthProperty().bind(content.widthProperty());
tableView.getColumns().forEach(col -> col.setPrefWidth(300)); // to have enough "space" to scroll
tableView.setItems(FXCollections.observableArrayList(new Object()));
}
}
FXML:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="stackoverflow.testscroll.Controller"
fx:id="content">
<TitledPane fx:id="titledPane">
<TableView fx:id="tableView">
<columns>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
<TableColumn/>
</columns>
</TableView>
</TitledPane>
</AnchorPane>
Any idea how can I prevent the scroll of the tableview to reset every time I collapse the pane?
After a bit of digging, it looks like some layout optimization in VirtualFlow might be the reason (all seems to be fine if the scrolled content is not a TableView - not thoroughly analyzed, though)
What happens is:
during collapse, the TitledPane's content is resized vertically to 0
in VirtualFlow's layoutChildren a zero height/width is special cased to do nothing except hide everything, including the scrollBars
an internal listener to the scrollBar's visiblilty resets its value to 0
A tentative (read: dirty and might have unwanted side-effects, totally untested beyond this quick outline!) hack around is a custom TableViewSkin that tries to "remember" the last not-zero value and resets it on getting visible again.
An example:
public class TitledPaneTableScroll extends Application {
public static class TableViewScrollSkin<T> extends TableViewSkin<T> {
DoubleProperty hvalue = new SimpleDoubleProperty();
public TableViewScrollSkin(TableView<T> control) {
super(control);
installHBarTweak();
}
private void installHBarTweak() {
// Note: flow and bar could be legally retrieved via lookup
// protected api pre-fx9 and post-fx9
VirtualFlow<?> flow = getVirtualFlow();
// access scrollBar via reflection
// this is my personal reflective access utility method - use your own :)
ScrollBar bar = (ScrollBar) FXUtils
.invokeGetFieldValue(VirtualFlow.class, flow, "hbar");
bar.valueProperty().addListener((s, o, n) -> {
if (n.intValue() != 0) {
hvalue.set(n.doubleValue());
// debugging
// new RuntimeException("who is calling? \n").printStackTrace();
}
//LOG.info("hbar value: " + n + "visible? " + bar.isVisible());
});
bar.visibleProperty().addListener((s, o, n) -> {
if (n) {
bar.setValue(hvalue.get());
}
});
}
}
int counter;
private Parent createContent() {
TableView<Object> table = new TableView<>(FXCollections.observableArrayList(new Object()) ) {
#Override
protected Skin<?> createDefaultSkin() {
return new TableViewScrollSkin<>(this);
}
};
table.getColumns().addAll(Stream
.generate(TableColumn::new)
.limit(10)
.map(col -> {
col.setPrefWidth(50);
col.setText("" + counter++);
return col;
})
.collect(Collectors.toList()));
TitledPane titled = new TitledPane("title", table);
titled.setAnimated(true);
BorderPane content = new BorderPane(titled);
return content;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent(), 400, 400));
// stage.setTitle(FXUtils.version());
stage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TitledPaneTableScroll.class.getName());
}

Merge multiple FXML and have one controller for each file

The best article I found was: How to create multiple javafx controllers with different fxml files?
However im really confused on how this works. All examples just seem a bit too complex for the initial learning.
So here I have a simple helloWorld for testing purposes. As you can see in the xml, I have a container, menu and footer. However, I want all 3 of them to have seperate controllers and XML files which are then merged and shown as seen in the XML below after the class:
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class HelloWorld extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
FXMLLoader loader = new FXMLLoader();
Parent root = loader.setLocation(getClass().getResource("main.fxml"));
primaryStage.setScene(new Scene(root, 300, 250));
primaryStage.show();
MainController mainController = loader.getController();
}
}
XML
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import javafx.scene.canvas.*?>
<HBox fx:id="container" id="container" fx:controller="core.GuiController" xmlns:fx="http://javafx.com/fxml">
<HBox fx:id="post" id="post">
<!-- Stuff -->
</HBox>
<HBox fx:id="friends" id="friends">
<!-- Stuff -->
</HBox>
<HBox fx:id="profile" id="profile">
<!-- Stuff -->
</HBox>
</HBox>
I could really benefit from a simple example. How can I keep them in seperate files and merge them while they each retain their own controllers?
You could follow this tutorial
public class MainApp extends Application {
private Stage primaryStage;
private BorderPane rootLayout;
#Override
public void start(Stage primaryStage) {
this.primaryStage = primaryStage;
this.primaryStage.setTitle("AddressApp");
initRootLayout();
showPersonOverview();
}
/**
* Initializes the root layout.
*/
public void initRootLayout() {
try {
// Load root layout from fxml file.
FXMLLoader loader = new FXMLLoader();
loader.setLocation(MainApp.class.getResource("view/RootLayout.fxml"));
rootLayout = (BorderPane) loader.load();
// Show the scene containing the root layout.
Scene scene = new Scene(rootLayout);
primaryStage.setScene(scene);
primaryStage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Shows the person overview inside the root layout.
*/
public void showPersonOverview() {
try {
// Load person overview.
FXMLLoader loader = new FXMLLoader();
loader.setLocation(MainApp.class.getResource("view/PersonOverview.fxml"));
AnchorPane personOverview = (AnchorPane) loader.load();
// Set person overview into the center of root layout.
rootLayout.setCenter(personOverview);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Returns the main stage.
* #return
*/
public Stage getPrimaryStage() {
return primaryStage;
}
public static void main(String[] args) {
launch(args);
}
}
In this example You have two fxml files, RootLayout.fxml and PersonOverview.fxml.
You set the scene of your primarystage to (BorderPane)RootLayout.fxml then add PersonOverview.fxml to the BorderPane.

JavaFX BorderPane multi FXML space arrangement and transition effect

i want to design a GUI that contains BorderPane in the top of the container there is a MenuBar, in the left of the container there is a Acordion with different buttons that change the content of the center of the container for diferent FXML files, someting like the desktop app of Spotiffy
i got a working prototipe and it looks like this
the change in FXML happens and the buttos respont very well, the problem i have is that the FXML that fill the center part of the BoderPane doesn't auto rize and if is to big parts of the FXML doesn't show and if the FXML is smaller that the space left for the center part stays of the same small size and left alot of space with nothing
this is my code for the calling of the new FXML
public void lanzaUno(){
try {
// load first FXML
FXMLLoader loader = new FXMLLoader();
loader.setLocation(Coordinador.class.getResource(
"VistaControlador/Usuario/Uno.fxml"));
/*i put the AnchorPane inside of a
ScrollPane for the desire funcionality of alot
of vertical space for many nodes in a single FXML file
*/
ScrollPane unoAnchorPane = (ScrollPane) loader.load();
UnoController controller = loader.getController();
//example method for passing variables to the FXML controler
controller.pasoPrincipal(this, primaryStage, "Rhutt");
//puts the FXML in the center of the BorderPane
rootLayout.setCenter(unoAnchorPane);
//this if for trying to accommodate the content en the BorderPane
BorderPane.setAlignment(unoAnchorPane, Pos.TOP_LEFT);
} catch (IOException e) {
e.printStackTrace();
}
}
my first cuestion is what is the thing i am missing in the calling of the FXML for the contents of this ocupy the space available in the BorderPane?
my secon cuestion regards the change of FXML, when i pass fron one to another the change in the BorderPane is in a instant and looks very bad is there a way for making the transicion like one where the content of a FXML that is call push the content of the FXML in the center?, it doesn't have to be very elaborated just make the transition a little better
EDIT
i got a cordinator class where i send and recive parameters for all the FXML and where i declare the methods of calling new FXML, so i have a cordinator class, a FXML root with its controller and two FXML and its controllers with different things in each one, this two FXML are the ones that change in the BorderPane center of the root
this is the coordinator class
//Variables
private Stage primaryStage;
private BorderPane rootLayout;
/**
* launh
* #param primaryStage
*
*/
#Override
public void start(Stage primaryStage) throws Exception{
// Inicializa la escena
this.primaryStage = primaryStage;
this.primaryStage.setTitle("Login");
this.primaryStage.centerOnScreen();
//star method
iniLogin();
}
/**
*load the root scene
*/
public void iniLogin(){
try {
// Carga el loader.
FXMLLoader loader = new FXMLLoader();
loader.setLocation(com.aohys.rehabSys.MVC.Coordinador.class.getResource(
"VistaControlador/Pricipal/Principal.fxml"));
rootLayout = (BorderPane) loader.load();
//the root scene
Scene scene = new Scene(rootLayout);
primaryStage.setScene(scene);
// Da acceso al programa principal.
PrincipalController controller = loader.getController();
controller.pasoPrincipal(this, primaryStage);
primaryStage.centerOnScreen();
// Muesta la escena,
primaryStage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
after this method there are two identical methods like the one at the beginning where i call the 2 changing FXML call LanzaUno, LanzaDos
this is my rood FXML controler
public class PrincipalController implements Initializable {
//variable of the coordinator class
private Coordinador cordi;
private Stage stage;
/**
* method for passing parameters to the FXML
* #param cordi
* #param stage
*/
public void pasoPrincipal(Coordinador cordi, Stage stage) {
this.cordi = cordi;
this.stage = stage;
}
//FXML in root
#FXML private Button btt1;
#FXML private Button btt2;
#FXML public static StackPane stackPane;
/**
* Initializes the controller class.
*/
#Override
public void initialize(URL url, ResourceBundle rb) {
//Call the firts FXML
btt1.setOnAction((evento)->{
cordi.lanzaUno();
});
//Call the second FXML
btt2.setOnAction((evento)->{
cordi.lanzaDos();
});
}
for the moment the controllers on the two FXML dont do anything
You should read up on Adam Bien's AfterburnerFX library for managing dependency injection and controllers in your application. It has changed my life. :)
For transitions, I use this code. It is a nice fade-out/fade-in from one screen to the next. In this case, stackPane is a StackPane in the center of your BorderPane defined in your FXML.
Here is a simple FXML with the aforementioned stackPane:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.text.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<BorderPane fx:id="borderPane" minHeight="-Infinity" minWidth="-Infinity" prefHeight="768.0" prefWidth="1280.0" xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.tada.gui.tada.TadaPresenter">
<center>
<StackPane fx:id="stackPane" minHeight="-Infinity" minWidth="-Infinity" prefHeight="240.0" prefWidth="320.0" BorderPane.alignment="CENTER" />
</center>
</BorderPane>
And the setScreen method that changes to the new one passed in:
/**
* Set Screen
*
* #param view
* #return boolean
*/
public boolean setScreen(Parent view) {
final DoubleProperty opacity = stackPane.opacityProperty();
if (!stackPane.getChildren().isEmpty()) { //if there is more than one screen
Timeline fade = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 1.0)),
new KeyFrame(new Duration(TRANSITION_TIMER), new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent t) {
stackPane.getChildren().remove(0); //remove the displayed screen
stackPane.getChildren().add(0, view); //add the screen
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(TRANSITION_TIMER), new KeyValue(opacity, 1.0)));
fadeIn.play();
}
}, new KeyValue(opacity, 0.0)));
fade.play();
} else {
stackPane.setOpacity(0.0);
stackPane.getChildren().add(view); //no one else been displayed, then just show
Timeline fadeIn = new Timeline(
new KeyFrame(Duration.ZERO, new KeyValue(opacity, 0.0)),
new KeyFrame(new Duration(TRANSITION_TIMER), new KeyValue(opacity, 1.0)));
fadeIn.play();
}
return true;
}
You'll also need this in your controller...
private static final double TRANSITION_TIMER = 200;
EDIT:
I tried to put together a very basic "application". It is rudimentary, but I think it does a good job of illustrating the use of AfterburnerFX and the screen fade transition. There is a lot more to AfterburnerFX. I just used the view switching without the dependency injection, which is very important when you start wanting to work on objects in your application. Also, property binding is very important for a good UX. Anyway, Here is a link to my example on GitHub.

JavaFX loading a stylesheet

JavaFX has a method that is added to the controller:
public void initialize(URL url, ResourceBundle rb)
This seems to run before any of the controls are added to the scene, because when I add this to it:
#Override
public void initialize(URL url, ResourceBundle rb){
String treeItemCss = getClass().getResource("/media/css/TreeItem.css").getPath();
main.getScene().getStylesheets().add(treeItemCss);
}
The CSS:
.tree-cell{
-fx-indent: 100;
-fx-underline: true;
}
I get an error from this method: getStylesheets(). But if I move that to an OnAction and execute that action I get no errors.
So my question is, is there a method that runs after all the controls are added to the scene, or a good way to add css to items that are created from a user action, such as a button click?
The initialize() method runs at the end of the FXMLLoader's load() method. Since you don't get a reference to the root of the FXML until that completes, there's obviously no way you can add it to a scene until after then.
You can:
Add the css to the Scene in the application code. I.e. Somewhere you create an FXMLLoader, call load(), and add the result to the Scene. Just set the css file on the scene right there, or:
Add the css stylesheet to the root node instead of to the scene (assuming main is a Parent):
public void initialize() {
String treeItemCss = ... ;
main.getStylesheets().add(treeItemCss);
}
or:
Observe the Scene property and add the stylesheet when it changes to something not null:
public void initialize() {
String treeItemCss = ... ;
main.sceneProperty().addListener((obs, oldScene, newScene) -> {
if (newScene != null) {
newScene.getStylesheets().add(treeItemCss);
}
});
}
Update Here is a complete example to demonstrate the second option. Everything is in the "application" package:
Main.java:
package application;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
BorderPane root = FXMLLoader.load(getClass().getResource("Main.fxml"));
Scene scene = new Scene(root,400,400);
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
Main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.TreeView?>
<?import javafx.scene.control.TreeItem?>
<BorderPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.MainController" fx:id="root">
<center>
<TreeView>
<root>
<TreeItem value="Root">
<children>
<TreeItem value="One"/>
<TreeItem value="Two"/>
<TreeItem value="Three"/>
</children>
</TreeItem>
</root>
</TreeView>
</center>
</BorderPane>
MainController.java:
package application;
import javafx.fxml.FXML;
import javafx.scene.layout.BorderPane;
public class MainController {
#FXML
private BorderPane root ;
public void initialize() {
root.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
}
}
application.css:
.tree-cell{
-fx-indent: 100;
-fx-underline: true;
}
Note that you can add the stylesheet directly in the FXML file with
<BorderPane xmlns:fx="..." fx:controller="..." stylesheets="#application.css">
and then omit it completely from the controller logic.

Categories