JavaFX - 实现管理多个Stage窗口及源码解析

前言

     JavaFX相比AWT就是和Android一样通过xml文件来定义界面的设计,并且可以通过fxml资源文件结合Java代码来控制界面的变化。摒弃之前写AWT那种什么都在Java代码中定义(窗口大小,颜色,控件等等....)的设计。通过fxml+Java代码控制界面达到界面程序更加人性化(猿性化)。

     但是JavaFX对于窗口的管理却不是那么地人性化,我目前没有发现官方人性化的解决方案。
     有人就说将所有FXML界面的Controller写到同一个类里面不就好了吗?
     答曰:这样和AWT有多大区别了?我们需要的是每个fxml对应上一个Controller类,这样才能进行更好的、更方便的设计。
     还有人说将所有窗口的大小设计成为统一大小不就好了么,这样就可以通过管理Scene的方式进行操作所有的界面了?

     Oracle的一位大神写了一个关于多个窗口管理的解决方案(本文也是根据这位大神的博客的教程进行修改),但是这位大神是基于所有的窗口都是同等大小的情况下进行操作Scene的内容切换达到多个窗口同时管理的,一旦需要的界面窗口大小不一的时候就有问题了,大家可以去参考下她的内容。


     如果看了她的文章的人不难发现,她的Stage、Scene对象由始至终都是一个,改变的是Scene里面的容器内容。也就是说:Stage的长宽从加载到程序结束是不会改变的,如果强行将Stage注入到每个View(FXML)的Controller中,在改变Scene里面的内容的时候改变Stage的大小,那么倒不如直接一开始直接将Stage交给控制器进行管理,这也是我今天在博客这里要写的东西。

此文老猫原创,转载请加本文连接:http://blog.csdn.net/nthack5730/article/details/51901593
更多有关老猫的文章:http://blog.csdn.net/nthack5730






     在开始之前再一次谴责那些通过爬虫爬出来的垃圾程序网站!!!
     本文只在本人CSDN博客发布,如果你看到本文的时候地址非CSDN或者没有注明来源的转载,或者网页内容广告很多,那么可能就是爬虫网站!
     爬虫网站损害的不仅仅是我们的心血,因为爬虫或多或少都会出现内容的纰漏,对读者造成的危害更大,误人子弟。



好了,开始:

     相对来说,Oracle大神创建的是Scene的管理器,而且管理的是Scene里面的内容,按照她的说法呢,就是所有的窗口都是同一个大小的,因为她的Stage从头到尾都是一个,但是JavaFX的Stage一旦显示了就不能进行大小的修改,强行修改会抛出异常。
     但是很多桌面程序是大小不一的,例如:登录框、菜单主界面、提示框等等...因此我将她的内容进行修改,将管理Scene的内容改成管理Stage窗口。这样就可以通过管理不同的Stage达到我们需要的大小不一的窗口的效果。



第一步:创建一个StageController控制器

     StageController控制器主要是加载fxml资源文件和对应的View控制器、生成Stage对象以及对Stage对象进行管理,因此该StageController控制器对象也需要被注入到每个fxml的View控制器中。
     下面给出StageController.java的源码:

StageController.java

package com.marer.view;

import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

import java.util.HashMap;

/**
 * Created by CatScan on 2016/6/23.
 */

public class StageController {
    //建立一个专门存储Stage的Map,全部用于存放Stage对象
    private HashMap<String, Stage> stages = new HashMap<String, Stage>();


    /**
     * 将加载好的Stage放到Map中进行管理
     *
     * @param name  设定Stage的名称
     * @param stage Stage的对象
     */

    public void addStage(String name, Stage stage) {
        stages.put(name, stage);
    }


    /**
     * 通过Stage名称获取Stage对象
     *
     * @param name Stage的名称
     * @return 对应的Stage对象
     */

    public Stage getStage(String name) {
        return stages.get(name);
    }


    /**
     * 将主舞台的对象保存起来,这里只是为了以后可能需要用,目前还不知道用不用得上
     *
     * @param primaryStageName 设置主舞台的名称
     * @param primaryStage     主舞台对象,在Start()方法中由JavaFx的API建立
     */

    public void setPrimaryStage(String primaryStageName, Stage primaryStage) {
        this.addStage(primaryStageName, primaryStage);
    }


    /**
     * 加载窗口地址,需要fxml资源文件属于独立的窗口并用Pane容器或其子类继承
     *
     * @param name      注册好的fxml窗口的文件
     * @param resources fxml资源地址
     * @param styles    可变参数,init使用的初始化样式资源设置
     * @return 是否加载成功
     */

    public boolean loadStage(String name, String resources, StageStyle... styles) {
        try {
            //加载FXML资源文件
            FXMLLoader loader = new FXMLLoader(getClass().getResource(resources));
            Pane tempPane = (Pane) loader.load();

            //通过Loader获取FXML对应的ViewCtr,并将本StageController注入到ViewCtr中
            ControlledStage controlledStage = (ControlledStage) loader.getController();
            controlledStage.setStageController(this);

            //构造对应的Stage
            Scene tempScene = new Scene(tempPane);
            Stage tempStage = new Stage();
            tempStage.setScene(tempScene);

            //配置initStyle
            for (StageStyle style : styles) {
                tempStage.initStyle(style);
            }

            //将设置好的Stage放到HashMap中
            this.addStage(name, tempStage);

            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 显示Stage但不隐藏任何Stage
     *
     * @param name 需要显示的窗口的名称
     * @return 是否显示成功
     */

    public boolean setStage(String name) {
        this.getStage(name).show();
        return true;
    }


    /**
     * 显示Stage并隐藏对应的窗口
     *
     * @param show  需要显示的窗口
     * @param close 需要删除的窗口
     * @return
     */

    public boolean setStage(String show, String close) {
        getStage(close).close();
        setStage(show);
        return true;
    }


    /**
     * 在Map中删除Stage加载对象
     *
     * @param name 需要删除的fxml窗口文件名
     * @return 是否删除成功
     */

    public boolean unloadStage(String name) {
        if (stages.remove(name) == null) {
            System.out.println("窗口不存在,请检查名称");
            return false;
        } else {
            System.out.println("窗口移除成功");
            return true;
        }
    }
}


    控制器在对fxml资源文件加载的时候使用的是Pane这个容器作为最基底的容器。原因是Pane是其他Pane容器的父类。(如下图)
技术分享

技术分享

     注:fxml资源文件一定要绑定其对应的View控制器类,可以在SceneBuilder中的左下角绑定界面的Controller进行指定,也可以在fxml的源码中修改fx:controller...进行绑定。

此文老猫原创,转载请加本文连接:http://blog.csdn.net/nthack5730/article/details/51901593
更多有关老猫的文章:http://blog.csdn.net/nthack5730




第二步:将StageController控制器注入(注册)到每个界面的控制器中

     上面的loadStage()中大家不难发现有:
ControlledStage controlledStage = (ControlledStage) loader.getController();
controlledStage.setStageController(this);

     这段代码就是将StageController对象注入到每个View控制器中,那么要达到这个效果我们每个View控制器就必须有StageController属性(域、字段)。同时,为了能够将控制器对象注入,必须有一个setStageController(...)方法。
     在这里创建一个接口,所有的View控制器都去实现这个接口即可:

ControlledStage.java

/**
 * Created by CatScan on 2016/6/23.
 */

public interface ControlledStage {
    public void setStageController(StageController stageController);
}

     这样,每个View控制器在实现这个接口后都必须要重写这个方法,将StageController对象保存到自己属性中。
     其实还可以将上面的方法写成抽象类的形式,每个View控制器继承这个类,也是可以的,具体喜欢怎样就看各位的选择啦。

     下面给出一个栗子(例子):登陆窗口的View控制器。

LoginViewController.java

package com.marer.view;

import javafx.fxml.Initializable;

import java.net.URL;
import java.util.ResourceBundle;

/**
 * Created by CatScan on 2016/6/21.
 */

public class LoginViewController implements ControlledStageInitializable {
    StageController myController;

    public void setStageController(StageController stageController) {
        this.myController = stageController;
    }

    public void initialize(URL location, ResourceBundle resources) {

    }

    public void goToMain(){
        myController.setStage(MainApp.mainViewID);
    }
}


     上面的代码就实现了将StageController对象注入到LoginViewController里面,并且在goToMain()里面对StageController对象进行了调用。

此文老猫原创,转载请加本文连接:http://blog.csdn.net/nthack5730/article/details/51901593
更多有关老猫的文章:http://blog.csdn.net/nthack5730




第三步:在MainApp.java中对StageController实例化并加载所有的窗口

     现在假设所有的fxml资源文件都将加载为一个独立的窗口,当然,如果你需要一个窗口里面有多个fxml资源文件也是可以的,具体的看自己的需求。现在这里每个fxml资源文件就是一个窗口作为例子。
     首先,我们需要对所有的界面进行静态“留名”(将资源文件和ID写成静态),并在Start的方法中进行“加载”:

MainApp.java

package com.marer.view;
/**
 * Created by CatScan on 2016/6/19.
 */


import javafx.application.Application;
import javafx.stage.Stage;
import javafx.stage.StageStyle;

public class MainApp extends Application {
    public static String mainViewID = "MainView";
    public static String mainViewRes = "MainView.fxml";

    public static String loginViewID = "LoginView";
    public static String loginViewRes = "LoginView.fxml";

    private StageController stageController;



    @Override
    public void start(Stage primaryStage) {
        //新建一个StageController控制器
        stageController = new StageController();

        //将主舞台交给控制器处理
        stageController.setPrimaryStage("primaryStage", primaryStage);

        //加载两个舞台,每个界面一个舞台
        stageController.loadStage(loginViewID, loginViewRes, StageStyle.UNDECORATED);
        stageController.loadStage(mainViewID, mainViewRes);

        //显示MainView舞台
        stageController.setStage(mainViewID);
    }


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

     所有的静态属性,分别是每个界面的ID和资源文件的相对路径:
    public static String mainViewID = "MainView";
    public static String mainViewRes = "MainView.fxml";

    public static String loginViewID = "LoginView";
    public static String loginViewRes = "LoginView.fxml";

     因为是静态的属性,也可以自行建立一个类或者Properties文件进行存储并读取,自然是哪个方便用哪个方式。
     静态的ID属性是为了在后面调用过程中容易找到对应的ID,不会出现打错字符串出现空指针异常。例如:第二步中的goToMain()方法....

     然后需要的是在start()方法中对StageController控制器进行实例化并用StageController对象加载fxml资源文件。

        //加载两个舞台,每个界面一个舞台
        stageController.loadStage(loginViewID, loginViewRes, StageStyle.UNDECORATED);
        stageController.loadStage(mainViewID, mainViewRes);

     上面两行代码实现的就是加载两个fxml资源文件并注册为Stage窗口,loadStage(...)就是加载fxml资源文件, 并且在调用此方法的过程中已经将StageController控制器的对象注入到每个被加载的fxml对应的View控制器中,这就是每个fxml资源文件要绑定View控制器类的原因,具体可以回顾第一步。
     好了,回到这里,其中第一行在加载的时候设置了窗口无边框,该方法我使用了Java的可变参数,允许我们对窗口进行多个样式的设置,具体大家可以看回第一步StageController.java的代码。

     值得我们注意的是:JavaFX的Stage在调用show()之后是不允许对舞台的外观再进行修改的,包括长、宽,否则会抛出异常,这也是催生我写这篇文章的原因之一。

     最后,我们就设置需要显示的窗口即可:
        //显示MainView舞台
        stageController.setStage(mainViewID);

     不难发现,只要调用这个StageController对象中的setStage(...),即可显示对应的ID的Stage窗口。
     在StageController控制器中的setStage(String show, String hide)方法是可以调用一个窗口后隐藏一个窗口,源码中我添加了注释。
     更多方法如:showAndWait(...)这些我还没用上,会在以后用上的时候进行修改和优化这个StageController控制器。

此文老猫原创,转载请加本文连接:http://blog.csdn.net/nthack5730/article/details/51901593
更多有关老猫的文章:http://blog.csdn.net/nthack5730






总结:

     首先对StageController控制器中必用的方法简单归纳下,具体解析都在源码注释中:

    * loadStage(String ,String):加载fxml资源文件并对对应的fxml控制器注入StageController控制器对象。
    * setStage(String):显示对应ID的窗口,该窗口已经而且必须是被加载过的,否则会抛出空指针异常。
    * setStage(String ,String):显示第一个ID参数的窗口对象,隐藏第二个ID参数的窗口对象。
    * unloadStage(String):根据ID卸载已经加载的窗口对象。



     其次需要注意的是:每个fxml资源文件的路径名最好用一个类静态包装起来,并赋予对应的ID值,本文就包装在MainApp.java中。方便每个View控制器调用setStage(...)。






     好了,关于控制多个Stage窗口管理的设计的文章已经写完了。
     谢谢大家的支持,有更好的想法或者文中不足的地方老猫非常欢迎大家提出来讨论。

此文老猫原创,转载请加本文连接:http://blog.csdn.net/nthack5730/article/details/51901593
更多有关老猫的文章:http://blog.csdn.net/nthack5730

 
 



 


文章来自:http://blog.csdn.net/nthack5730/article/details/51901593
© 2021 jiaocheng.bubufx.com  联系我们
ICP备案:鲁ICP备09046678号-3