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

Posted 老猫烧须

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaFX - 实现管理多个Stage窗口切换及源码解析相关的知识,希望对你有一定的参考价值。

前言

     JavaFX相比AWT就是和android一样通过xml文件来定义界面的设计,并且可以通过fxml资源文件结合Java代码来控制界面的变化。摒弃之前写AWT那种什么都在Java代码中定义(窗口大小,颜色,控件等等....)的设计。通过fxml+Java代码控制界面达到界面程序更加人性化(猿性化)。
     但是JavaFX对于窗口的管理却不是那么地人性化,我目前没有发现官方人性化的解决方案。      有人就说将所有FXML界面的Controller写到同一个类里面不就好了吗?      答曰:这样和AWT有多大区别了?我们需要的是每个fxml对应上一个Controller类,这样才能进行更好的、更方便的设计。      还有人说将所有窗口的大小设计成为统一大小不就好了么,这样就可以通过管理Scene的方式进行操作所有的界面了?
     Oracle的一位大神写了一个关于多个窗口管理的解决方案(本文也是根据这位大神的博客的教程进行修改),但是这位大神是基于所有的窗口都是同等大小的情况下进行操作Scene的内容切换达到多个窗口同时管理的,一旦需要的界面窗口大小不一的时候就有问题了,大家可以去参考下她的内容。 https://blogs.oracle.com/acaicedo/entry/managing_multiple_screens_in_javafx1
https://blogs.oracle.com/acaicedo/entry/managing_multiple_screens_in_javafx
     如果看了她的文章的人不难发现,她的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(...)。



期间有人问关于执行流程,我就简单描述下:(注意:是简单描述!望高手再指点!) 我就顺着执行的路径大概描述下吧(可能有出入,但思维一样的):
1.在MainApp中【stageController = new StageController();】创建了一个StageController,这是借助HashMap包装的容器工厂类(上面有写)。


2.然后执行【stageController.loadStage(loginViewID, loginViewRes, StageStyle.UNDECORATED);】调用【StageController类的对象】加载了【LoginViewController】。


3.因为【LoginView】实现了【ControlledStage接口】,【StageController类的对象】在执行【loadStage(...)】方法的时候,执行了【controlledStage.setStageController(this);】,意思就是把【自己(StageController类的对象)】注入到【LoginViewController类的对象】的【myController属性】中。


4.而【LoginView】的对象则有JavaFX内部机制进行实例化,通过【FXMLLoader】中的【getController()】方法获得【LoginViewController对象】。


5.于是就能够通过【StageController.setStage(...)】(要实例对象)来控制显示和隐藏窗口。




     好了,关于控制多个Stage窗口管理的设计的文章已经写完了。      谢谢大家的支持,有更好的想法或者文中不足的地方老猫非常欢迎大家提出来讨论。
此文老猫原创,转载请加本文连接:http://blog.csdn.net/nthack5730/article/details/51901593 更多有关老猫的文章:http://blog.csdn.net/nthack5730
   


 

以上是关于JavaFX - 实现管理多个Stage窗口切换及源码解析的主要内容,如果未能解决你的问题,请参考以下文章

JavaFX实战:几种事件监听实现,键盘按键事件监听,鼠标滚轮事件监听,鼠标按键事件监听,鼠标移动事件监听

JavaFX实战:几种事件监听实现,键盘按键事件监听,鼠标滚轮事件监听,鼠标按键事件监听,鼠标移动事件监听

JavaFX实战:模拟电子琴弹奏效果,鼠标弹奏一曲piano送给大家

JavaFX实战:模拟电子琴弹奏效果,鼠标弹奏一曲piano送给大家

JavaFX实战:模拟电子琴弹奏效果,鼠标弹奏一曲piano送给大家

JavaFX实战:模拟电子琴弹奏效果,鼠标弹奏一曲piano送给大家