3 releases
0.1.2 | Nov 26, 2023 |
---|---|
0.1.1 | Nov 26, 2023 |
0.1.0 | Nov 25, 2023 |
#239 in GUI
49KB
279 lines
nwg-webview-ctrl
封装Microsoft Edge WebView2浏览器内核为Native Windows GUI (i.e. NWG crate)开发框架的WebView
图形控件 — 具体包括
WebviewContainer
自定义控件和WebviewContainerBuilder
控件构建器。
进而,给Rust Win32 Bindings
增添WebView
图形控件新成员。
相较人气爆棚的Tauri crate,nwg-webview-ctrl
允许WebView
参与原生图形控件的布局管理,包括但不限于:
- 网格布局
GridLayout
- 弹性布局
FlexboxLayout
- 动态布局
DynLayout
WebviewContainer
图形控件的功能定位等同于OSX Cacao crate图形界面开发框架中的cacao::webview::WebView控件。它们都力图凭借构建功能丰富的原生图形界面,重塑原生图形交互在应用程序中的【主体地位】,而不只是陪衬作为H5
网页程序的套壳浏览器“附属品”(— 至多也就是位“收租公”)。要说有差别,那也仅是
cacao::webview::WebView
封装的是Apple Webkit Webview
WebviewContainer
套壳的是Microsoft Edge Webview2
运行环境要求
- 预安装
Microsoft Edge 86+
浏览器的Windows
操作系统。或 - 已安装Evergreen WebView2 Runtime的其它版本
Windows
操作系统。
一般来讲,Windows 11
与打过升级补丁的Windows 10
都可以直接运行包含了此图形控件的应用程序。
编译环境要求
Cargo Package
配置已经锁定了nightly-x86_64-pc-windows-msvc
工具链。虽然stable channel
工具链也可成功编译,但工具链的GNU (ABI) build
却会导致编译时链接WebView2 Runtime
失败。
此外,编译环境也需要给Windows
操作系统预安装Microsoft Edge 86+
或Evergreen WebView2 Runtime
。
我的贡献
nwg-webview-ctrl crate
并没有直接调用浏览器内核的Win32 COM ABI。而是,站在巨人的肩膀上,将webview2 crate套壳封装于NWG
图形开发框架的nwg::Frame控件内。最后,以nwg::Frame
控件为“代理”参与原生控件布局。
题外话,
nwg::Frame
控件本就是NWG
图形开发框架对第三方扩展提供的“接入插槽”。nwg-webview-ctrl crate
也算是第三方扩展了。
以图描述更直观,一图抵千词。
Webview
初始化是异步的
这不是我定的,而是从Win32 COM
那一层开始就已经是异步回调了。然后,再经由webview2 crate
透传至nwg-webview-ctrl
封装代码。但,WebviewContainer
还是做了些使生活更美好的工作 — 将【异步回调】变形为
-
要么,futures::future::FusedFuture的异步阻塞
// 这是【伪码】呀!真实的【返回值】类型会更复杂,但本质如下。 WebviewContainer::ready_fut(&self) -> FusedFuture<Output = (Environment, Controller, WebView)>
将该成员方法返回值直接注入【异步块
Task
】。再将NWG
事件循环作为【反应器Reactor
】对接futures crate
的【执行器Executor
】,以持续轮询推进【异步块Task
】的程序执行。这是我非常推荐的用法,也是
examples
采用的代码套路。 -
要么,futures::executor::block_on(Future)的同步阻塞
// 这是【伪码】呀!真实的【返回值】类型会更复杂,但本质如下。 WebviewContainer::ready_block(&self) -> (Environment, Controller, WebView)
该成员方法内部会调用
futures::executor::block_on()
阻塞当前线程。特别注意:该成员方法仅能在同步上下文中被调用。否则,会导致应用程序运行崩溃!
虽然Webview
初始化是异步的,但WebviewContainerBuilder
控件构造器自身却未执行任何(同/异步)阻塞操作,而仅只
- 构造
nwg::Frame
控件占位原生布局流 - 开启
webview2::Controller(i.e.
Microsoft.Web.WebView2.Core.CoreWebView2Controller)
异步初始化流程,却不等待初始化处理结束
然后,由WebviewContainer
控件“亲自”阻塞程序执行,和等待Webview
完全就绪
同步阻塞
除了简单,啥也不是。
let mut webview_container = WebviewContainer::default();
// builder 自身是不阻塞的
WebviewContainer::builder().parent(&window).window(&window).build(&mut webview_container)?;
// 由控件对象的成员方法阻塞主线程,和等待 Webview 完全就绪
let (_, _, webview) = webview_container.ready_block().unwrap();
webview.navigate("https://www.minxing365.com").unwrap();
异步阻塞
仅四步便点亮Native GUI
【异步编程】科技树 — 绝对值得拥有:
- 构造一个异步任务
Task
- 构造一个单线程异步执行器
Executor
- 将异步执行器对接
NWG
事件循环,和将NWG
事件循环作为Reactor
- 将
Webview
初始化FusedFuture
对象捕获入异步任务Task
let mut webview_container = WebviewContainer::default();
// builder 自身是不阻塞的
WebviewContainer::builder()
.parent(&window) // nwg::Frame 控件的父控制是主窗体 window
.window(&window) // webview2::Controller 的关联主窗体也是相同的 window
.build(&mut webview_container).unwrap();
// 1. 构造一个异步任务
let webview_ready_fut = webview_container.ready_fut().unwrap();
// 2. 构造一个单线程异步执行器
let mut executor = {
let executor = LocalPool::new();
executor.spawner().spawn_local(async move {
// 4. 将异步任务注入异步执行器
let (_, _, webview) = webview_ready_fut.await;
webview.navigate("https://www.minxing365.com").unwrap();
Ok::<_, Box<dyn Error>>(())
}).unwrap();
executor
};
// 3. 将异步执行器对接`NWG`事件循环
nwg::dispatch_thread_events_with_callback(move || executor.run_until_stalled());
Webview
初始化成功的返回值
返回值是三元素元组。其三个子元素依次是
-
webview2::Environment(i.e.
Microsoft.Web.WebView2.Core.CoreWebView2Environment)
在多
TAB
签场景下,此返回值允许多个webview2::Controller
实例共享同一个webview2::Environment
构造源。于是,多个同源webview2::Controller
实例就能共用一套- 浏览器进程
- 渲染进程
- 缓存目录
-
webview2::Controller(i.e.
Microsoft.Web.WebView2.Core.CoreWebView2Controller)
面向整个应用程序中的原生部分,实现
-
焦点传递
-
DPI
级的整体缩放 -
改变整体背景色
-
挂起/恢复渲染进程。
WebviewContainer
控件内部正在调用该接口,并- 在主窗体被隐藏
NwgEvent::OnWindowMinimize
时,挂起Webview
渲染进程 - 在主窗体被弹出
(WM_SYSCOMMAND, SC_RESTORE)
时,恢复Webview
渲染进程
- 在主窗体被隐藏
-
同步发送主窗体的
UI
状态信息给CoreWebView2Controller
。WebviewContainer
控件内部就正在监听- 主窗体的位移事件
NwgEvent::OnMove
nwg::Frame
父控件的- 尺寸变化事件
NwgEvent::OnResize
- 位移事件
NwgEvent::OnMove
- 尺寸变化事件
和传递最新的位置与尺寸信息给
CoreWebView2Controller
。 - 主窗体的位移事件
-
析构掉整个
Webview
控件(包括CoreWebView2Controller
和CoreWebView2
)。WebviewContainer
控件已被实现为底层Webview
控件的RAII
守卫。即,只要WebviewContainer
控件被析构,那么Webview
控件也将同步地被释放。
-
-
webview2::WebView(i.e.
Microsoft.Web.WebView2.Core.CoreWebView2)
面向应用程序中网页部分,实现
native <-> javascript
桥- 直接操控网页内容
- 定制与替换浏览器弹出对话框。
- 拦截与篡改网络请求
- 拦截与篡改网页路由
目前,被用于布局占位的nwg::Frame
控件实例还尚未对外可见。
WebviewContainer
的构造与配置
WebviewContainer
控件支持API
与【派生宏】两种实例化方式
API
实例化模式
// 构造主窗体
let mut window = Window::default();
// 配置主窗体
Window::builder().title("内嵌 WebView 例程").size((1024, 168)).build(&mut window)?;
nwg::full_bind_event_handler(&window.handle, move |event, _data, _handle| {
if let NwgEvent::OnWindowClose = event { // 关闭主窗体。
nwg::stop_thread_dispatch();
}
});
// 构造 Webview 控件
let mut webview_container = WebviewContainer::default();
// 配置 Webview 控件
WebviewContainer::builder()
.parent(&window) // 指定紧上一级控件
.window(&window) // 指定主窗体。在本例中,【主窗体】即是【紧上一级控件】
.flags(WebviewContainerFlags::VISIBLE) // 指定不显示控件边框
.build(&mut webview_container)?;
// 构造 网格布局
let mut grid = GridLayout::default();
// 配置 网格布局
GridLayout::builder()
.margin([0; 4]) // 白边
.max_column(Some(1)) // 网络总列数
.max_row(Some(1)) // 网络总行数
.parent(&window) // 指定给谁布局
.child(0, 0, &webview_container) // 给布局加入子控件。在本例中,唯一的子控件就是 Webview
.build(&mut grid)?;
// 构造【异步·执行器】与【异步·任务】
let mut executor = {
let executor = LocalPool::new();
let webview_ready_fut = webview_container.ready_fut()?;
executor.spawner().spawn_local(async move {
// 在这可以发起能够与 webview 初始化并行工作的异步任务。比如,
// 1. 请求后端接口。
// 2. 读取配置文件
// 然后,再将这些 Future 实例与 webview 初始化 FusedFuture 实例 futures::join! 在一起。
// ....
// ....
let (_, _, webview) = webview_ready_fut.await;
// 执行直接依赖于 webview 实例的业务处理功能。
// 比如,跳转至【欢迎页】
webview.navigate(&cli_params.url)?;
Ok::<_, Box<dyn Error>>(())
}.map(|result| {
if let Err(err) = result {
eprintln!("[app_main]{err}");
}
}))?;
executor
};
// 阻塞主线程,等待用户手动关闭主窗体
nwg::dispatch_thread_events_with_callback(move ||
// 以 win32 UI 的事件循环为【反应器】,对接 futures crate 的【执行器】
executor.run_until_stalled());
Ok(())
执行命令cargo run --example nwg-remote-page
可直接运行该例程。
【派生宏】模式
将层叠嵌套的数据结构映射为分区划块的图形界面。这似乎能少写相当一部分重复代码。
// 以数据结构定义与映射图形界面布局。
#[derive(Default, NwgUi)]
pub struct DemoUi {
// 主窗体
#[nwg_control(size: (1024, 168), title: "内嵌 WebView 例程", flags: "MAIN_WINDOW|VISIBLE")]
#[nwg_events(OnWindowClose: [nwg::stop_thread_dispatch()])]
window: Window,
// 布局对象
#[nwg_layout(margin: [0; 4], parent: window, max_column: Some(1), max_row: Some(1), spacing: 0)]
grid: GridLayout, // 网格布局主窗体
// webview 控件
#[nwg_control(flags: "VISIBLE", parent: window, window: &data.window)]
#[nwg_layout_item(layout: grid, row: 0, col: 0)]
webview_container: WebviewContainer, // 向网格布局填入唯一的子控件
}
impl DemoUi {
// 构造【异步·执行器】与【异步·任务】
fn executor(&self, url: &str) -> Result<LocalPool, Box<dyn Error>> {
let executor = LocalPool::new();
let webview_ready_fut = self.webview_container.ready_fut()?;
executor.spawner().spawn_local(async move {
// 在这可以发起能够与 webview 初始化并行工作的异步任务。比如,
// 1. 请求后端接口。
// 2. 读取配置文件
// 然后,再将这些 Future 实例与 webview 初始化 FusedFuture 实例 futures::join! 在一起。
// ....
// ....
let (_, _, webview) = webview_ready_fut.await;
// 执行直接依赖于 webview 实例的业务处理功能。
// 比如,跳转至【欢迎页】
webview.navigate(url)?;
Ok::<_, Box<dyn Error>>(())
}.map(|result| {
if let Err(err) = result {
eprintln!("[app_main]{err}");
}
}))?;
Ok(executor)
}
}
fn main() -> Result<(), Box<dyn Error>> {
let demo_ui_app = DemoUi::build_ui(Default::default())?;
// 构造【异步·执行器】
let mut executor = demo_ui_app.executor("https://www.minxing365.com")?;
// 阻塞主线程,等待用户手动关闭主窗体
nwg::dispatch_thread_events_with_callback(move ||
// 以 win32 UI 的事件循环为【反应器】,对接 futures crate 的【执行器】
executor.run_until_stalled());
Ok(())
}
执行命令cargo run --example nwd-remote-page
可直接运行该例程。
WebviewContainerBuilder
配置参数是nwg::Frame
与webview2::Environment(i.e.
Microsoft.Web.WebView2.Core.CoreWebView2Environment)
的合集
后续出现的文字链都直接关联至
Microsoft MSDN
的Win32
线上文档,因为
Rust docs
实在太稀缺Rust Binding
对Win32 COM ABI
几乎是1:1
映射的,所以直接阅读Microsoft MSDN
就足够理解接口功能了。另外,Rust Binding
仍尚未全面覆盖每个Win32 COM ABI
,所以别看到什么高级功能就兴奋得不要不要的,还得确认它是否已经被webview2-sys crate
绑定?
占位原生布局流
依赖于来自nwg::FrameBuilder
的配置参数flags
, size
, position
, enabled
, 和parent
。这些配置
- 参数名与底层
nwg::FrameBuilder
签名保持一致,而 - 参数值仅被透传给
nwg::FrameBuilder
实例
webview2::Environment(i.e.
CoreWebView2Environment)
初始化
依赖于来自webview2::EnvironmentBuilder
的配置参数 browser_executable_folder, user_data_folder, language, target_compatible_browser_version, additional_browser_arguments,
allow_single_sign_on_using_osprimary_account。这些参数的含义与用法,请点开链接自己读吧。Microsoft MSDN
文档写得极精细。
WebviewContainerBuilder
独有的参数
window: nwg::Window
- 【必填】图形应用程序的主窗体句柄。即便
WebviewContainer
控件的父控件就是应用程序的主窗体,该参数也得显式地传递 — 像例程里那样。
- 【必填】图形应用程序的主窗体句柄。即便
webview_env: webview2::Environment
- 【可选】在多
TAB
场景下,共享相同的webview2::Environment
构造源
- 【可选】在多
Webview
操控接口
后续出现的文字链都直接关联至
Microsoft MSDN
的Win32
线上文档,因为
Rust docs
实在太稀缺Rust Binding
对Win32 COM ABI
几乎是1:1
映射的,所以直接阅读Microsoft MSDN
就足够理解接口功能了。另外,Rust Binding
仍尚未全面覆盖每个Win32 COM ABI
,所以别看到什么高级功能就兴奋得不要不要的,还得确认它是否已经被webview2-sys crate
绑定?
按照“(对外)面向原生图形界面上下文hosting-related
”与“(对内)面向网页内容web-specific
”的分类标准,Webview API
被分别挂到
两个类实例上 — 对Webview API
的分类也是从Win32 COM
那一层就开始了,而不是我搞的。
在webview2::Controller
上的Webview API
包括:
- 关联图形应用程序的主窗体
- 析构整个
Webview
控件 - 读写
Webview
默认背景色 - 读写
Webview
显示尺寸 - 读写
Webview
显示位置 - 否挂起或恢复
Webview
控件- 主窗口最小化时,推荐挂起
Webview
控件,以降低耗电 - 主窗口恢复正常大小时,再恢复
Webview
控件。
- 主窗口最小化时,推荐挂起
- 缩放
Webview
原生控件。涵盖了:- 网页内容,
- 弹出对话框
- 上下文菜单
- 滚动条
- 仅缩放
Webview
内的网页内容 - 监听
Webview
的聚焦/失焦事件,以及焦点在不同原生控件之间转移的事件 - 监听来自键盘的
Ctrl / Alt +
任意键的组合键敲击事件 - 监听来自键盘的不可打印字符输入事件
在webview2::WebView
上的Webview API
包括:
- 原生<->
js
桥 - 浏览器功能
- 进程管理
- 网页内容
其它Webview API
包括:
- 抑制触屏设备上的左滑倒退,右滑前进,下拉刷新的手势识别
- 这个功能默认就已经是被关闭了,除非给
Webview
的额外启动参数AdditionalBrowserArguments
添加--pull-to-refresh
- 这个功能默认就已经是被关闭了,除非给
- 抑制
PDF
视图的工具条 - 变换
Webview
主题色 - 获取当前网页是如何路由打开的
这个汇总列表直接参考自Microsoft MSDN
文档。其中有些Win32 COM ABI
接口还没有被webview2-sys crate
封装,所以需要亲自动手编写FFI
代码才能正常调用许多高级功能接口。
Dependencies
~6MB
~90K SLoC