JavaFX - 简单的自定义最小 window 实现

JavaFX - simple custom minimal window implementation

我刚刚发现了 JavaFX,我非常喜欢它。我讨厌 java-默认 GUI,所以我立即决定个性化我的 window。我尝试了很多次,但我有一大局限和一大objective;局限性?我必须使用 MVC 模式。 Objective?使自定义 window 可重用。

所以...这就是我现在的观点: wstaw.org/m/2016/04/07/resoruces.png

我制作了一个包含 App.java 的通用包应用程序,它将启动该应用程序。然后我制作另一个内部包,包含 "MinimalWindow" 逻辑,以及我需要的所有资源。

我实现了这个 FXML 代码来执行 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>

然后我实现了控制器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();
        }       */
    }

和 CSS 样式:

* {
    /* 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');
}

在App.java中我应该这样使用它:

public class App {

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

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

我 post 这是我的解决方案,因为在互联网上我完全没有发现 MVC 模式中的自定义样式(是的......我需要为考试项目做这件事)。

有什么问题?它必须简单易用且可重复使用。试图使构造函数像这样:

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

在第 11 行(定义堆栈面板的第一行)中解析 XAML 文件时出现错误,或者出现错误 "Caused by: java.lang.IllegalStateException: Toolkit not initialized"。 首先,我不知道是什么原因造成的。第二,网上的解决方案建议从应用程序扩展我的 class 然后覆盖 "start" 方法,但它不起作用。

提问时间:有什么办法吗?建议?

PS:我用非 mvc 模式编写了这段代码,风格不同,效果很好:wstaw.org/m/2016/04/07/ezgif.com-crop.gif

Applicationclass代表整个应用。它不代表 window。 Windows中的JavaFX由Stageclass表示。 Application.start() 方法是 JavaFX 应用程序的入口点(开始):您应该将其视为 "regular" Java 中 main 的替代品应用。 Application subclass 实例是作为启动过程的一部分为您创建的,它也会启动 FX 工具包。在 Oracle JDK 中,启动过程可以通过调用 Java 运行时(例如从命令行调用 java)并指定一个 Application subclass 作为 class 来执行。对于不支持直接启动 JavaFX 应用程序的环境,您应该包含一个调用 Application.launch(args)main 方法,即

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);
    }
}

因此

  1. Application subclass 本质上是不可重用的,你应该尽可能地保持 start(...) 方法(它应该基本上什么都不做,但是,启动应用程序).
  2. 在任何 JVM
  3. 中,您的 Application 子 class 应该只有一个实例
  4. 作为 (2) 的结果,you should never use the Application class as the controller class

所以为了做你想做的事,我想你想创建一个单独的 MinimalWindow class 而不是 Application subclass。使用 FXML 文档中描述的 Custom Component 模式让它加载自己的 FXML 并将自己设置为控制器 class。然后你可以创建一个最小的 main class,扩展 Applicationstart 方法创建并显示 MinimalWindow.

好的,我按照从您那里学到的所有知识进行了学习,几乎完成了所有工作。那么,我现在拥有的是:

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

现在,我有了 MinimalWindow 的 FXML:

<?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>

它的样式:

* {
    /* 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');
}

它的控制器class:

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));

    }
}

而在 Main.java 我做的是:

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);
    }
}

它缺少一些功能,例如 "I don't know why the icons for buttons is not showed",阴影仍然有问题,但它通常可以正常工作。

这是结果: