#webview2 #edge #winapi #api-bindings #web-gui #nwg #nwd

nwg-webview-ctrl

封装Microsoft Edge WebView2浏览器内核为Native Windows GUI (i.e. NWG crate)开发框架的WebView图形控件

3 releases

0.1.2 Nov 26, 2023
0.1.1 Nov 26, 2023
0.1.0 Nov 25, 2023

#238 in GUI

MIT license

49KB
279 lines

nwg-webview-ctrl

封装Microsoft Edge WebView2浏览器内核为Native Windows GUI (i.e. NWG crate)开发框架的WebView图形控件 — 具体包括

  1. WebviewContainer自定义控件和
  2. WebviewContainerBuilder控件构建器。

进而,给Rust Win32 Bindings增添WebView图形控件新成员。

相较人气爆棚的Tauri cratenwg-webview-ctrl允许WebView参与原生图形控件的布局管理,包括但不限于:

  1. 网格布局GridLayout
  2. 弹性布局FlexboxLayout
  3. 动态布局DynLayout

WebviewContainer图形控件的功能定位等同于OSX Cacao crate图形界面开发框架中的cacao::webview::WebView控件。它们都力图凭借构建功能丰富原生图形界面,重塑原生图形交互在应用程序中的【主体地位】,而不只是陪衬作为H5网页程序的套壳浏览器“附属品”(— 至多也就是位“收租公”)。要说有差别,那也仅是

  • cacao::webview::WebView封装的是Apple Webkit Webview
  • WebviewContainer套壳的是Microsoft Edge Webview2

运行环境要求

  1. 预安装Microsoft Edge 86+浏览器的Windows操作系统。或
  2. 已安装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控件构造器自身却执行任何(同/异步)阻塞操作,而仅只

  1. 构造nwg::Frame控件占位原生布局流
  2. 开启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【异步编程】科技树 — 绝对值得拥有:

  1. 构造一个异步任务Task
  2. 构造一个单线程异步执行器Executor
  3. 将异步执行器对接NWG事件循环,和将NWG事件循环作为Reactor
  4. 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初始化成功的返回值

返回值是三元素元组。其三个子元素依次是

  1. webview2::Environment(i.e. Microsoft.Web.WebView2.Core.CoreWebView2Environment)

    在多TAB签场景下,此返回值允许多个webview2::Controller实例共享同一个webview2::Environment构造源。于是,多个同源webview2::Controller实例就能共用一套

    • 浏览器进程
    • 渲染进程
    • 缓存目录
  2. webview2::Controller(i.e. Microsoft.Web.WebView2.Core.CoreWebView2Controller)

    面向整个应用程序中的原生部分,实现

    • 焦点传递

    • DPI级的整体缩放

    • 改变整体背景色

    • 挂起/恢复渲染进程。WebviewContainer控件内部正在调用该接口,并

      • 在主窗体被隐藏NwgEvent::OnWindowMinimize时,挂起Webview渲染进程
      • 在主窗体被弹出(WM_SYSCOMMAND, SC_RESTORE)时,恢复Webview渲染进程
    • 同步发送主窗体的UI状态信息给CoreWebView2ControllerWebviewContainer控件内部就正在监听

      • 主窗体的位移事件NwgEvent::OnMove
      • nwg::Frame父控件的
        • 尺寸变化事件NwgEvent::OnResize
        • 位移事件NwgEvent::OnMove

      和传递最新的位置与尺寸信息给CoreWebView2Controller

    • 析构掉整个Webview控件(包括CoreWebView2ControllerCoreWebView2)。WebviewContainer控件已被实现为底层Webview控件的RAII守卫。即,只要WebviewContainer控件被析构,那么Webview控件也将同步地被释放。

  3. 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::Framewebview2::Environment(i.e. Microsoft.Web.WebView2.Core.CoreWebView2Environment)的合集

后续出现的文字链都直接关联至Microsoft MSDNWin32线上文档,因为

  • Rust docs实在太稀缺
  • Rust BindingWin32 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 MSDNWin32线上文档,因为

  • Rust docs实在太稀缺
  • Rust BindingWin32 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包括:

webview2::WebView上的Webview API包括:

其它Webview API包括:

这个汇总列表直接参考自Microsoft MSDN文档。其中有些Win32 COM ABI接口还没有被webview2-sys crate封装,所以需要亲自动手编写FFI代码才能正常调用许多高级功能接口。

Dependencies

~6MB
~92K SLoC