不久前,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编写一个应用程序,运行并展示一个窗口:
0.2x版本的winit的运行模型主要基于过程式:
- 创建事件循环
- 创建该事件循环关联的窗体
- 启动事件循环
尽管使用起来比较简单,但是实际的应用场景会比较复杂,考虑到多窗体情况,这块的代码会愈发的复杂,需要用户做出适当的封装,才能让代码更加的清晰。
关于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
,内容如下:
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有两个必须实现的方法:resumed
和window_event
方法。
先看window_event
方法。该在窗口事件发生时被调用,这块其实就是0.2x版本中事件循环中的触发事件的封装。但值得注意的是,在该方法的2个入参:
event_loop: &ActiveEventLoop
window_id: WindowId
这两个参数从含义上讲,代表了当前正激活的事件循环以及与之匹配的窗口。这里就不难理解,winit的0.30.0的新模型,主要是为了以友好的接口方式来支持多窗体、多事件循环。我们可以通过该事件回调,来得到当前是哪个窗体触发,在哪个激活的事件循环中触发的窗体事件。
再看resumed
方法。官方文档:ApplicationHandler#resumed,笔者简单总结下:
- 所有的平台(桌面端、Android、iOS以及Web)都会 Resumed 事件,各个平台触发该事件是对应了相关平台应用生命周期的某个阶段(例如,iOS中对应
applicationDidBecomeActive
)。 - 考虑多平台可以移植性,推荐建议应用程序在收到第一个 Resumed 事件后仅初始化其图形上下文并创建窗口。由于系统平台的事件驱动具体实现的差异,可能会调用多次,要做“幂等”处理,确保在收到 Resumed 事件后仅初始化一次图形上下文和窗口(比如,iOS上只要激活了就会触发一次,如果没做幂等处理,就会在每次激活时都初始化一次图形上下文和窗口)。
鉴于上述说明,我们在App结构体中增加一个字段:window: Option<winit::window::Window>
,稍后我们会在resumed
方法中创建窗口,并把它存储在这个字段中,同时给App加上Default特性以便于快速创建App实例:
// 添加 Default 以便App::default()来快速创建App实例
#[derive(Default)]
pub struct App {
window: Option<winit::window::Window>,
}
接着,我们在resumed
方法中创建窗口,并把它存储在window
字段中:
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以及运行该应用的代码:
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,我们将整个应用程序的生命周期抽象出来,并通过事件回调的方式,来处理窗体事件。
上述代码运行以后,会在桌面出现一个窗体,不过此时你还无法点击窗体关闭按钮关闭它。因为我们没有实现对应的窗体退出逻辑,让我们在前面的ApplicationHandler
的window_event
方法中,处理下退出事件:
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,十分感谢。