一、前言

  • 在上一章节中我们把登陆窗体开发完成了,并进行了效果演示。那么接下来我们就需要在这个窗体里面添加行为事件和接口,待完成内容如下;

    序号 类型 描述
    1 事件 鼠标拖拽窗体移动
    2 事件 最小化到快捷栏
    3 事件 退出当前窗体
    4 事件 使用用户 ID 和密码登陆
    5 接口 登陆成功,执行跳转操作
    6 接口 登陆失败,执行提示操作
  • 在桌面版程序开发中不同于 web。桌面版开发需要有界面的事件的发起,例如 Button 按钮点击,当接收外部条件变化后要有接口承载,例如登陆成功后的页面跳转。但是在 web 中大部分时候只需要一个 http 请求同步响应即可。

  • 另外也可能有一部分桌面开发程序中是类似同步请求和反馈的,那么在一个事件的发起后,就直接影响事件内容的变化,来改变窗体或者填充数据行为。

  • 以下的章节我们会先去非常直接简单的添加事件和接口,以更清晰的直观的了解这部分内容的开发。之后我们会进行一次小的 重构,以此来适应更好的拓展。

二、工程结构 (重构前)

itstack-naive-chat-ui-03
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.navice.chat.ui
    │   │       ├── view
    │   │       │  └── Login.java
    │   │       └── Application.java
    │   └── resources
    │       └── fxml.login
    │           ├── css
    │           ├── img
    │           └── login.fxml
    └── test
        └── java
            └── org.itstack.test
                └── ApiTest.java
  • 在目前的工程结构下,直接在里面开发事件和接口。

三、事件内容讲解 (重构前)

接下来我们会在现有代码中,org.itstack.navice.chat.ui.view.Login.java,进行编写事件和接口。

1. 鼠标移动窗体事件

 private double xOffset;
 private double yOffset;

private void move() {
    root.setOnMousePressed(event -> {xOffset = getX() - event.getScreenX();
        yOffset = getY()- event.getScreenY();
        root.setCursor(Cursor.CLOSED_HAND);
    });
    root.setOnMouseDragged(event -> {setX(event.getScreenX() + xOffset);
        setY(event.getScreenY() + yOffset);});
    root.setOnMouseReleased(event -> {root.setCursor(Cursor.DEFAULT);
    });}
  • 这里的 root 是我们整个登陆窗体的元素面板,可以通过它来设置一些行为设置或者还可以通过 ID 查找根里面还有的元素 root.lookup
  • 在我们所有使用的桌面程序里,都是可以通过鼠标拖拽来移动位置的。那么这里我们是定义三个鼠标事件,来实现窗体的移动,如下;
  • setOnMousePressed;鼠标按下事件,这个时候记录窗体位置
  • setOnMouseDragged;鼠标拖动事件,这个设置 setX、setY
  • setOnMouseReleased;鼠标释放事件,这个时候恢复默认鼠标样式,最终完成了整个窗口的拖拽过程

2. 最小化窗体事件

private void min() {Button login_min = $("login_min", Button.class);
    login_min.setOnAction(event -> {System.out.println("最小化窗体");
        setIconified(true);
    });}
  • 这个事件比较简单,只需要设置按钮点击事件后执行;setIconified(true) 即可,如果有些快捷键操作,弹出窗体可以动态设置为 false

3. 退出窗体事件

private void quit() {Button login_close = $("login_close", Button.class);
    login_close.setOnAction(event -> {System.out.println("退出窗体");
        close();
        System.exit(0);
    });}
  • 最小化与退出是一对的事件,退出事件同样需要绑定按钮,并执行 close() 操作以退出窗体,并执行程序退出 System.exit(0)
  • 另外在 socket 通信的时候,退出还需要断开服务端连接,记录个人状态等操作。例如;某某地方登陆、某某地方退出、时间、设备等信息,后续在通信开发中我们会陆续完善部分功能

4. 登陆操作事件

private void login() {TextField userId = $("userId", TextField.class);
    PasswordField userPassword = $("userPassword", PasswordField.class);
    $("login_button", Button.class).setOnAction(event -> {System.out.println("登陆操作");
        System.out.println("用户 ID:" + userId.getText());
        System.out.println("用户密码:" + userPassword.getText());
    });}
  • 登陆操作是我们在点击按钮时,获取输入框内的‘用户 ID’与‘用户密码’,发送给服务端进行验证。当然这里的真实业务场景下,不会直接明文传输,会进行非对称加密,并在服务端解析后做登陆验证。
  • 登陆验证完成后,开始收到异步消息。如果验证成功则可以进行页面跳转,否则进行提示。
  • 那么现在你可能会想到这部分怎么和通信交互,以及事件的触发,这部分内容我们会逐步讲解。

四、工程结构 (重构后)

itstack-naive-chat-ui-04
└── src
    ├── main
    │   ├── java
    │   │   └── org.itstack.navice.chat.ui
    │   │       ├── view
    │   │       │  └── login
    │   │       │  │    ├── ILoginEvent.java
    │   │       │  │    ├── ILoginMethod.java
    │   │       │  │    ├── LoginController.java
    │   │       │  │    ├── LoginEventDefine.java
    │   │       │  │    ├── LoginInit.java
    │   │       │  │    └── LoginView.java
    │   │       │  └── UIObject.java
    │   │       └── Application.java
    │   └── resources
    │       └── fxml.login
    │           ├── css
    │           ├── img
    │           └── login.fxml
    └── test
        └── java
            └── org.itstack.test
                └── ApiTest.java
  • 从上面新的结构可以看到,原来我们仅是一个类被抽取出这么多类,而同时每一个都被赋予了不同的功能,以整洁我们的代码;

五、代码讲解 (重构后)

抽象后的类功能结构,如下;

序号 描述
1 ILoginEvent 事件接口类,具体实现交给调用方。例如我们在点击登陆后将属于窗体的功能处理完毕后,实际的验证交给外部
2 ILoginMethod 方法接口类,在上面我们说过桌面程序的开发基本都是事件触达和等待回调,那么我们给外部提供接口主要用于类似登陆处理完毕后,来执行相应方法进行窗体切换或者数据填充
3 LoginController 窗体的控制管理类,也是一个窗体的管家;因为它会继承窗体的装载、实现接口方法、初始化界面、初始化事件定义
4 LoginEventDefine 窗体事件定义,例如将登陆、最小化、退出等在这里完成定义
5 LoginInit 窗体的初始化操作,可以创建一些待填充的元素
6 LoginView 窗体的展示,主要用于扩展一些随着用户操作新展示的元素,例如后续在聊天窗体新增的消息提醒等
7 UIObject UI 父类定义,这是一个抽象类,提供了基础的初始化内容和接口,以及定义抽象方法

1. 窗体事件接口

ILoginEvent.java & 窗体事件接口

public interface ILoginEvent {

    /**
     * 登陆验证
     * @param userId        用户 ID
     * @param userPassword  用户密码
     */
    void doLoginCheck(String userId, String userPassword);

}
  • 事件方法里提供了登陆所需的参数,用户 ID、用户密码,如果是实际业务开发还会需要传递;IP 地址、设备信息、请求时间等信息,用于判断是否正常登陆

2. 窗体方法接口

ILoginMethod.java & 窗体方法接口

public interface ILoginMethod {

    /**
     * 打开登陆窗口
     */
    void doShow();

    /**
     * 登陆失败
     */
    void doLoginError();

    /**
     * 登陆成功;跳转聊天窗口 [关闭登陆窗口,打开新窗口]
     */
    void doLoginSuccess();}
  • 这里面定义登陆操作的基本方法,按需可以提供一些入参。
  • doShow(); 是调用展示登陆窗体的接口,因为在我们重构定义后,调用方并不能直接使用 show() 来展示页面
  • doLoginError(); 登陆失败的操作
  • doLoginSuccess(); 登陆成功的操作,主要包含关闭现有页面,打开新的页面。这里新的页面,也就是我们后续的聊天主窗体

3. 窗体的控制管理类

LoginController.java & 窗体的控制管理类

public class LoginController extends LoginInit implements ILoginMethod {

    private LoginView loginView;
    private LoginEventDefine loginEventDefine;

    public LoginController(ILoginEvent loginEvent) {super(loginEvent);
    }

    @Override
    public void initView() {loginView = new LoginView(this, loginEvent);
    }

    @Override
    public void initEventDefine() {loginEventDefine = new LoginEventDefine(this, loginEvent, this);
    }

    @Override
    public void doShow() {super.show();
    }

    @Override
    public void doLoginError() {System.out.println("登陆失败,执行提示操作");
    }

    @Override
    public void doLoginSuccess() {System.out.println("登陆成功,执行跳转操作");
        // 关闭原窗口
        close();}

}
  • 首先可以看到这里继承了窗体的初始化,实现了窗体的接口定义。这样的方式可以更加方便外部的调用,同时内部的逻辑也会更加清晰。
  • initView(),初始化了窗体页面,如果随着后续的窗体内容的增加,这部分初始化的内容也会有所增加。
  • initEventDefine(),等窗体初始化完成后,我们就可以初始化我们的事件定义。
  • 接下来我们后面看到的代码就是关于接口的具体的实现,供外部调用的。

4. 窗体事件定义

LoginEventDefine.java & 窗体事件定义

public class LoginEventDefine {

    private LoginInit loginInit;
    private ILoginEvent loginEvent;
    private ILoginMethod loginMethod;

    public LoginEventDefine(LoginInit loginInit, ILoginEvent loginEvent, ILoginMethod loginMethod) {
        this.loginInit = loginInit;
        this.loginEvent = loginEvent;
        this.loginMethod = loginMethod;

        loginInit.move();
        min();
        quit();
        doEventLogin();}

    // 事件;最小化
    private void min() {
        loginInit.login_min.setOnAction(event -> {loginInit.setIconified(true);
        });}

    // 事件;退出
    private void quit() {
        loginInit.login_close.setOnAction(event -> {loginInit.close();
            System.exit(0);
        });}

    // 事件;登陆
    private void doEventLogin() {
        loginInit.login_button.setOnAction(event -> {loginEvent.doLoginCheck(loginInit.userId.getText(), loginInit.userPassword.getText());});
    }
}
  • 这里的事件定义基本和前面一样,唯一不同的是我们不需要在这里通过 id 获取元素
  • 事件定义完成后,都会交给构造方法进行初始化。当然,如果后面这部分内容较多,还可以抽象为配置

5. 窗体的初始化操作

LoginInit.java & 窗体的初始化操作

public abstract class LoginInit extends UIObject {

    private static final String RESOURCE_NAME = "/fxml/login/login.fxml";

    protected ILoginEvent loginEvent;

    public Button login_min;          // 登陆窗口最小化
    public Button login_close;        // 登陆窗口退出
    public Button login_button;       // 登陆按钮
    public TextField userId;          // 用户账户窗口
    public PasswordField userPassword;// 用户密码窗口

    LoginInit(ILoginEvent loginEvent) {
        this.loginEvent = loginEvent;
        try {root = FXMLLoader.load(getClass().getResource(RESOURCE_NAME));
        } catch (IOException e) {e.printStackTrace();
        }
        Scene scene = new Scene(root);
        scene.setFill(Color.TRANSPARENT);
        setScene(scene);
        initStyle(StageStyle.TRANSPARENT);
        setResizable(false);
        this.getIcons().add(new Image("/fxml/login/img/system/logo.png"));
        obtain();
        initView();
        initEventDefine();}

    private void obtain() {login_min = $("login_min", Button.class);
        login_close = $("login_close", Button.class);
        login_button = $("login_button", Button.class);
        userId = $("userId", TextField.class);
        userPassword = $("userPassword", PasswordField.class);
    }

}
  • 这个类是一个抽象类,同时继承了 UI 父类方法。
  • 并且在构造函数中执行了初始化操作;initView()initEventDefine()
  • obtain() 方法中可以看到,我们这里就已经初始化获取了基本需要的元素,这样也方面我们后续的使用,不需要重复获取。

6. 窗体的展示

LoginView.java & 窗体的展示

public class LoginView {

    private LoginInit loginInit;
    private ILoginEvent loginEvent;

    public LoginView(LoginInit loginInit, ILoginEvent loginEvent) {
        this.loginInit = loginInit;
        this.loginEvent = loginEvent;
    }

}
  • 这部分内容当前类主要承担着窗体初始化和窗体的事件的一个构造函数,对于后续其他复杂页面初始化更多的预定义元素才会更有用。

7. UI 父类定义

UIObject.java & UI 父类定义

public abstract class UIObject extends Stage {

    protected Parent root;
    private double xOffset;
    private double yOffset;

    public  <T> T $(String id, Class<T> clazz) {return (T) root.lookup("#" + id);
    }

    public void clearViewListSelectedAll(ListView<Pane>... listViews) {for (ListView<Pane> listView : listViews) {listView.getSelectionModel().clearSelection();}
    }

    public void move() {
        root.setOnMousePressed(event -> {xOffset = getX() - event.getScreenX();
            yOffset = getY()- event.getScreenY();
            root.setCursor(Cursor.CLOSED_HAND);
        });
        root.setOnMouseDragged(event -> {setX(event.getScreenX() + xOffset);
            setY(event.getScreenY() + yOffset);});
        root.setOnMouseReleased(event -> {root.setCursor(Cursor.DEFAULT);
        });}

    // 初始化页面
    public abstract void initView();

    // 初始化事件定义
    public abstract void initEventDefine();}
  • 这里我们将一些公用的方法和事件操作抽象为父类,共所有的框体使用
  • 同时我们预定了两个抽象类函数,initView()initEventDefine(),这样主要为了方便统一名称下的初始化操作。尤其在团队编码中,更加重要

六、效果演示

  • 首先我们在 org.itstack.naive.chat.ui.Application 中,添加我们的窗体启动代码,同时我们还实现了事件并传给构造函数;

    public class Application extends javafx.application.Application {
    
      @Override
      public void start(Stage primaryStage) throws Exception {ILoginMethod login = new LoginController((userId, userPassword) -> {System.out.println("登陆 userId:" + userId + "userPassword:" + userPassword);
          });
      login.doShow();}
    
    public static void main(String[] args) {launch(args); } }
  • 点击运行,效果如下;

    登陆框体事件与接口_java

七、总结

  • 在这章节中我们定义了事件和实现了接口,并且我们将原有代码进行重构,以更加适合扩展的方式进行编码。
  • 关于最初的代码和重构的结构,需要好好理解下,也许你心里还有更佳的方案,那么也是可以尝试的,找到自己最合适的。
  • 等到我们开始实现具体业务流程开发的时候,就会体会到这么拆解架构的原因。因为我们的目标是让 UI 里不要写逻辑,保持 前后端分离