0

I want a Button that allows me to switch between Dark/Light-mode. But I have the problem If I switch, the active windows will not change their Style.

First of the Code snippets to easy recreate the problem. For Maven projects starter class:

package testapp;

//This Class is Required in mavenproject to Start the Application

public class GUIStarter {
    public static void main(final String[] args) {
        try{
            TestApp.main(args);
        }catch (Exception e){
            System.out.println("GUIStarter Errror:\n"+e);
        }
    }
}

The primary Window class:

public class TestApp extends Application {
    //stylepaths
    public static String mainLightModePath = ".\\style_lightmode.css";
    public static String mainDarkModePath = ".\\style-Darkmode.css";
    public static boolean isItDarkmode = true;

    public static void toggleMode(){
        if(isItDarkmode){
            isItDarkmode  = false;
        }else if(!isItDarkmode){
            isItDarkmode = true;
        }

    }

    //This is required to get the primary Stage in other Stages (Controllers)
    private static Stage pStage;

    public static Stage getPrimaryStage() {
        return pStage;
    }

    private void setPrimaryStage(Stage pStage) {
        TestApp.pStage = pStage;
    }

    public void setPrimaryWindow(Stage primaryStage){

        try{
            FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("Mainframe.fxml"));
            
            Parent root = (Parent) fxmlLoader.load();
            Scene scene = new Scene(root);

            primaryStage.setTitle("TestApp");

            //scene.setMoveControl(titleBar);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch (Exception e){
            e.printStackTrace();
        }
        
    }

    //Start of Application
    @Override
    public void start(Stage primaryStage) {
        setPrimaryStage(primaryStage);
        pStage = primaryStage;
        setPrimaryWindow(primaryStage);
    }

    public static void main(String[] args) {
        launch(args);
    }
}

The maincontroller Class:

public class Mainframe {
        public static Timeline time;

        @FXML
        public void options(ActionEvent event){
            try{ 
                TestApp.toggleMode();
                Stage stage = (Stage) ((Node)event.getSource()).getScene().getWindow();
                Stage changer = TestApp.getPrimaryStage();
                stage.close();
                changer.hide();
                //My idea was to initialize again and load the styles in that way again but does not change anything
                FXMLLoader loader = new FXMLLoader(getClass().getResource("Mainframe.fxml"));
                loader.load();
                Mainframe ctrl = loader.getController();
                ctrl.initialize();
                changer.show();
                
                
            }catch (Exception e) {
                System.out.println("Error bei der App Klicken von ModeButton. \n error is: "+e);
            e.printStackTrace();
            }
        }

    @FXML
    public BorderPane primaryparent;

    //get and set for elements
    @FXML
    void initialize() {
        //Check Theme
        if(TestApp.isItDarkmode){
            primaryparent.getStylesheets().clear();
            primaryparent.getStylesheets().add(getClass().getResource(TestApp.mainDarkModePath).toString());
        }else if(!TestApp.isItDarkmode){
            primaryparent.getStylesheets().clear();
            primaryparent.getStylesheets().add(getClass().getResource(TestApp.mainLightModePath).toString());
        }
    }
    
}

Mainframe.fxml:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.text.Font?>

<BorderPane fx:id="primaryparent" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="0.0" minWidth="0.0" prefHeight="224.0" prefWidth="515.0" xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="testapp.Mainframe">
   <center>
      <AnchorPane prefHeight="200.0" prefWidth="200.0" styleClass="rootBackground" BorderPane.alignment="CENTER">
         <children>
            <Button fx:id="setDarkModeButton" layoutX="175.0" layoutY="99.0" mnemonicParsing="false" onAction="#options" styleClass="settingsButton" text="toggle Dark/LightMode">
               <font>
                  <Font name="Arial" size="13.0" />
               </font>
               <padding>
                  <Insets bottom="5.0" left="17.0" right="17.0" top="5.0" />
               </padding>
            </Button>
         </children>
      </AnchorPane>
   </center>
</BorderPane>


Before I just overwrote the "style.css" file and used .stop() & .show(). This worked fine. But it can get complicated the more css-files you have and after it is packed into a .jar file it doesn't work anymore.

I hope anyone can help me. Because now I have really no Idea...

UPDATE: I want to control it from another controller:

package testapp;
public class Mainframe {
        public static Timeline time;

        @FXML Button setDarkModeButton;

        @FXML
        public void options(ActionEvent event){
            try{ 
                try{
                    Stage secSTAGE = new Stage();
                    FXMLLoader fxmlLoader = new FXMLLoader(TestApp.class.getResource("Mainframe2.fxml"));
                    Parent root = (Parent) fxmlLoader.load();
                    Scene scene = new Scene(root);
        
                    secSTAGE.setTitle("TestAppw2");
        
                    //scene.setMoveControl(titleBar);
                    secSTAGE.setScene(scene);
                    secSTAGE.show();
                } catch (Exception e){
                    e.printStackTrace();
                }
                
                
            }catch (Exception e) {
                System.out.println("Error bei der App Klicken von ModeButton. \n error is: "+e);
            e.printStackTrace();
            }
        }

    @FXML
    public BorderPane primaryparent;

    //get and set for elements
    @FXML
    void initialize() {
        //Check Theme
        if(TestApp.isItDarkmode){
            primaryparent.getStylesheets().clear();
            primaryparent.getStylesheets().add(getClass().getResource(TestApp.mainDarkModePath).toString());
            primaryparent.getStylesheets().add(getClass().getResource(TestApp.mainLightModePath).toString());
        }
    }
    
}

public class Mainframe2 {
        public static Timeline time;

        @FXML Button setDarkModeButton;

        @FXML
        public void options(ActionEvent event){
            try{ 
                TestApp.toggleMode();
                Stage stage = (Stage) ((Node)event.getSource()).getScene().getWindow();

                FXMLLoader loader = new FXMLLoader(getClass().getResource("Mainframe.fxml"));
                Parent root = (Parent) loader.load();
                Mainframe ctrl = loader.getController();

                //setDarkModeButton.getStylesheets().set(0, "style_lightmode.css");
                if(TestApp.isItDarkmode){
                    root.getStylesheets().set(0, Mainframe.class.getResource(TestApp.mainDarkModePath).toString());
                }else{
                    root.getStylesheets().set(0, Mainframe.class.getResource(TestApp.mainLightModePath).toString());
                }            
            }catch (Exception e) {
                System.out.println("Error bei der App Klicken von ModeButton. \n error is: "+e);
            e.printStackTrace();
            }
        }

    @FXML
    public BorderPane primaryparent;

    //get and set for elements
    @FXML
    void initialize() {
        //Check Theme
        if(TestApp.isItDarkmode){
            primaryparent.getStylesheets().clear();
            primaryparent.getStylesheets().add(getClass().getResource(TestApp.mainDarkModePath).toString());

        }else if(!TestApp.isItDarkmode){
            primaryparent.getStylesheets().clear();
            primaryparent.getStylesheets().add(getClass().getResource(TestApp.mainLightModePath).toString());
        }
    }
    
}

Muddyblack k
  • 314
  • 3
  • 16
  • 2
    There are many strange things happening in these code snippets. You are exiting the platform then trying to do stuff with JavaFX -> you can't do that. You are closing a stage and trying to start a new Application -> you can't do that. You are trying to init style on a stage after it is visible -> you can't do that. – jewelsea May 06 '22 at 09:13
  • 1
    See: [some info on applying themes via css in JavaFX](https://stackoverflow.com/questions/58087164/javafx-dynamically-colored-window-with-css/58107552#58107552). – jewelsea May 06 '22 at 09:13
  • 1
    Also, maybe all you need to do is set `-fx-base` on the root to the color you want, try that, it may not be exactly what you want, but is much easier. – jewelsea May 06 '22 at 09:15
  • 2
    You have also named a class Application (`app.Application`). Don't do that, it is super confusing. JavaFX already has an [Application class](https://openjfx.io/javadoc/17/javafx.graphics/javafx/application/Application.html). Read the Javadoc for it to understand how the lifecycle works and what its usage rules are. – jewelsea May 06 '22 at 09:23
  • @jewelsea I renamed my Classes for stackoverlow to reduce it. So I made small snippets to all files that are needed to recreate the problem as I need it. Hope you are able to help me now :) – Muddyblack k May 06 '22 at 12:03
  • please read the referenced help page and act accordingly - mind the __M__ .. that looks like far too much code (alwaysontop is unrelated to styling and most of the style rules are not necessary) and still missing the stacktrace .. unrelated: don't use static scope .. – kleopatra May 06 '22 at 12:45
  • .. or don't you get any errors? If so, is it real or just because you swallow them in the runlater? – kleopatra May 06 '22 at 12:51
  • No this does not create any error. I removed now all that I have tried to get it work... I have no idea how to do it. – Muddyblack k May 06 '22 at 17:34
  • Okay added one of the things I have tried out: ```initialize()``` . Creates no error but also has no effect. So but now I really hope you guys are able to help me... – Muddyblack k May 06 '22 at 17:43
  • 1
    I'm a little confused with your `Mainframe2` class. In the `options` method, you load an FXML file and add the stylesheet to the root `Node`. But you never add that `Node` to the scene graph, so it is not displayed to the user. Thus, no visual effects. If you want to change the style of all nodes currently displayed in a scene, either modify the stylesheets list of the `Scene` or the `root` of the `Scene`. – Slaw May 12 '22 at 22:25
  • THANK YOU!!! I forgot ```changer.setScene(scene)```. Man this was struggle.... – Muddyblack k May 13 '22 at 15:44

2 Answers2

3

Css file can be changed on the run without reloading or setting a new stage

replacing the styleSheet with set() method for observableList

css file javafx

App.java

public class App extends Application {

    @Override
    public void start(Stage stage) {
        
        Button button = new Button("change");
        button.setOnAction(e ->button.getScene().getStylesheets().set(0, "1.css"));
        
        Button button1 = new Button("default");
        button1.setOnAction(e ->button.getScene().getStylesheets().set(0, "0.css"));
        HBox hBox = new HBox(button,button1);
        
        
        AnchorPane anchorPane =new AnchorPane(hBox);
        anchorPane.getStyleClass().add("pane");
        
        var scene = new Scene(anchorPane, 640, 480);
        scene.getStylesheets().add("0.css");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }

}

0.css

.pane{
    -fx-background-color:cyan;
    
}

1.css

.pane{
    -fx-background-color:orange;
    
    }
Giovanni Contreras
  • 2,345
  • 1
  • 13
  • 22
  • Hm when I add it to my code I get this error: ```java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0``` Do you know to do it with fxml files? – Muddyblack k May 12 '22 at 13:21
  • In my ```Main Controller``` I used this on ```ActionEvent```: ```setDarkModeButton.getScene().getStylesheets().set(0, ".\\style_lightmode.css");``` – Muddyblack k May 12 '22 at 13:23
  • 2
    @Muddyblackk There has to be at least one element in the list for `set(0, element)` to work. You can see `scene.getStylesheets().add("0.css")` is called in the above example before any of the `set(...)` calls. Also, when you add a stylesheet without a URL scheme then it assumes the path is a resource name; resource lookup does not support `.` and `..`. – Slaw May 12 '22 at 19:40
  • Yeah I added them in the ```initialize()``` but idk why i put ````.getScene()``` in it ^^ Thank you it did work! After I implemented it properly in my real application I will mark it as answer :) – Muddyblack k May 12 '22 at 21:02
  • Okay It does work for the newest opened window but how can I apply it on another active window? The others said I had useless code so it is not in my question anymore... ```ctrl.primaryparent.getStylesheets().set(0, getClass().getResource(FirstController.mainDarkModePath).toString());``` So I want to use it in another controller. ctrl loads the other controller like in my qestion above. – Muddyblack k May 12 '22 at 21:29
  • This code above does nothing. Even no Error – Muddyblack k May 12 '22 at 21:32
  • you need to know how to pass values between stages – Giovanni Contreras May 12 '22 at 22:32
  • I post this just to point : "You can close the old stage and create a new one with the new Theme-style." is not necessary reload or load a new stage in order to change its Style – Giovanni Contreras May 12 '22 at 22:55
  • Yeah ```ctrl.primaryparent``` is the value from the other stage but it does not update the style. – Muddyblack k May 13 '22 at 15:08
  • 1
    Okay @slaw pointed my problem out :) Thank you guys for your patience with me. I really struggled this time... – Muddyblack k May 13 '22 at 15:45
  • if you master how to pass values between controllers you can solve many tasks like this – Giovanni Contreras May 13 '22 at 15:53
0

Seen that kind of nobody is interested in my question. It is not what I wanted but as long as nobody has a better solution:

You can close the old stage and create a new one with the new Theme-style.

So in the TestApp.class the setPrimaryWindow() method must be set public and static. So you can't use .getClass() and the pStage must be set to the new Stage in the setPrimaryWindow():

public static void setPrimaryWindow(Stage primaryStage){

    try{
        pStage = primaryStage;
        FXMLLoader fxmlLoader = new FXMLLoader(TestApp.class.getResource("Mainframe.fxml"));
        
        Parent root = (Parent) fxmlLoader.load();
        Scene scene = new Scene(root);

        primaryStage.setTitle("TestApp");

        //scene.setMoveControl(titleBar);
        primaryStage.setScene(scene);
        primaryStage.show();
    } catch (Exception e){
        e.printStackTrace();
    }
    
}

@Override
public void start(Stage primaryStage) {
    setPrimaryStage(primaryStage);
    setPrimaryWindow(primaryStage);
}

In the options maincontroller.class you just close() the old window and call setPrimaryWindow():

@FXML
public void options(ActionEvent event){
    try{ 
        TestApp.toggleMode();
        Stage stage = (Stage) ((Node)event.getSource()).getScene().getWindow();
        Stage changer = TestApp.getPrimaryStage();
        stage.close();
        changer.close();

        //create new Window on Theme switch
        Stage nStage = new Stage();
        TestApp.setPrimaryWindow(nStage);
        
        
    }catch (Exception e) {
        System.out.println("Error bei der App Klicken von ModeButton. \n error is: "+e);
    e.printStackTrace();
    }
}
Muddyblack k
  • 314
  • 3
  • 16