14 Replies Latest reply: Aug 7, 2013 8:40 AM by user13665758 RSS

    How to update stylesheets at runtime?

    user13665758

      Strangely the style does'nt change if i manually change the css class rules in one of the css document and switch between the 2 files.

       

        @Override

        public void initialize(URL url, ResourceBundle rb) {

          final String cssUrl1 = getClass().getResource("/tracker/view/fxmlgui1.css").toExternalForm();

          final String cssUrl2 = getClass().getResource("/tracker/view/fxmlgui2.css").toExternalForm();

          rootPane.getStylesheets().add(cssUrl1);

          rootPane.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {

            @Override

            public void handle(KeyEvent keyEvent) {

              if (keyEvent.getCode().equals(KeyCode.DIGIT1)) {

                rootPane.getStylesheets().clear();

                rootPane.getStylesheets().add(cssUrl1);

                msgLabel.setText("Css1 selected.");

       

       

              } else if (keyEvent.getCode().equals(KeyCode.DIGIT2)) {

                rootPane.getStylesheets().clear();

                rootPane.getStylesheets().add(cssUrl2);

                msgLabel.setText("Css2 selected.");

       

       

              }

            }

          });

        }

       

      The event occurs and the style change from css1 and css2 as they was at the time of stage rendering, but if then i change one of the rules in the sylesheets after the stage is layed out, the style does'nt change anymore.

      So wich is the general way to change the style of nodes changing the css document at runtime?

      It's as the initial stylesheets in the css documents are someway cached and does'nt update at any switch.

        • 1. Re: How to update stylesheets at runtime?
          James_D

          The following (plain Java, but that shouldn't matter) works for me:

           

          import javafx.application.Application;
          import javafx.beans.value.ChangeListener;
          import javafx.beans.value.ObservableValue;
          import javafx.scene.Scene;
          import javafx.scene.control.ComboBox;
          import javafx.scene.control.Label;
          import javafx.scene.layout.VBox;
          import javafx.stage.Stage;
          
          
          public class CssSwitcher extends Application {
          
          
            @Override
            public void start(Stage primaryStage) {
            final ComboBox<String> combo = new ComboBox<String>();
            combo.getItems().addAll("css1.css", "css2.css");
            final Label msgLabel = new Label();
            msgLabel.textProperty().bind(combo.getSelectionModel().selectedItemProperty());
          
            final VBox root = new VBox(5);
            combo.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<String>() {
                      @Override
                      public void changed(ObservableValue<? extends String> observable,
                              String oldValue, String newValue) {
                          root.getStylesheets().clear();
                          root.getStylesheets().add(getClass().getResource(newValue).toExternalForm());
                      }
            });
          
            root.getChildren().addAll(combo, msgLabel);
            primaryStage.setScene(new Scene(root, 300, 150));
            primaryStage.show();
            }
          
          
            public static void main(String[] args) {
            launch(args);
            }
          }
          

           

          css1.css:

          @CHARSET "UTF-8";
          .label {
           -fx-text-fill: red ;
          }
          

           

          css2.css:

          @CHARSET "UTF-8";
          .label {
           -fx-text-fill: blue ;
          }
          

           

          Which JavaFX version are you using? I remember there were some bugs with applying css to Parents (i.e. not directly to Scenes); I think these are fixed in the latest ea release of JavaFX 8, but I don't know the extent (if any) to which they were backported to JavaFX 2.2. You might try working directly with the scene, which you can do by replacing all occurrences of

          rootPane.getStylesheets()

          with

          rootPane.getScene().getStylesheets()

           

          It also might be worth checking if your current code runs in JavaFX 8.

          • 2. Re: How to update stylesheets at runtime?
            user13665758

            Do you think i can use jdk8 as a stable jvm or it's yet for testing only as it's an early release?

            I'm usng the jdk build 1.7.0_25-b17 and the default javafx that comes with this last version of the jdk(i think 2.2).

            However i'm tryng to set the stylesheets from the controller rather than from the Application class, to strictly follow the mvc pattern, but i don't succeed in obtaining a stage reference from the controller.

            I tryed using:

             

              @FXML

              private Label msgLabel;

             

              @Override

              public void initialize(URL url, ResourceBundle rb) {

                Stage stage=(Stage)rootPane.getScene().getWindow();

            ..

             

            from the controller class, but i receive a NullPointerException and the application doesn't start at all.

            If i would have a scene reference i could use:

             

               sceneReference.getStylesheets().add("cssDoc.css");

             

            So the problem is yet: how to obtain a scene or stage reference from the controller class without receive a NullPointerException?

             

            • 3. Re: How to update stylesheets at runtime?
              James_D

              The controller's initialize() method is invoked during the call to FXMLLoader.load(...). Your application almost certainly has a structure like this:

               

              Parent root = FXMLLoader.load(...);
              Scene scene = new Scene(root);
              stage.setScene(scene);
              

               

              So at the point the initialize(...) method is called the root pane hasn't actually been attached to a Scene; thus the call to getScene() returns null.

               

              However, when your handler methods are called, the root will have been attached to a Scene and the Scene placed in a Stage, so at that point it is safe to call getScene() and getWindow() on the result.

              I would actually manipulate the stylesheets on the Scene, rather than the Stage, so the call to getWindow() is probably not needed.

               

              So I would try:

               

                @Override
                public void initialize(URL url, ResourceBundle rb) {
                  final String cssUrl1 = getClass().getResource("/tracker/view/fxmlgui1.css").toExternalForm();
                  final String cssUrl2 = getClass().getResource("/tracker/view/fxmlgui2.css").toExternalForm();
              
              
                  rootPane.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
                    @Override
                    public void handle(KeyEvent keyEvent) {
                      if (keyEvent.getCode().equals(KeyCode.DIGIT1)) {
                        rootPane.getScene().getStylesheets().clear();
                        rootPane.getScene().getStylesheets().add(cssUrl1);
                        msgLabel.setText("Css1 selected.");
              
              
                      } else if (keyEvent.getCode().equals(KeyCode.DIGIT2)) {
                        rootPane.getScene().getStylesheets().clear();
                        rootPane.getScene().getStylesheets().add(cssUrl2);
                        msgLabel.setText("Css2 selected.");
              
              
                      }
                    }
                  });
                }
              

               

              The only issue now is that the initial stylesheet isn't set on the Scene. If you really want to do this from the controller, you need to listen for changes to the sceneProperty on the root pane. This gets a little ugly, but you can do something like this in your initialize method:

               

              rootPane.sceneProperty().addListener(new ChangeListener<Scene>() {
                   @Override
                   public void changed(ObservableVale<? extends Scene> observable, Scene oldScene, Scene newScene) {
                        if (newScene != null) {
                             newScene.getStylesheets().add(cssUrl1);
                             rootPane.sceneProperty().removeListener(this);
                        }
                   }
              });
              

               

              If you wanted to hold a reference to the Scene in the controller, you could use this same ChangeListener to do so, but there's no real benefit to doing that and the code is less readable, imo.

               

              Do you think i can use jdk8 as a stable jvm or it's yet for testing only as it's an early release?


              It depends to a certain extent what you're doing, but generally I would recommend using it for testing only. I was just suggesting trying your code against it to see if the issues were caused by a bug that had been fixed, which would rule out the possibility that there was something still wrong in your code that we weren't seeing. I can see two use cases for using JavaFX 8 right now other than just for testing. One (which I'm actually doing in one project right now) is if you have a release date suitably far ahead (ours is summer 2014) that you can reasonably expect JDK8 to be in a stable public release at that time. (You'd also probably want either to be able to control the JRE version for your client machines or be planning on a "native bundle" release.) The other is if you had a non-critical application that was going to be deployed in-house for a very limited number of users, and you wanted to take advantage of some new JDK/JavaFX 8 features.

              • 4. Re: How to update stylesheets at runtime?
                user13665758

                Your response is very usefull!

                I don't understand only why you write this approach is "ugly".

                If i want to set the initial stage width/height or stylesheets and so on, is there a more correct way that is less ugly to do all those initial stage layout things according to mvc?

                Pheraps is more correct to set these initial stage things in the class that extends Application and wich already has a Stage reference as formal parameter in the start method?

                 

                Thanks!

                • 5. Re: How to update stylesheets at runtime?
                  James_D

                  Your response is very usefull!

                  I don't understand only why you write this approach is "ugly".

                  It feels to me that this is a bit of a misuse of a listener. I think of the purpose of attaching a listener to a property as being a mechanism for repeatedly performing some updates when a property changes: the implication being that this is a "dynamic" property whose values are going to change throughout the lifetime of the application. In this case, I simply want to perform an action on a value which hasn't been initialized yet, so I'm saying "once this has been initialized, do this one time". Note how the listener removes itself once it's done its job, which is kind of weird. You probably don't need this in this case, but sometimes these "one-shot" listeners can create memory leaks if you don't carefully clean up like this.

                   

                  Of course, the real way to do this is the way you tried first; manipulating the stylesheets on the root pane itself. That *should* work. I'm curious to know if that approach worked in JavaFX 8, and/or if the approach where you changed stylesheets on the Scene worked in the current version.

                  If i want to set the initial stage width/height or stylesheets and so on, is there a more correct way that is less ugly to do all those initial stage layout things according to mvc?

                  Pheraps is more correct to set these initial stage things in the class that extends Application and wich already has a Stage reference as formal parameter in the start method?

                  Yes, I think the Application code is the correct place to do this. Keeping the root element of your FXML as a Pane subclass is much more reusable. The View (FXML) simply defines what the view of your data looks like. The controller defines the logic for processing user input and updating the view. What you do with the view (typically put it in a Scene and display the Scene in a Stage) is up to the application. For example, you might start with a simple application, then decide that you want all of that functionality to be part of a bigger application (maybe as a Tab in a TabPane, for example). You can simply reuse your existing code. If the root element of your FXML is a Scene (which is possible), then the only thing you can do with it is set it in a Stage, so more refactoring would be required to make those changes.

                  • 6. Re: How to update stylesheets at runtime?
                    user13665758

                    Incredibly exhaustive!

                    I love this detailed approach in explaining and describing things.

                    Thanks a lot.

                    However i need a way to update the style of nodes in the scene at runtime without close and restart the application.

                    Pheraps i didn't explain well before, but i need a way that allows me to change the css rules values at runtime maybe pressing a button or a key.

                    But unfortunately in both ways i implement the listener i can only switch between 2 styles defined in the 2 css files, but if i change/save one of them manually while the stage is running and i fire the event switching to the css file just modifyed, the nodes in the scene don't change their style.

                    In fact i don't need 2 css documents at all.

                    I only need to change the scene nodes style with the stage running.

                    If there is a way to do this using only one css document and modifying always the same one, i could prefer this method.

                    Someghing like:

                     

                        rootPane.addEventHandler(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {

                          @Override

                          public void handle(KeyEvent keyEvent) {

                            if (keyEvent.getCode().equals(KeyCode.DIGIT1)) {

                              rootPane.getStylesheets().clear();

                              rootPane.getStylesheets().add(cssUrl);

                              msgLabel.setText("Css saved.");

                              keyEvent.consume();

                            }

                          }

                        });

                     

                    So the question is always the same: is there a way to real update the stylesheets of a scene while it is running?

                    Thanks as usual!

                     

                    • 7. Re: How to update stylesheets at runtime?
                      James_D

                      Is the aim to be able to edit the css file, then press a key or hit a "reload style" button on the UI and see the updated styles? I don't think this is really the purpose of css styling: the intention really is that the css file will be bundled in your application jar file and never even extracted, never mind edited and reloaded. (It can even be converted to a binary form which is quicker to load.)

                       

                      That said, however, the following works just fine for me. (JDK 1.7.0_25, JavaFX 2.2.25). Run the application, you see the label in blue. Edit the css and save it (without exiting the application), press the button to reload the style, and you see any changes that were made take effect. Does that not work for you?

                       

                       

                      import javafx.application.Application;
                      import javafx.event.ActionEvent;
                      import javafx.event.EventHandler;
                      import javafx.scene.Parent;
                      import javafx.scene.Scene;
                      import javafx.scene.control.Button;
                      import javafx.scene.control.Label;
                      import javafx.scene.layout.VBox;
                      import javafx.stage.Stage;
                      
                      public class CssReloader extends Application {
                      
                       @Override
                        public void start(Stage primaryStage) {
                        final VBox root = new VBox(5);
                        final Button reloadButton = new Button("Reload style");
                        reloadButton.setOnAction(new EventHandler<ActionEvent>() {
                                  @Override
                                  public void handle(ActionEvent event) {
                                      loadStyle(root);
                                  }
                              });
                        final Label label = new Label("A message");
                        root.getChildren().addAll(reloadButton, label);
                        loadStyle(root);
                        primaryStage.setScene(new Scene(root, 300, 150));
                        primaryStage.show();
                        }
                      
                        private void loadStyle(Parent node) {
                            node.getStylesheets().clear();
                            node.getStylesheets().add(getClass().getResource("editMe.css").toExternalForm());
                        }
                      
                        public static void main(String[] args) {
                        launch(args);
                        }
                      }
                      
                      

                       

                      editMe.css:

                      
                      @CHARSET "UTF-8";
                      .label {
                       -fx-text-fill: blue ;
                      }
                      
                      
                      

                       

                      Update: Of course, this code won't work if you bundle this as a jar file and run it from there. getClass().getResource(name) produces a URL representing a resource loaded from the same place as the current class (in this case the jar file), and toExternalForm() will convert that to a String. Obviously editing a css file won't change anything in the bundled jar file. (Try displaying it as follows. Running from a file system class loader you'll get a file:// url; running from a jar class loader you'll get a jar:// class loader.)

                       

                      private void loadStyle(Parent node) {
                            node.getStylesheets().clear();
                           String resource = getClass().getResource("editMe.css").toExternalForm() ;
                           System.out.println(resource);
                            node.getStylesheets().add(resource);
                        }
                      

                       

                      To force a file system URL, even if you're running from a jar file, you can do this:

                       

                      privatevoid loadStyle(Parent node) {
                            node.getStylesheets().clear();
                            try {
                                  final String resource = Paths.get("/path/to/editMe.css").toUri().toURL().toExternalForm();
                                  System.out.println(resource);
                                  node.getStylesheets().add(resource);
                              } catch (MalformedURLException e) {
                                  e.printStackTrace();
                              }
                        }
                      

                       

                      Now of course you have something of a deployment headache; you have to somehow ensure there is a css file in the correct location - outside of the jar package - as part of the deployment of your application. You could check for its existence on startup, and if it's not there read a default version from your jar file and write it to the location in which you'd expect to find it; or find some other strategy.

                       

                      Message was edited by: James_D

                      • 8. Re: How to update stylesheets at runtime?
                        user13665758

                        Ooooh finally!!

                        I've understood why it was impossible to update sylesheets at runtime thanks to your last post.

                        Because the

                         

                        getClass().getResource("path")

                         

                        refers to the relative location inside the jar and if i modify the css file i see outside of the /dist folder(in netbeans) i really don't modify the css file used while the app is running, obviously.

                        I was going out crazy for this stupid problem.

                        However, with the javafx script old release i developed a gui for one of the apps produced in the company i work for and it was a great functionaity the possibility to modify the style on the fly.

                        This allowed anyone inside the company to modify the style of the gui according to the customer layout preferences, without boring me developer, busy on other works.

                        However it's a simple work to decompress the application jar, modify the css and repack it, but it will never be as convenient as modify style on the fly for customization pourpouses.

                        Many thanks!

                         

                         

                        • 9. Re: How to update stylesheets at runtime?
                          jsmith

                          > However it's a simple work to decompress the application jar, modify the css and repack it, but it will never be as convenient as modify style on the fly for customization


                          You don't have to load the css out of a jar, you can load it off a file system. 


                          You can use a FileWatcher to watch a certain location (e.g. ~/user/.myapp.css) and if the file there changes, reload it from that location.


                          You can even apply multiple stylesheets to your scene, so you could ship a default css in a jar and allow the user to add their own preference one to their home directory if they wanted and the user specific stylesheet only needs to specify a few desired user specific changes to the defaults and the rest of your defaults will still apply.

                          • 10. Re: How to update stylesheets at runtime?
                            user13665758

                            Oh thanks for the info about FileWatcher!

                            I will try it now.

                            • 11. Re: How to update stylesheets at runtime?
                              jsmith

                              > Oh thanks for the info about FileWatcher! I will try it now.

                               

                              There is nice documentation and demonstrations of FileWatcher usage in the Java Tutorials.

                               

                              Be careful of threading.

                               

                              For example make sure that you don't directly update the scene graph or load modify the active css style sheet rules in the file watcher callbacks, but instead use JavaFX concurrency tools such as Platform.runLater to wrap the css style sheet updates so that you don't run into concurrency issues.

                               

                              The system property for getting the user's home directory is: "user.home".

                               

                              If you create a nice standalone solution for this, please post back the executable code in the thread or a gist.

                               

                              Thanks!

                              • 12. Re: How to update stylesheets at runtime?
                                user13665758

                                Ok, i will try.

                                Why have you written about the user's home dir system property?

                                • 13. Re: How to update stylesheets at runtime?
                                  jsmith

                                  > Why have you written about the user's home dir system property?

                                   

                                  The user specific css style sheet that was previously discussed can't be stored in the jar, but will be loaded from the filesystem.

                                  It made sense to me that the style sheet file would be located in the user directory (hence the pointer on how to determine the user directory).

                                  However, it was just a pointer, you can place the custom stylesheet files used by your application wherever you want and have access to on the file system.

                                  • 14. Re: How to update stylesheets at runtime?
                                    user13665758

                                    Sure!

                                    But in general i use a path relative to the application jar.

                                    Ie: ApplicationRoot

                                                   |

                                                   |

                                                   --Css

                                                        |

                                                        --cssFile.css

                                     

                                    and i reference it using:

                                     

                                    Paths.get("css/cssFile.css").toUri().toURL().toExternalForm();

                                     

                                    without the leading slash, so the jvm accepts it as a relative path.

                                    Obviously using the user.dir is a valid option too and in a release stage it is the best one.

                                    But i'm mainly in a developing stage and for me is more convenient having all the files in the same root dir of the application.