Rust winit 0.30.0版本简介

2024-05-16 16:41:24 浏览数 (1)

不久前,Rust著名的跨平台窗体管理库winit发布了它的0.30.0版本,较之前的0.2x.x版本,新增了19个的模块API,改动大约19个模块API,移除了大约8个模块API。可见本次升级改动之大,主要是对事件循环、窗口管理的重构。鉴于目前网上较多的文章都是基于0.2x版本的winit的代码,存在时效性问题,所以我决定写一篇文章,对winit的0.30.0版本做一个简单的介绍,同时也为后面的Rust Wgpu系列文章做铺垫。

关于0.2x版本winit

为了呈现清晰的对比,我们先给一关于0.2x版本的winit编写一个应用程序,运行并展示一个窗口:

010-v0_2x_winit010-v0_2x_winit

0.2x版本的winit的运行模型主要基于过程式:

  1. 创建事件循环
  2. 创建该事件循环关联的窗体
  3. 启动事件循环

尽管使用起来比较简单,但是实际的应用场景会比较复杂,考虑到多窗体情况,这块的代码会愈发的复杂,需要用户做出适当的封装,才能让代码更加的清晰。

关于0.30.0版winit

关于0.30.0版本的winit,则新增ApplicationHandler,来对整个应用程序进行抽象,并把窗体创建、事件处理,收敛到了应用程序这个抽象中,提供更加直观的API。具体是怎样呢?话不多说,让我们通过代码实践来理解。首先初始化一个项目(这里不再赘述,请读者自行创建基础空项目),添加0.30.0版本winit依赖:

代码语言:javascript复制
[dependencies]
winit = { version = "0.30.0" }

接着,为了后续项目结构的划分,我们在main.rs同级目录下创建一个名为app.rs,内容如下:

代码语言:javascript复制
use winit::application::ApplicationHandler;
use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::window::WindowId;

pub struct App {}

impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {}

    fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {}
}

在新版的winit中,我们首先定义一个自定义结构体:App,它代表了我们运行的应用程序;接着,我们为App实现来自winit 0.30.0中的新trait:ApplicationHandler。该trait有两个必须实现的方法:resumedwindow_event方法。

先看window_event方法。该在窗口事件发生时被调用,这块其实就是0.2x版本中事件循环中的触发事件的封装。但值得注意的是,在该方法的2个入参:

  1. event_loop: &ActiveEventLoop
  2. window_id: WindowId

这两个参数从含义上讲,代表了当前正激活的事件循环以及与之匹配的窗口。这里就不难理解,winit的0.30.0的新模型,主要是为了以友好的接口方式来支持多窗体、多事件循环。我们可以通过该事件回调,来得到当前是哪个窗体触发,在哪个激活的事件循环中触发的窗体事件。

再看resumed方法。官方文档:ApplicationHandler#resumed,笔者简单总结下:

  1. 所有的平台(桌面端、Android、iOS以及Web)都会 Resumed 事件,各个平台触发该事件是对应了相关平台应用生命周期的某个阶段(例如,iOS中对应applicationDidBecomeActive)。
  2. 考虑多平台可以移植性,推荐建议应用程序在收到第一个 Resumed 事件后仅初始化其图形上下文并创建窗口。由于系统平台的事件驱动具体实现的差异,可能会调用多次,要做“幂等”处理,确保在收到 Resumed 事件后仅初始化一次图形上下文和窗口(比如,iOS上只要激活了就会触发一次,如果没做幂等处理,就会在每次激活时都初始化一次图形上下文和窗口)。

鉴于上述说明,我们在App结构体中增加一个字段:window: Option<winit::window::Window>,稍后我们会在resumed方法中创建窗口,并把它存储在这个字段中,同时给App加上Default特性以便于快速创建App实例:

代码语言:javascript复制
  // 添加 Default 以便App::default()来快速创建App实例
  #[derive(Default)]
pub struct App {
    window: Option<winit::window::Window>,
}

接着,我们在resumed方法中创建窗口,并把它存储在window字段中:

代码语言:javascript复制
impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        // 如果窗口为创建,我们就创建一个窗口
        if self.window.is_none() {
            let win_attr = Window::default_attributes().with_title("demo");
            let window = event_loop.create_window(win_attr).unwrap();
            self.window = Some(window);
        }
    }
    fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {}
}

上述的代码,通过判断self.window.is_none(),我们可以避免重复创建窗口。

至此,我们的app.rs中的代码就编写完毕了。接下来,我们需要在main.rs中增加创建App以及运行该应用的代码:

代码语言:javascript复制
use winit::event_loop::EventLoop;
use crate::app::App;

mod app;

fn main() {
    let event_loop = EventLoop::new().unwrap();
    let mut app = App::default();
    event_loop.run_app(&mut app).expect("run app error.");
}

其实,读者可以感受到,新版本的winit下的应用程序运行模型,更加好进行模块的划分了。通过ApplicationHandler,我们将整个应用程序的生命周期抽象出来,并通过事件回调的方式,来处理窗体事件。

上述代码运行以后,会在桌面出现一个窗体,不过此时你还无法点击窗体关闭按钮关闭它。因为我们没有实现对应的窗体退出逻辑,让我们在前面的ApplicationHandlerwindow_event方法中,处理下退出事件:

代码语言:javascript复制
impl ApplicationHandler for App {
    fn resumed(&mut self, event_loop: &ActiveEventLoop) {
        // ...
    }

    fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) {
        match event {
            // 处理退出
            WindowEvent::CloseRequested => {
                event_loop.exit();
            }
            _ => ()
        }
    }
}

至此,我们就能显示一个空白的窗体,并能通过点击关闭按钮关闭它。当然,有读者在macOS关闭窗体时,会出现如下panic:

代码语言:javascript复制
a delegate was not configured on the application
stack backtrace:
   0: rust_begin_unwind
   ... ...

这是0.30.0的BUG,具体可以参考issue,该问题会在0.30.1版本修复。

写在最后

在本文中,笔者对winit的0.30.0版本的主要变动进行简单的介绍,更多的内容还需要读者自行阅读官方文档以及examples。当然,相信通过本篇文章,不难看出,新版的winit,对其运行模型架构进行了重构,使得其更加易于使用,更符合现代GUI框架的运行模型思路。

但是,由于其架构升级,导致一些现阶段网络上一些经典的文章,可能无法在新版的winit下正确运行,例如《学习 Wgpu》就还是使用的0.29版本。笔者后续会开启关于Rust Wgpu系列文章,会使用新版winit来进行项目的搭建,并且讲解其中一些在新版winit下的Wgpu构建的注意点,敬请期待。

本文完整代码就不单独放库了,主要是概念讲解。读者可以直接参考官方文档的简单例子。

PS:笔者虽然还没有编写Rust Wgpu系列文章,但其基于winit 0.30.0版本的example已经在开发编写中了,笔者可以在这个仓库中checkout代码:w4ngzhen/wgpu_winit_example,也欢迎读者给个star,十分感谢。

0 人点赞