Martinocom
Martinocom

Reputation: 330

JavaFX - simple custom minimal window implementation

I just discovered JavaFX and I really liked it. I hate java-default GUI, so I immediatly decided to personalize my window. I had numerous tries, but I have one big limitation and one big objective; limitation? I must use MVC pattern. Objective? Make the custom window reusable.

So... This is the point I have now: wstaw.org/m/2016/04/07/resoruces.png

I made a general package application that contains App.java, that will launch the app. Then I make another internal package, containin the "MinimalWindow" logic, with all resoruces I need.

I implemented this FXML code to perform the window:

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

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.control.Label?>

<StackPane fx:id="minimalWindowShadowContainer" id="minimalWindowShadowContainer" stylesheets="@style.css" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" onMousePressed="#updateXY" onMouseDragged="#windowDragging" onMouseReleased="#updateStatus" >
    <BorderPane fx:id="minimalWindowContainer" id="minimalWindowContainer">
        <!-- This padding will create the dropshadow effect for the window behind -->
        <padding>
            <Insets top="5" right="5" bottom="5" left="5"/>
        </padding>

        <!-- "Title Bar" -->
        <top>
            <HBox id="titleBar" alignment="CENTER" spacing="5" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="30.0" prefWidth="600.0">
                <padding>
                    <Insets top="5" right="5" bottom="5" left="5"/>
                </padding> 

                <ImageView fx:id="logo" fitWidth="20" fitHeight="20"></ImageView> 
                <Label fx:id="lblTitle" id="title" text="MinimalWindow"></Label>
                <Region HBox.hgrow="ALWAYS" prefHeight="30.0" prefWidth="200.0"></Region>

                <HBox alignment="CENTER_RIGHT">
                    <Button id="btnMin" onMouseClicked="#minimizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
                    <Button fx:id="btnMax" id="btnMax" onMouseClicked="#maximizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
                    <Button id="btnCls" onMouseClicked="#closeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
                </HBox>
            </HBox>
        </top>

        <!-- The content of the window will go here -->
        <center>
            <StackPane fx:id="contentArea" id="contentArea"></StackPane>
        </center>

        <!-- Footer -->
        <bottom>
            <HBox id="footer">
                <padding>
                    <Insets top="5" right="5" bottom="5" left="5"/>
                </padding> 

                <Button fx:id="btnResize" id="btnResize" alignment="BOTTOM_RIGHT" onMouseClicked="#updateXY" onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow" minHeight="10" minWidth="10" maxHeight="10" maxWidth="10"></Button>      
            </HBox>
        </bottom>
    </BorderPane>
</StackPane>

I implemented then the controller class:

package application.minimalWindow;


import javafx.application.Application;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class MinimalWindow extends Application {

    @FXML
    Label lblTitle;

    @FXML
    Button btnMax, btnResize;

    @FXML
    StackPane minimalWindowShadowContainer, minimalWindowContainer,contentArea;

    @FXML
    Double SHADOW_SPACE;

    final private static int MIN_WIDTH = 730, MIN_HEIGHT = 500;


    private double actualX, actualY;
    private boolean isMovable;
    private String source, title;

    private Stage mainStage;

    //
    // Public logic of the class
    //

    public MinimalWindow() {
        //TODO must work...
    }


    //Show the window
    public void show() {
        mainStage.show();
    }


    //
    // MIMIZIE | MAXIMIZE | CLOSE 
    //

    //When pressed, will minimize the window to tray
    @FXML
    private void minimizeApp(MouseEvent e) {
        mainStage.setIconified(true);
    }

    //When pressed, check if it must maximize or restore the window
    @FXML
    private void maximizeApp(MouseEvent e) {
        if (mainStage.isMaximized()) {
            setMin();
            isMovable = true;
        }

        else {
            setMax();
            isMovable = false;
        }
    }

    //When pressed, will kill the window
    @FXML
    private void closeApp(MouseEvent e) {
        mainStage.close();
        System.exit(0);
    }


    //
    // WINDOW MOVING
    //

    //When i must update the XY of the click
    @FXML
    private void updateXY(MouseEvent e){
        actualX = e.getScreenX() - mainStage.getX();
        actualY = e.getScreenY() - mainStage.getY();
    }

    //When pressing and dragging the mouse it will move the window
    @FXML
    private void windowDragging(MouseEvent e) {
        if (isMovable) {
            mainStage.setX(e.getScreenX() - actualX);
            mainStage.setY(e.getScreenY() - actualY);
        }

        else {
            //setMin();
            mainStage.setX(e.getScreenX());
            mainStage.setY(e.getScreenY());
        }
    }

    //Update the status of the window from not movable to movable, after "normalize" effect
    //from the dragging it when it's maximized
    @FXML
    private void updateStatus(MouseEvent e) {
        if (mainStage.isMaximized() == false) { 
            isMovable = true;
        }
    }


    //
    // WINDOW RESIZING
    //

    /*onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow"*/

    @FXML
    private void setMouseCursor (MouseEvent e) {
        minimalWindowContainer.setCursor(Cursor.CROSSHAIR);
    }

    @FXML
    private void resetMouseCursor (MouseEvent e) {
        minimalWindowContainer.setCursor(Cursor.DEFAULT);
    }

    @FXML
    private void resizeWindow (MouseEvent e) {
        actualX = e.getScreenX() - mainStage.getX() + 13;
        actualY = e.getScreenY() - mainStage.getY() + 10;

        if (actualX % 5 == 0 || actualY % 5 == 0) {
            if (actualX > MIN_WIDTH) {
                mainStage.setWidth(actualX);
            } else {
                mainStage.setWidth(MIN_WIDTH);
            }

            if (actualY > MIN_HEIGHT) {
                mainStage.setHeight(actualY);
            } else {
                mainStage.setHeight(MIN_HEIGHT);
            }
        }
    }


    //
    // Internal methods
    //

    //Will set the window to MAXIMIZE size
    private void setMax() {
        mainStage.setMaximized(true);
        btnResize.setVisible(false);
        btnMax.setStyle("-fx-background-image: url('/res/dSquare.png');");
        minimalWindowContainer.setPadding(new Insets(0, 0, 0, 0));
    }

    //Will set the window to NORMAL size
    private void setMin() {
        mainStage.setMaximized(false);
        btnResize.setVisible(true);
        btnMax.setStyle("-fx-background-image: url('/res/square.png');");
        minimalWindowContainer.setPadding(new Insets(SHADOW_SPACE, SHADOW_SPACE, SHADOW_SPACE, SHADOW_SPACE));
    }

    @Override
    public void start(Stage primaryStage) {

        /* //NOT SURE IF DOING RIGHT YA'
        try {
            //Prepare the resource with the FXML file
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/application/minimalWindow/MainWindow.fxml"));

            //Load the main stackpane
            Parent root = loader.load();

            loader.setController(this);

            //Prepare the content of the window, with a minWidth/Height
            Scene scene = new Scene(root, MIN_WIDTH, MIN_HEIGHT);

            //Making the scene transparent
            scene.setFill(Color.TRANSPARENT);

            //Undecorate the window due its persolalisation
            primaryStage.initStyle(StageStyle.TRANSPARENT);

            //Set the content of the window
            primaryStage.setScene(scene);   *   
        }

        catch (Exception e) {
            e.printStackTrace();
        }       */
    }

and the CSS for styling:

* {
    /* Some general colors */
    primaryColor: #f9f9f9;  
    secondaryColor: derive(primaryColor, -75%);

    textColor: white;
    closeBtnColor: red;

}

#titleBar, #footer {
    -fx-background-color: secondaryColor;
}

#title {
    -fx-text-fill: textColor;
}

#contentArea {
    -fx-background-color: primaryColor;
}

#minimalWindowShadowContainer {
    -fx-background-color: transparent;      
    -fx-effect: dropshadow( gaussian , black , 5,0,0,0 );
    -fx-background-insets: 5;
}

#btnCls, #btnMax, #btnMin, #btnResize {
    -fx-background-color: transparent;
    -fx-background-radius: 0;
    -fx-border-color: transparent;
    -fx-border-width: 0;
    -fx-background-position: center;
    -fx-background-repeat: stretch;
}

#btnMax:hover, #btnMin:hover {
    -fx-background-color: derive(secondaryColor, 20%);  
}

#btnCls:hover {
    -fx-background-color: derive(red, 45%); 
}

#btnCls {
    -fx-background-image: url('/res/x.png');    
}

#btnMax {
    -fx-background-image: url('/res/square.png');
}

#btnMin {
    -fx-background-image: url('/res/line.png');
}

#btnResize {
    -fx-background-image: url('/res/resize.png');
}

In the App.java I should use it like this:

public class App {

    public static void main(String[] args) {        
        //Initialize the minimal window
        MinimalWindow mainWindow = new MinimalWindow();

        //Show the window, after all
        mainWindow.show();
    }
}

I post this my solution here becouse on internet I found exactly NOTHING about custom styling in MVC pattern (yes... I need to do it for the exam project).

What are the problems? It must be simple to use and reusable. Trying to make the constructor like this:

public MinimalWindow(String title, String source) {
        this.title = title;
        this.source = source;       
        start(mainStage);
    }

it gives me errors in parsing XAML file in the 11 row (the first line that define the stackpanel), or giving me an error "Caused by: java.lang.IllegalStateException: Toolkit not initialized". For the first, I don't know what is causing it. For the second, the solution on internet suggest to extend my class from Application and then override the "start" method, but it doesn't worked.

Question time: any solution? Suggestions?

PS: I make work this code in a non-mvc pattern, with a different style, and it worked great: wstaw.org/m/2016/04/07/ezgif.com-crop.gif

Upvotes: 1

Views: 867

Answers (2)

Martinocom
Martinocom

Reputation: 330

Ok, I followed all I learned from you and I make almost work all. So, what's I having now it's this:

wstaw.org/m/2016/04/10/project.png

Now, I have FXML of the MinimalWindow:

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

<?import javafx.scene.layout.BorderPane?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.GridPane?>

<!-- Container that will do the "shadow" effect -->
<fx:root xmlns:fx="http://javafx.com/fxml/1" type="BorderPane" fx:id="root" id="root" stylesheets="@MinimalWindowStyle.css" onMousePressed="#updateXY" onMouseDragged="#windowDragging" onMouseReleased="#updateStatus">
    <center>
        <!-- Main content -->
        <BorderPane fx:id="mainWindow" id="mainWindow">
            <!-- Padding will show the shadow effect under the window -->
            <padding>
                <Insets top="5" right="5" bottom="5" left="5"></Insets>
            </padding>

            <!-- Top bar of the window -->
            <top>
                <HBox id="titleBar" alignment="CENTER" spacing="5" prefHeight="30">
                    <padding>
                        <Insets top="5" right="5" bottom="5" left="5"/>
                    </padding> 

                    <ImageView fx:id="logo" fitWidth="20" fitHeight="20"></ImageView> 
                    <Label fx:id="lblTitle" id="title" text="MinimalWindow"></Label>
                    <Region HBox.hgrow="ALWAYS" prefHeight="30.0" prefWidth="200.0"></Region>

                    <HBox alignment="CENTER_RIGHT">
                        <Button id="btnMin" onMouseClicked="#minimizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
                        <Button fx:id="btnMax" id="btnMax" onMouseClicked="#maximizeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
                        <Button id="btnCls" onMouseClicked="#closeApp" minHeight="20" minWidth="20" maxHeight="20" maxWidth="20"></Button>
                    </HBox>
                </HBox>
            </top>

            <!-- Window content -->
            <center>
                <GridPane fx:id="contentArea" id="contentArea"></GridPane>
            </center>

            <!-- Footer of the window -->
            <bottom>
                <HBox id="footer" prefHeight="20" alignment="BOTTOM_RIGHT">
                    <padding>
                        <Insets top="5" right="5" bottom="5" left="5"/>
                    </padding> 

                    <Button fx:id="btnResize" id="btnResize" onMouseClicked="#updateXY" onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow" minHeight="10" minWidth="10" maxHeight="10" maxWidth="10"></Button>       
                </HBox>
            </bottom>

        </BorderPane>
    </center>
</fx:root>

The style for it:

* {
    /* Some general colors */
    primaryColor: #f9f9f9;  
    secondaryColor: derive(primaryColor, -75%);

    textColor: white;
    closeBtnColor: red; 
}

#titleBar, #footer {
    -fx-background-color: secondaryColor;
}

#title {
    -fx-text-fill: textColor;
}

#contentArea {
    -fx-background-color: primaryColor;
}

#root {
    -fx-background-color: transparent;      
    -fx-effect: dropshadow( gaussian , black , 5,0,0,0 );
    -fx-background-insets: 5;
}

#btnCls, #btnMax, #btnMin, #btnResize {
    -fx-background-color: transparent;
    -fx-background-radius: 0;
    -fx-border-color: transparent;
    -fx-border-width: 0;
    -fx-background-position: center;
    -fx-background-repeat: stretch;
}

#btnMax:hover, #btnMin:hover {
    -fx-background-color: derive(secondaryColor, 20%);  
}

#btnCls:hover {
    -fx-background-color: derive(red, 45%); 
}

#btnCls {
    -fx-background-image: url("/resources/x.png");  
}

#btnMax {
    -fx-background-image: url('/resources/square.png');
}

#btnMin {
    -fx-background-image: url('/resources/line.png');
}

#btnResize {
    -fx-background-image: url('/resources/resize.png');
}

The controller class for it:

package controller.minimalWindow;

import application.Main;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class MinimalWindowCtrl extends BorderPane {

    //Values injected from the FXML
    @FXML
    private BorderPane root, mainWindow;

    @FXML
    private Label lblTitle;

    @FXML
    private Button btnMax, btnResize;

    @FXML
    private GridPane contentArea;

    //Reference to the primaryStage
    final private Stage stage;

    //References to min/max width/height and the shadow effect
    final private int MINWIDTH, MINHEIGHT, SHADOWSPACE = 5;

    //Things for the resizing/moving window
    private double actualX, actualY;
    private boolean isMovable = true;




    public MinimalWindowCtrl (Stage stage, int minwidth, int minheight) {
        //First, take the reference to the stage
        this.stage = stage;

        //Taking the references to the window
        MINWIDTH = minwidth;
        MINHEIGHT = minheight;

        //Then load the window, setting the root and controller
        FXMLLoader loader = new FXMLLoader(getClass().getResource("../../view/minimalWindow/MinimalWindow.fxml"));
        loader.setRoot(this);
        loader.setController(this);



        //Try to load
        try {
            loader.load();
        }
        catch (Exception e) {
            e.printStackTrace();
            //TODO Show a message error
            Main.close();
        }
    }

    public void setTitle(String s) {
        lblTitle.setText(s);
    }

    public void setContent(Node node) {
        contentArea.getChildren().add(node);
    }



    //
    // MIMIZIE | MAXIMIZE | CLOSE 
    //

    //When pressed, will minimize the window to tray
    @FXML
    private void minimizeApp(MouseEvent e) {
        stage.setIconified(true);
    }

    //When pressed, check if it must maximize or restore the window
    @FXML
    private void maximizeApp(MouseEvent e) {
        if (stage.isMaximized()) {
            setMin();
            isMovable = true;
        }

        else {
            setMax();
            isMovable = false;
        }
    }

    //When pressed, will kill the window
    @FXML
    private void closeApp(MouseEvent e) {
        stage.close();
        System.exit(0);
    }


    //
    // WINDOW MOVING
    //

    //When i must update the XY of the click
    @FXML
    private void updateXY(MouseEvent e){
        actualX = e.getScreenX() - stage.getX();
        actualY = e.getScreenY() - stage.getY();
    }

    //When pressing and dragging the mouse it will move the window
    @FXML
    private void windowDragging(MouseEvent e) {
        if (isMovable) {
            stage.setX(e.getScreenX() - actualX);
            stage.setY(e.getScreenY() - actualY);
        }

        else {
            //setMin();
            stage.setX(e.getScreenX());
            stage.setY(e.getScreenY());
        }
    }

    //Update the status of the window from not movable to movable, after "normalize" effect
    //from the dragging it when it's maximized
    @FXML
    private void updateStatus(MouseEvent e) {
        if (stage.isMaximized() == false) { 
            isMovable = true;
        }
    }


    //
    // WINDOW RESIZING
    //

    /*onMouseEntered="#setMouseCursor" onMouseExited="#resetMouseCursor" onMouseDragged="#resizeWindow"*/

    @FXML
    private void setMouseCursor (MouseEvent e) {
        mainWindow.setCursor(Cursor.CROSSHAIR);
    }

    @FXML
    private void resetMouseCursor (MouseEvent e) {
        mainWindow.setCursor(Cursor.DEFAULT);
    }

    @FXML
    private void resizeWindow (MouseEvent e) {
        actualX = e.getScreenX() - stage.getX() + 13;
        actualY = e.getScreenY() - stage.getY() + 10;

        if (actualX % 5 == 0 || actualY % 5 == 0) {
            if (actualX > MINWIDTH) {
                stage.setWidth(actualX);
            } else {
                stage.setWidth(MINWIDTH);
            }

            if (actualY > MINHEIGHT) {
                stage.setHeight(actualY);
            } else {
                stage.setHeight(MINHEIGHT);
            }
        }
    }


    //
    // Internal methods
    //

    //Will set the window to MAXIMIZE size
    private void setMax() {
        stage.setMaximized(true);
        btnResize.setVisible(false);
        btnMax.setStyle("-fx-background-image: url('/res/dSquare.png');");
        mainWindow.setPadding(new Insets(0, 0, 0, 0));
    }

    //Will set the window to NORMAL size
    private void setMin() {
        stage.setMaximized(false);
        btnResize.setVisible(true);
        btnMax.setStyle("-fx-background-image: url('/res/square.png');");
        mainWindow.setPadding(new Insets(SHADOWSPACE, SHADOWSPACE, SHADOWSPACE, SHADOWSPACE));

    }
}

And in the Main.java I do:

package application;



import controller.MainWindowCtrl;
import controller.minimalWindow.MinimalWindowCtrl;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import javafx.scene.Scene;
import javafx.scene.control.TabPane;

public class Main extends Application {

    final private int MINWIDTH = 750,  MINHEGIHT = 500;


    @Override
    public void start(Stage primaryStage) {
        try {
            //Preparing the model
            //TODO the interface of model
            Object m = new Object();

            //Loading main content
            FXMLLoader loader = new FXMLLoader(getClass().getResource("/view/MainWindow.fxml"));
            TabPane mainPane = loader.load();

            //Setting the model for the controller
            ((MainWindowCtrl) loader.getController()).setModel(m);

            //Creating the style for the custom window
            MinimalWindowCtrl minimalWindowCtrl = new MinimalWindowCtrl(primaryStage, MINWIDTH, MINHEGIHT);
            minimalWindowCtrl.setContent(mainPane);

            //Making new scene
            Scene scene = new Scene(minimalWindowCtrl, MINWIDTH, MINHEGIHT);

            //Setting the style to the window (undecorating it)
            primaryStage.initStyle(StageStyle.TRANSPARENT);

            //Setting the scene on the window
            primaryStage.setScene(scene);

            //Showing the window
            primaryStage.show();

        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

    public static void close() {        
        System.exit(0);
    }
}

It's missing some features, like "I don't know why the icons for buttons is not showed", the shadow is still buggy, but it generally works.

This is the result:

enter image description here

Upvotes: 0

James_D
James_D

Reputation: 209225

The Application class represents the whole application. It does not represent a window. Windows in JavaFX are represented by the Stage class. The Application.start() method is the entry point (start) for a JavaFX application: you should consider it as the replacement for the main in a "regular" Java application. The Application subclass instance is created for you as part of the launch process, which also starts the FX toolkit. In the Oracle JDK, the launch process can be initiated by invoking the Java runtime (e.g. invoking java from the command line) and specifying an Application subclass as the class to execute. For environments that don't support direct launch of JavaFX applications, you should include a main method that invokes Application.launch(args), i.e.

public class MyApp extends Application {

    @Override
    public void start(Stage primaryStage) {
        // create objects and set up GUI, etc
    }

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

Consequently

  1. the Application subclass is inherently not reusable, and you should keep the start(...) method as minimal as possible (it should basically do nothing but, well, start the application).
  2. You should only ever have one instance of your Application subclass in any JVM
  3. As a consequence of (2), you should never use the Application class as the controller class

So to do what you are trying to do, I think you want to create a separate MinimalWindow class that is not an Application subclass. Use the Custom Component pattern described in the FXML documentation to have it load its own FXML and set itself as the controller class. Then you can create a minimal main class, extending Application whose start method creates and shows MinimalWindow.

Upvotes: 2

Related Questions