JavaFX桌面开发——进阶-FXML+Controller

286 阅读6分钟

前言

在进行了一段时间的 JavaFX 初级开发后,不可避免地会出现一些问题,例如:

  • 代码量过大,阅读和维护非常的麻烦。
  • 大部分的代码都是页面的样式,和具体的业务逻辑不能分开,总是挤在一块。
  • 不方便和其他更高级的组件交互,能做的貌似只有页面和对静态数据或者输入数据的展示与处理


而这些问题,在 进阶的 JavaFX 开发中都可以解决。

FXML 和 Controller

首先,要理解 进阶JavaFX 开发,就得知道 FXML 和 Controller 都是什么东西。

FXML

用来存储 JavaFX 的页面信息,文件内部采用标签的形式表示组件。而且标签中一些特殊属性可以和外界的Controller 相关联,达到页面与逻辑代码分离的效果。

Controller

关于这个,学过SSM 的应该不陌生,在 JavaFX中,它的作用就是实现 FXML 中定义组件的 相关逻辑功能,比如按钮的点击,文本框的输入数据的提取等。它是一个Java文件


举例

对于一个简单的登录页面,如果使用初级的JavaFX 开发,在Java文件中的代码可能是这样的:



//        登录的主页面
        Stage loginStage = new Stage();
        CommonUtils.setStageStyle(loginStage);


//        设置与父窗口之间的关系
        loginStage.initModality(Modality.APPLICATION_MODAL);

//  登录容器
        VBox login = new VBox(10);
        GridPane loginGrid = new GridPane();
//  用户名部分
        Label usernameLabel = new Label("用户名:");
        usernameLabel.setStyle("-fx-font-size: 18px;");

        TextField username = new TextField();
        username.setPrefSize(200,30);
        username.setPromptText("请输入用户名");
        username.setFocusTraversable(false);

         loginGrid.add(usernameLabel,0,0);
         loginGrid.add(username,1,0);

//  密码部分
        Label passwordLabel = new Label("密码:");
        passwordLabel.setStyle("-fx-font-size: 18px;");

        PasswordField password = new PasswordField();
        password.setLayoutX(passwordLabel.getWidth() + 10);
        password.setPrefSize(200,30);
        password.setPromptText("请输入密码");
        password.setFocusTraversable(false);

       loginGrid.add(passwordLabel,0,1);
       loginGrid.add(password,1,1);

        loginGrid.setVgap(10);

//  操作栏部分
        Label loginLabel = new Label("用户登录");
        loginLabel.setStyle("-fx-font-size: 25px;");
/*        不需要设置具体尺寸,放置在 BorderPane中心 会覆盖设定的尺寸
        login.setPrefWidth(100);
        login.setPrefHeight(100);
        */
        Label registryLabel = new Label("没有账号?点击注册");
//        Button registryLabel = new Button("注册");
        registryLabel.setStyle("-fx-font-size: 15px;" +
                               "-fx-text-fill: #0000ff;");

        Button loginButton = new Button("登录");

//        按钮的内边距 是文本与按钮边框的距离
//        loginButton.setPadding(new Insets(10,10,10,10));
//        login.setMargin(loginButton,new Insets(20,20,20,100));
        loginButton.setStyle("-fx-font-size: 18px;");

        Label findLabel = new Label("忘记密码?");
        findLabel.setStyle("-fx-font-size: 15px;" +
                "-fx-text-fill: #0000ff;");

        HBox operaBox = new HBox(10,registryLabel,loginButton,findLabel);

//        设置 注册 标签 与 登录 按钮的间距
        operaBox.setMargin(registryLabel,new Insets(10,10,0,10));
        operaBox.setMargin(findLabel,new Insets(10,0,0,10));
        operaBox.setMargin(loginButton,new Insets(10,0,0,0));

//  总体调整


        login.getChildren().setAll(loginLabel,loginGrid,operaBox);
        ```

        Scene loginScene = new Scene(loginPane,1000,545);

//        设置登录页面的各种事件
//        1. 鼠标 左击登录按钮事件
//        2. 键盘 回车键事件
//        3. 注册 被点击事件
//        4. 忘记密码 被点击事件

        loginButton.setOnAction(e -> {
            loginNow(loginStage,username.getText(),password.getText());
        } );

//        设置 输入用户名之后 TAB 转换焦点到password
        username.setOnKeyPressed(key -> {
            if (key.getCode() == KeyCode.TAB)
                    password.requestFocus();
        });

        password.setOnKeyPressed(key -> {
            if (key.getCode() == KeyCode.ENTER)
                    loginNow(loginStage,username.getText(),password.getText());
        });

        registryLabel.setOnMousePressed(e -> {
            if (e.getButton() == MouseButton.PRIMARY)
                Registry.registryInit(loginStage);
        });

        findLabel.setOnMousePressed(e -> {
            if (e.getButton() == MouseButton.PRIMARY)
                FindPassword.findPasswordInit(loginStage);
        });


        loginStage.setScene(loginScene);
        loginStage.show();

但是如果是采用FXML+ Controller 的话,在Java 文件中 就可以简化成这样:


public class LoginController  {
    public TextField username;
    public PasswordField password;
    public Button loginButton;


    public void userLogin(ActionEvent actionEvent) throws IOException {

        String username = this.username.getText();
        String userPassword = this.password.getText();

            Users user = uService.login(username, userPassword);

            if (user != null) {
               // 登录成功逻辑

            } else CommonUtils.showAlert("登录失败", "用户名或密码错误", Alert.AlertType.ERROR);
    }
    public void passwordEnter(KeyEvent keyEvent) throws IOException {
        if (keyEvent.getCode() == KeyCode.ENTER) {
            userLogin(new ActionEvent());
        }
    }
    public void register(MouseEvent mouseEvent) throws IOException {
        if (mouseEvent.isPrimaryButtonDown()){
            // 注册逻辑
        }

    }
    public void findPassword(MouseEvent mouseEvent) throws IOException {
         if (mouseEvent.isPrimaryButtonDown()){
            // 寻找密码逻辑
        }
    }
    
private UserService uService;

@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
    uService = BeanUtils.getBean(UserService.class);
}
}

那这个时候就有人反驳了,博主,即使 Controller 中内容这么少,你FXML 不也有很多代码吗,而且如果是标签的话,只会比原来初级JavaFX 中的代码多吧。

是这样,但是FXML里的页面可以通过拖拉拽的形式直接生成,要做的也只是将组件和Controller绑定(通过fx:id 属性)

SceneBuilder的下载与配置

SceneBuilder是一个专门服务于 JavaFX 中 FXML 文件的软件。它可以通过拖拉拽的形式快速生成页面并且同步到FXML文件中

下载地址:Scene Builder - Gluon (gluonhq.com)

在IDEA中进行配置

下载并安装完成后,在IDEA中点开设置-->语言和框架--> JavaFX

将SceneBuider.exe的路径填入即可

image.png

使用

先在JavaFX 项目中新建一个FXML 文件,然后配置controller,之后 右击该文件,选择 在SceneBuilder 打开,

image.png

绑定的Controller 为 fxml.login 包下的 LoginController类,后续该FXML中的组件都可以成为该Controller 中的属性值


image.png

打开后 在 SceneBuilder 中的效果如下:

image.png

使用时将组件拖到中心预览区即可在组件结构中看到具体位置在预览区看到具体效果

注意事项

  • SceneBuilder 中并未指出各种组件之间是否可以互相包含.
  • SceneBuilder 中并未给组件的属性配备说明,一些不合理的数值也可能生效。
  • SceneBuilder 中并不能直接调整对应的Controller中的内容,需要手动去生成。

    使用时需要具备一定的 JavaFX 基础

Controller 配置

在FXML中配置了对应的Controller,而且也在SceneBuilder定义好了页面,应该怎样去调整Controller 的内容呢?

导入属性值

在FXML文件中将 需要导入到Controller中的组件 添加属性值 fx:id = "属性名",这个属性名将同步到 Controller 作为组件对象的变量名

添加完成后,光标移动到fx:id上 按下 ALT+ENTER,然后回车即可同步到Controller中。

导入事件

在FXML文件中将 需要导入事件的组件 添加对应事件名的属性,例 onKeyPressed="#passwordEnter",这个事件属性的值将作为Controller中的方法名

添加完成后,光标移动到事件属性上 按下 ALT+ENTER,然后回车即可同步到Controller中。

注意事项

  • 即使FXML中声明了fx:id或者事件名的属性,如果不手动同步到Controller中,程序不会报错,对应的事件逻辑将不会执行。

FXML的调用方式

由于没有了代表性的 Stage ,FXML 文件页面在加载时不能通过简单的 show() 方法呈现到桌面。需要记住FXML文件的路径,使用FXML的加载器去加载,并返回一个 Pane 对象,可以参考以下代码:

AnchorPane login = new FXMLLoader(getClass().getResource("/fxml/login/login.fxml")).load();

上述代码中 AnchorPane 是 login.fxml组件结构中最外层的容器,如果这个返回值的类型不符合,程序将报错。

返回的 Pane 对象 可以封装进 Scene 进而封装进 Stage 进行展示,

总结

  • JavaFX的 进阶开发采用了页面与逻辑分离的思想,极大程度的提高了程序的可维护性与可阅读性。
  • SceneBuilder 可以通过 组件的拖拉拽 快速生成FXML页面代码,但是不能调整对应的Controller
  • FXML 需要通过 FXMLLoader 的加载才能转换成 Java对象,进而进行相关的展示。