eduwin WebView2 模块 API

头文件:通过 eduwin.h 统一引用(@public_api 已自动合并)

封装 Microsoft Edge WebView2,在 Eduwin 窗口中嵌入现代浏览器引擎。纯 C 实现,通过 COM lpVtbl + 手写 vtable 回调接入 WebView2 的三层 API(Loader flat 入口 / ICoreWebView2 接口调用 / 异步 CompletedHandler 回调)。

两种 Runtime 模式:

模式Runtime 来源包体客户端要求
Evergreen(默认)系统共享需预装 Runtime
Fixed Version应用本地打包~150MB零依赖

⚠️ Runtime 获取: eduwin_pub.zip 不含 WebView2 Runtime(体积过大)。

  • Evergreen(推荐开发/测试用):下载 Evergreen Bootstrapper(约 2MB),双击安装,重启即可
  • Fixed Version(推荐分发用):下载 Fixed Version Runtime(x64,约 150MB),解压后目录传给 webview_set_runtime_path()

两种下载均在同一页面,选择 "Evergreen Bootstrapper" 或 "Fixed Version" → 架构选 x64。

初始化 / 清理

void webview_set_runtime_path(const WCHAR *path);  /* 必须在 webview_init() 之前 */
int  webview_init(void);
void webview_cleanup(void);

webview_set_runtime_path() 设置 Runtime 路径。NULL = Evergreen 模式(默认),传路径 = Fixed Version 模式。

webview_init() 必须在 init_app() 之前调用。返回值 0 成功,非零失败。

webview_cleanup()run_app() 返回后调用,释放全局 COM 环境。

/* Fixed Version:客户端零依赖 */
webview_set_runtime_path(L"../runtime/");
if (webview_init() != 0) {
    init_app();
    alert(L"错误", L"WebView2 Runtime 初始化失败");
    return 1;
}
/*
 * Evergreen(默认):
 *   不调 webview_set_runtime_path,webview_init() 自动用系统 Runtime。
 *   Windows 11 已预装,Windows 10 需手动安装。
 */
init_app();
// ... 创建 WebView 窗口 ...
run_app();
webview_cleanup();

创建 WebView

HWND create_webview(HWND parent, int x, int y, int w, int h);

在父窗口中创建 WebView2 子控件。参数与 create_button / create_label 一致:(x, y) 左上角坐标,(w, h) 宽高。

WebView 的 Environment 和 Controller 是异步创建的,函数返回后 WebView 尚未就绪——需配合 on_webview_ready 回调使用。

HWND win = create_window(L"WebView Demo", 960, 640);
HWND wv = create_webview(win, 0, 0, 960, 600);

导航 & 内容

void webview_navigate(HWND hwv, const WCHAR *url);
void webview_load_html(HWND hwv, const WCHAR *html);

webview_navigate 导航到指定 URL。通常在 on_webview_ready 回调中调用,确保 WebView 已就绪。

webview_load_html 加载 HTML 字符串(<html>...</html>)。

如果在 ready 之前调用,请求会自动排队,WebView 就绪后执行。

static void on_ready(HWND hwv, void *data) {
    webview_navigate(hwv, L"https://www.bing.com");
    // 或:
    // webview_load_html(hwv, L"<html><body><h1>Hello</h1></body></html>");
}

就绪回调

typedef void (*webview_ready_fn)(HWND hwv, void *data);
void on_webview_ready(HWND hwv, webview_ready_fn cb, void *data);

WebView2 异步初始化完成后触发。这是安全调用 webview_navigate / webview_load_html / webview_post_message 的时机。

JS 双向通信

C 与 WebView 中页面可通过以下两种方式进行双向通信。

方式一:原始 postMessage

typedef void (*webview_message_fn)(HWND hwv, const char *json, void *data);
void on_webview_message(HWND hwv, webview_message_fn cb, void *data);
void webview_post_message(HWND hwv, const char *json);

C → JS: webview_post_message 向 WebView 中页面发送 JSON 字符串。页面通过 window.chrome.webview.addEventListener('message', ...) 接收。

JS → C: 页面通过 window.chrome.webview.postMessage(json) 发送消息,C 端通过 on_webview_message 注册的回调接收。

// C 端接收 JS 消息
static void on_msg(HWND hwv, const char *json, void *data) {
    // json 是从 JS 发来的字符串
    log_info(L"收到 JS 消息");
    // 回复消息给 JS
    webview_post_message(hwv, "{\"reply\": \"你好 JS\"}");
}

// 注册
on_webview_message(wv, on_msg, NULL);

页面端示例:

// JS 端发送消息到 C
window.chrome.webview.postMessage(JSON.stringify({ type: "click", value: 42 }));

// JS 端接收 C 消息
window.chrome.webview.addEventListener("message", (e) => {
    console.log("C 发来:", e.data);
});

方式二:Bridge(推荐)

Bridge 在页面启动时自动注入 window.eduwin 对象,封装了原始 postMessage,提供更简洁的双向调用方式。

void webview_bridge_init(HWND hwv);

启用 Bridge。调用后 WebView 中所有页面自动获得 window.eduwin.on_message(cb)window.eduwin.call(func, params, callback) 方法。

typedef void (*webview_bridge_fn)(HWND hwv, const char *func_name, const char *params_json, void *data);
void webview_bridge_register(const char *func_name, webview_bridge_fn cb, void *data);

注册一个 C 端 bridge 处理函数,供 JS 通过 eduwin.call(func_name, params) 调用。params_json 是参数字符串(JSON 格式),如 {"name":"eduwin"}

/* C 端注册 */
static void on_hello(HWND hwv, const char *func, const char *params, void *d) {
    webview_post_message(hwv, "{\"reply\":\"你好!\"}");
}
webview_bridge_register("hello", on_hello, NULL);
/* JS 端调用 */
eduwin.call("hello", { name: "eduwin" }, function (r) {
    console.log(r.reply); // "你好!"
});

Bridge 的底层仍基于 postMessage,消息类型为 { type: "bridge_call", func, params, id }。C 端自动路由到已注册的处理函数,未被识别为 bridge 的消息仍会传给 on_webview_message 回调。

导航控制

void webview_back(HWND hwv);
void webview_forward(HWND hwv);
void webview_reload(HWND hwv);

浏览器的后退、前进、刷新操作。仅在 WebView 就绪后有效。

导航完成回调

typedef void (*webview_nav_complete_fn)(HWND hwv, int success, int status_code, void *data);
void on_navigation_completed(HWND hwv, webview_nav_complete_fn cb, void *data);

注册导航完成回调。页面加载完成后触发,success 为 1 表示成功,0 表示失败。

static void on_nav(HWND hwv, int success, int code, void *d) {
    set_text(g_status, success ? L"Done" : L"Failed");
}
on_navigation_completed(wv, on_nav, NULL);

Virtual Host — 加载本地网页文件

将本地文件夹映射为虚拟域名,绕过 file:/// 的限制,正常加载外部 HTML/CSS/JS。

void webview_set_virtual_host(HWND hwv, const WCHAR *host_name, const WCHAR *folder_path);

host_name 为虚拟域名(如 L"eduwin.local"),folder_path 为本地文件夹路径(相对路径基于 exe 所在目录,也支持绝对路径)。

/* 映射当前 exe 目录到 eduwin.local */
webview_set_virtual_host(hwv, L"eduwin.local", L".");

/* 然后通过 https 访问本地文件 */
webview_navigate(hwv, L"https://eduwin.local/index.html");

Virtual Host 的访问权限为 COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS,禁止跨域请求但允许正常加载同域资源。

完整示例

参见:

  • samples/webview_browser/ —— 简易浏览器(工具栏 + 地址栏 + 状态栏)
  • samples/webview_bridge/ —— Bridge 演示(C ↔ JS 双向通信 + 外部 HTML/CSS/JS 加载)

链接库

使用 WebView2 模块时,build.bat 需额外链接:

link ... WebView2LoaderStatic.lib ole32.lib oleaut32.lib version.lib advapi32.lib
用途
WebView2LoaderStatic.libWebView2 环境加载器
ole32.libCOM 基础(CoInitializeEx 等)
oleaut32.libCOM 自动化
version.lib版本信息查询
advapi32.lib注册表(检测 WebView2 Runtime)

实现架构

eduwin_webview.c (纯 C)
├── 模式 A: Flat API ── CreateCoreWebView2EnvironmentWithOptions(...)
├── 模式 B: lpVtbl ──── controller->lpVtbl->put_Bounds(...)
│                       webview->lpVtbl->Navigate(...)
└── 模式 C: 回调 vtable ─ env_handler_t / ctrl_handler_t / msg_handler_t
                         (手写 QueryInterface / AddRef / Release / Invoke)

三种 COM 互操作模式全部在纯 C 中完成,未引入 .cpp 文件。