1. 项目概述从零到一构建WebView2窗口如果你已经跟着上一篇文章成功把WebView2的开发环境给搭起来了那恭喜你最磨人的一步已经过去了。接下来咱们要干点真正有成就感的事儿——创建第一个能跑起来的窗口。很多朋友学技术卡就卡在环境配置一旦环境通了后面看着窗口弹出来代码跑起来那个感觉是完全不一样的。这篇文章我们就专注一件事用C和Win32 API配合WebView2控件创建一个最基础的桌面应用程序窗口并在窗口里加载一个网页。别看目标简单这里面涉及到Win32窗口的生命周期管理、消息循环、以及WebView2核心对象的创建与交互是后续所有复杂功能的地基。我会把每一步为什么这么做、可能会遇到什么坑都掰开揉碎了讲清楚。无论你是刚接触Windows桌面开发还是从其他GUI框架转过来想试试WebView2这篇文章都能让你稳稳地上手。2. 核心思路与项目结构设计在动手写代码之前我们得先想明白这个程序要怎么组织。一个典型的、使用了WebView2的Win32桌面程序其核心架构可以看作“两层皮肉一个核心”。两层皮肉指的是Win32原生窗口这是程序的“外壳”负责显示窗口边框、标题栏处理移动、缩放、关闭等系统消息。我们通过注册窗口类、创建窗口实例、运行消息泵来完成这一部分。这是Windows桌面程序的传统手艺必须得会。WebView2控件这是程序的“内容”它作为一个子窗口嵌入到我们创建的Win32主窗口内部。它负责渲染网页内容处理HTML/CSS/JavaScript。WebView2本身是一个复杂的ActiveX/COM组件我们需要通过一套异步API来创建和管理它。一个核心就是连接这两者的桥梁代码。这部分代码负责在合适的时机通常是主窗口创建成功后创建WebView2控件实例并将其“贴”到主窗口的客户区。同时还要处理两者之间的通信比如窗口大小变化时需要通知WebView2控件同步调整大小。基于这个思路一个最小化的项目至少需要以下文件main.cpp程序的入口点包含WinMain函数负责启动消息循环。AppWindow.h/AppWindow.cpp定义和实现我们的主窗口类。我将它命名为AppWindow这个类封装了窗口创建、消息处理以及WebView2控件的初始化逻辑。将窗口逻辑封装成类比把所有代码堆在WinMain里要清晰得多也便于后续扩展。pch.h/pch.cpp(可选)预编译头文件用于加快编译速度。对于小型项目不是必须但养成好习惯很重要。我们的编码顺序将是先打造好Win32窗口这个“壳”确保它能正常显示和响应然后再把WebView2这个“芯”装进去并建立关联。3. 打造Win32应用程序窗口让我们先从最基础的Win32窗口开始。即使你有其他GUI框架的经验理解这套原始机制也大有裨益因为它能让你真正理解Windows桌面程序是如何运作的。3.1 定义窗口类与注册所有Win32窗口都基于一个“窗口类”。你可以把它理解为一个蓝图定义了窗口的外观图标、光标、背景色和行为处理消息的函数。我们需要先填写这个蓝图然后向系统注册它。// AppWindow.h #pragma once #include windows.h #include string class AppWindow { public: AppWindow(HINSTANCE hInstance, int nCmdShow); ~AppWindow(); bool Create(const std::wstring title, int width, int height); HWND GetWindowHandle() const { return m_hWnd; } HINSTANCE GetInstanceHandle() const { return m_hInstance; } // 消息处理函数的友元声明使其能访问私有成员 friend LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); private: HINSTANCE m_hInstance; // 应用程序实例句柄 HWND m_hWnd; // 窗口句柄 std::wstring m_ClassName LWebView2DemoWindowClass; // 自定义窗口类名 };在AppWindow.cpp中我们实现构造函数和Create方法。注册窗口类是Create方法的第一步。// AppWindow.cpp #include AppWindow.h #include stdexcept AppWindow::AppWindow(HINSTANCE hInstance, int nCmdShow) : m_hInstance(hInstance) { // 构造函数主要保存实例句柄窗口创建在Create方法中完成 } bool AppWindow::Create(const std::wstring title, int width, int height) { // 1. 定义并注册窗口类 WNDCLASSEX wc {}; wc.cbSize sizeof(WNDCLASSEX); wc.style CS_HREDRAW | CS_VREDRAW; // 窗口水平或垂直变化时重绘 wc.lpfnWndProc WindowProc; // 关键指定消息处理函数 wc.cbClsExtra 0; wc.cbWndExtra 0; wc.hInstance m_hInstance; wc.hIcon LoadIcon(nullptr, IDI_APPLICATION); // 使用默认应用程序图标 wc.hCursor LoadCursor(nullptr, IDC_ARROW); // 使用标准箭头光标 wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); // 默认窗口背景色 wc.lpszMenuName nullptr; // 无菜单 wc.lpszClassName m_ClassName.c_str(); // 使用我们定义的类名 wc.hIconSm LoadIcon(nullptr, IDI_APPLICATION); if (!RegisterClassEx(wc)) { MessageBox(nullptr, L窗口类注册失败, L错误, MB_ICONERROR); return false; } // ... 后续创建窗口代码 }注意WNDCLASSEX结构体中的lpfnWndProc成员必须指向一个正确的窗口过程函数。这里我们暂时使用一个全局函数WindowProc并通过友元声明让它可以修改AppWindow类的私有成员。这是一种常见的将C类与C风格回调函数结合的方法。另一种更面向对象的方法是使用GWLP_USERDATA来存储this指针在窗口过程中再取出来调用成员函数。3.2 创建窗口实例并显示注册完窗口类我们就可以用这个“蓝图”来创建具体的窗口实例了。bool AppWindow::Create(const std::wstring title, int width, int height) { // ... 接上面的注册窗口类代码 // 2. 创建窗口 m_hWnd CreateWindowEx( 0, // 扩展窗口样式 m_ClassName.c_str(), // 注册的窗口类名 title.c_str(), // 窗口标题 WS_OVERLAPPEDWINDOW, // 窗口样式可重叠、有标题栏、系统菜单、可调整大小等 CW_USEDEFAULT, CW_USEDEFAULT, // 初始x, y位置使用默认 width, height, // 窗口宽度和高度 nullptr, // 父窗口句柄无 nullptr, // 菜单句柄无 m_hInstance, // 应用程序实例句柄 this // 创建参数这里传入this指针后续在WM_CREATE中获取 ); if (!m_hWnd) { MessageBox(nullptr, L窗口创建失败, L错误, MB_ICONERROR); return false; } // 3. 显示并更新窗口 ShowWindow(m_hWnd, SW_SHOW); UpdateWindow(m_hWnd); return true; }这里有几个关键点WS_OVERLAPPEDWINDOW样式组合了最常用的窗口特性包括标题栏、边框、系统菜单、最小化/最大化按钮和可调整大小的边框。这是我们最熟悉的窗口样式。CreateWindowEx的最后一个参数是LPVOID lpParam。我们传入了this指针。这个指针会在窗口创建过程中随着WM_CREATE消息传递到窗口过程函数中。这是我们后续将窗口句柄与C对象实例关联起来的关键。3.3 实现消息处理循环窗口创建出来了但它现在是“静止”的。要让窗口能响应用户操作点击、拖动、关闭必须运行一个消息循环。同时我们需要实现窗口过程函数WindowProc来处理具体的消息。// AppWindow.cpp 中 WindowProc 的实现 LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { // 尝试从窗口的额外数据或创建参数中获取AppWindow实例指针 AppWindow* pThis nullptr; if (message WM_NCCREATE) { // WM_NCCREATE是窗口创建过程中最早的消息之一 // 从lParam中获取CREATESTRUCT其lpCreateParams保存了我们传入的this指针 CREATESTRUCT* pCreate reinterpret_castCREATESTRUCT*(lParam); pThis reinterpret_castAppWindow*(pCreate-lpCreateParams); // 将this指针存储到窗口的GWLP_USERDATA区域方便后续消息获取 SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_castLONG_PTR(pThis)); } else { // 对于其他消息从GWLP_USERDATA中取出this指针 pThis reinterpret_castAppWindow*(GetWindowLongPtr(hWnd, GWLP_USERDATA)); } // 如果成功获取到this指针并且该类有处理消息的方法可以在这里调用。 // 但为了简单和清晰我们目前先在这个全局函数中处理基础消息。 // 后续可以将消息分发到类的成员函数。 switch (message) { case WM_DESTROY: PostQuitMessage(0); // 发送WM_QUIT消息使主消息循环退出 return 0; case WM_SIZE: { // 窗口大小改变时我们需要在这里处理WebView2控件的大小调整 // 暂时留空后续补充 break; } case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); // 如果需要自定义绘制可以在这里进行 // 对于WebView2应用客户区主要由WebView2控件覆盖通常不需要额外绘制 EndPaint(hWnd, ps); break; } default: // 其他未处理的消息交给默认窗口过程处理 return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }现在我们需要在程序的入口点WinMain中将上述所有部分串联起来创建AppWindow实例运行消息循环。// main.cpp #include AppWindow.h int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { // 忽略未使用的参数 UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // 初始化COM库。WebView2是基于COM的所以这一步至关重要。 // COINIT_APARTMENTTHREADED 是Win32 GUI线程的标准选择。 HRESULT hr CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (FAILED(hr)) { // 错误处理 return -1; } // 创建应用程序窗口 AppWindow mainWindow(hInstance, nCmdShow); if (!mainWindow.Create(L我的第一个WebView2应用, 1024, 768)) { CoUninitialize(); // 清理COM return -1; } // 主消息循环 MSG msg {}; while (GetMessage(msg, nullptr, 0, 0)) { TranslateMessage(msg); // 翻译键盘消息 DispatchMessage(msg); // 将消息分发给窗口过程 } // 程序结束反初始化COM库 CoUninitialize(); return (int)msg.wParam; }编译并运行这段代码你应该能看到一个标准的、可移动、可缩放、有关闭按钮的空白Windows窗口。这是我们整个应用的骨架已经搭建完毕。实操心得在调试Win32窗口程序时如果窗口一闪而过通常是因为消息循环没有正确阻塞例如CreateWindow失败但没检查直接进入了很快退出的循环。务必检查CreateWindow的返回值并在WinMain开始处设置断点逐步执行确保窗口创建成功并进入了while (GetMessage(...))循环。4. 集成WebView2控件骨架有了现在要把“灵魂”——WebView2控件装进去。这是整个教程的核心。WebView2的创建是一个异步过程我们需要遵循“创建环境 - 创建控制器 - 创建核心对象 - 导航到网页”的流程。4.1 引入头文件与库首先确保你的项目配置正确。在AppWindow.h中添加WebView2的头文件并扩展我们的类。// AppWindow.h #pragma once #include windows.h #include string #include wrl.h // 微软的WRLWindows Runtime Library智能指针 #include WebView2.h // WebView2的核心头文件 #include WebView2EnvironmentOptions.h // 环境选项头文件 // 为方便使用WRL的智能指针 using namespace Microsoft::WRL; class AppWindow { public: // ... 之前的公共方法 ... bool InitializeWebView(); // 新增初始化WebView2 private: // ... 之前的私有成员 ... ComPtrICoreWebView2Controller m_controller; // WebView2控制器 ComPtrICoreWebView2 m_webView; // WebView2核心对象 // 异步初始化WebView2的辅助方法 HRESULT CreateWebViewEnvironment(); HRESULT CreateWebViewController(HWND hWnd); void OnWebViewInitializationCompleted(HRESULT result); };在你的项目属性中以Visual Studio为例需要确保C - 常规 - 附加包含目录添加WebView2 SDK的include目录路径。链接器 - 常规 - 附加库目录添加WebView2 SDK的lib目录路径通常包含x86和x64子目录。链接器 - 输入 - 附加依赖项添加WebView2Loader.lib。4.2 创建WebView2环境WebView2控件不能凭空产生它需要一个运行时环境。这个环境可能已经存在于用户的系统如果安装了Edge浏览器也可能需要我们从云端引导或使用固定版本的运行时。我们使用异步回调的方式来创建环境。// AppWindow.cpp #include AppWindow.h #include wil/com.h // 也可以使用WIL库简化COM操作这里我们用WRL演示 bool AppWindow::InitializeWebView() { // 步骤1创建WebView2环境 return SUCCEEDED(CreateWebViewEnvironment()); } HRESULT AppWindow::CreateWebViewEnvironment() { // 使用默认选项创建环境创建器 ComPtrICoreWebView2EnvironmentOptions options; // 在实际项目中你可以在这里设置一些选项比如语言、额外浏览器参数等。 // HRESULT hr CreateCoreWebView2EnvironmentOptions(nullptr, nullptr, nullptr, options); // 为了简单我们传递nullptr使用默认选项。 // 异步创建环境。CreateCoreWebView2EnvironmentWithOptions会立即返回。 // 当环境创建完成或失败时会调用我们提供的回调函数。 return CreateCoreWebView2EnvironmentWithOptions( nullptr, // browserExecutableFolder: nullptr表示使用已安装的或从云端引导的运行时。 nullptr, // userDataFolder: nullptr表示使用默认的用户数据文件夹。生产环境建议指定一个明确路径。 nullptr, // options: 环境选项我们使用nullptr默认。 Microsoft::WRL::CallbackICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler( [this](HRESULT result, ICoreWebView2Environment* env) - HRESULT { // 这个Lambda函数在环境创建完成后被调用 if (SUCCEEDED(result) env ! nullptr) { // 环境创建成功继续创建控制器 return CreateWebViewController(m_hWnd); } else { // 环境创建失败 OnWebViewInitializationCompleted(result); return result; } }).Get() // 获取回调接口的指针 ); }关键点解析CreateCoreWebView2EnvironmentWithOptions是入口函数。第一个参数browserExecutableFolder如果为nullptrSDK会尝试按以下顺序查找运行时1) 已安装的Microsoft Edge2) 固定版本的WebView2运行时如果存在3) 从云端引导下载。对于第一个教程我们使用这个最简单的模式。userDataFolder指定WebView2浏览器实例的用户数据如缓存、Cookie、历史记录存储位置。传入nullptr会使用一个默认路径在AppData目录下。在生产应用中强烈建议指定一个明确的、有读写权限的路径便于管理和清理。回调函数是一个Lambda它捕获了this指针以便访问AppWindow的成员方法。这是处理异步操作的标准模式。4.3 创建WebView2控制器与核心对象环境准备好之后我们需要在这个环境中为我们的特定窗口创建一个WebView2控制器。控制器负责管理WebView2控件与父窗口我们的Win32窗口之间的关系比如大小和位置。HRESULT AppWindow::CreateWebViewController(HWND hWnd) { if (!m_webviewEnvironment) { // 理论上不会走到这里因为环境创建成功才会调用此函数 return E_FAIL; } // 异步创建CoreWebView2Controller return m_webviewEnvironment-CreateCoreWebView2Controller( hWnd, // 父窗口句柄 Microsoft::WRL::CallbackICoreWebView2CreateCoreWebView2ControllerCompletedHandler( [this](HRESULT result, ICoreWebView2Controller* controller) - HRESULT { if (SUCCEEDED(result) controller ! nullptr) { // 1. 保存控制器和核心WebView2对象 m_controller controller; controller-get_CoreWebView2(m_webView); // 2. 调整WebView2控件大小使其充满窗口客户区 RECT bounds; GetClientRect(m_hWnd, bounds); m_controller-put_Bounds(bounds); // 3. 可选设置一些初始WebView2设置比如启用开发者工具 ComPtrICoreWebView2Settings settings; m_webView-get_Settings(settings); if (settings) { settings-put_AreDevToolsEnabled(TRUE); } // 4. 导航到一个初始网页比如Bing m_webView-Navigate(Lhttps://www.bing.com); // 5. 将焦点设置到WebView2控件 m_controller-MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); OnWebViewInitializationCompleted(S_OK); } else { OnWebViewInitializationCompleted(result); } return result; }).Get() ); } void AppWindow::OnWebViewInitializationCompleted(HRESULT result) { if (FAILED(result)) { // 初始化失败显示错误信息 std::wstring errorMsg LWebView2初始化失败。错误码: 0x std::to_wstring(result); MessageBox(m_hWnd, errorMsg.c_str(), L初始化错误, MB_ICONERROR); // 可以考虑关闭窗口或提供重试选项 DestroyWindow(m_hWnd); } else { // 初始化成功可以在这里执行一些后续操作比如更新UI状态 // 例如SetWindowText(m_hWnd, L我的WebView2应用 - 加载完成); } }现在我们需要在窗口创建成功后调用InitializeWebView方法。修改AppWindow::Create方法在ShowWindow之后调用它。bool AppWindow::Create(const std::wstring title, int width, int height) { // ... 之前的注册和创建窗口代码 ... ShowWindow(m_hWnd, SW_SHOW); UpdateWindow(m_hWnd); // 窗口显示后初始化WebView2 if (!InitializeWebView()) { MessageBox(nullptr, L启动WebView2初始化失败。, L错误, MB_ICONERROR); // 注意这里不能直接返回false因为窗口已经创建。 // 我们可以选择销毁窗口或者让用户决定。 DestroyWindow(m_hWnd); return false; } return true; }4.4 处理窗口大小变化当用户拖动窗口边框改变大小时我们需要同步调整WebView2控件的大小使其始终充满窗口的客户区。这需要在窗口过程函数中处理WM_SIZE消息。修改AppWindow.cpp中的WindowProc函数在WM_SIZE分支中添加处理逻辑LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { AppWindow* pThis reinterpret_castAppWindow*(GetWindowLongPtr(hWnd, GWLP_USERDATA)); switch (message) { case WM_SIZE: { if (pThis pThis-m_controller) { // 获取新的窗口客户区大小 RECT bounds; GetClientRect(hWnd, bounds); // 更新WebView2控件边界 pThis-m_controller-put_Bounds(bounds); } break; } // ... 其他case ... } // ... 默认处理 ... }至此一个完整的、集成了WebView2控件的基本应用就完成了。编译并运行你应该能看到一个窗口里面显示着Bing的首页。5. 常见问题与深度排查指南即使按照步骤操作第一次运行时也可能会遇到各种问题。这里我总结了一些最常见的坑和解决方法。5.1 运行时缺失或创建失败问题现象程序运行后窗口空白或者弹出错误对话框提示“无法创建WebView2环境”错误码可能是HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)或类似的。排查步骤检查运行时这是最常见的问题。我们的代码使用了CreateCoreWebView2EnvironmentWithOptions(nullptr, ...)这意味着SDK会尝试查找系统已安装的WebView2运行时或Edge浏览器。打开“设置”-“应用”-“应用和功能”搜索“Microsoft Edge WebView2 Runtime”查看是否已安装。如果未安装程序会尝试从微软服务器引导下载。这需要网络连接。如果网络环境受限可能会失败。使用固定版本运行时为了获得更稳定的体验特别是在离线环境或需要版本锁定的场景可以使用固定版本的运行时。从 Microsoft WebView2官方网站 下载“固定版本运行时”的引导程序或独立安装包。将下载的运行时文件例如Microsoft.WebView2.FixedVersionRuntime.xxx.x.x.x文件夹放置在你的项目目录下例如./runtimes。修改CreateWebViewEnvironment函数中的browserExecutableFolder参数指向该运行时文件夹的绝对路径。// 假设运行时放在项目根目录的 runtimes 子文件夹下 std::wstring runtimePath L.\\runtimes\\Microsoft.WebView2.FixedVersionRuntime.110.0.1900.0\\x64; // 注意路径需要根据实际架构x86/x64调整 return CreateCoreWebView2EnvironmentWithOptions( runtimePath.c_str(), // 指定固定版本运行时路径 nullptr, nullptr, ... // 回调 );注意指定路径后SDK将只使用该路径下的运行时不会尝试其他方式。请确保路径正确且包含对应架构Win32/x86 或 x64的二进制文件。5.2 编译或链接错误问题现象在Visual Studio中编译失败。“无法打开包括文件: ‘WebView2.h’”说明附加包含目录没有设置正确。请确认项目属性中附加包含目录指向了WebView2 SDK的include文件夹。“无法解析的外部符号 …CreateCoreWebView2EnvironmentWithOptions”说明链接器没有找到WebView2Loader.lib。请确认附加库目录指向了SDK的lib文件夹例如C:\path\to\WebView2Sdk\lib\x64。附加依赖项中添加了WebView2Loader.lib。项目平台x86/x64与库的路径架构匹配。5.3 窗口显示但WebView2区域空白问题现象窗口正常出现但客户区是灰色的没有加载网页。排查步骤检查初始化流程在CreateWebViewEnvironment和CreateWebViewController的回调函数中设置断点或添加日志输出确认异步创建流程是否成功走到了Navigate那一步。检查窗口句柄确保创建控制器时传入的hWnd是有效的、已创建的窗口句柄并且该窗口具有WS_VISIBLE样式。检查DPI感知在高DPI显示器上如果应用程序不是DPI感知的窗口坐标和实际像素可能不匹配导致WebView2控件位置或大小计算错误。可以在应用程序清单文件(app.manifest)中声明DPI感知或调用SetProcessDpiAwareness函数。检查网络与URL确认Navigate的URL字符串是否正确以及设备是否有网络权限可以访问该URL。可以尝试导航到一个简单的本地HTML文件或about:blank来测试。5.4 内存管理与资源释放WebView2对象是COM对象理论上使用ComPtr智能指针管理当AppWindow析构、ComPtr超出作用域时会自动调用Release。但窗口销毁顺序需要注意。最佳实践在窗口的WM_DESTROY消息处理中主动释放WebView2资源。因为WebView2控件作为子窗口需要在父窗口销毁前清理。修改WindowProc中的WM_DESTROY处理case WM_DESTROY: { if (pThis) { // 先关闭WebView2停止所有活动 if (pThis-m_webView) { pThis-m_webView-Stop(); // 有些操作可能需要调用 Close() } // 智能指针ComPtr会在pThis析构时自动释放资源。 // 但为了确保顺序可以显式重置。 if (pThis-m_controller) { pThis-m_controller-Close(); pThis-m_controller.Reset(); } pThis-m_webView.Reset(); // 注意不要在这里delete pThis因为pThis的生命周期可能由其他地方管理。 // 在我们的简单设计中pThis是随着主窗口创建而new出来的需要在WM_NCDESTROY中删除。 } PostQuitMessage(0); return 0; } case WM_NCDESTROY: { // 这是窗口被销毁前的最后一个消息 AppWindow* pThis reinterpret_castAppWindow*(GetWindowLongPtr(hWnd, GWLP_USERDATA)); if (pThis) { SetWindowLongPtr(hWnd, GWLP_USERDATA, 0); delete pThis; // 安全删除窗口类实例 } return DefWindowProc(hWnd, message, wParam, lParam); }同时需要修改AppWindow::Create中创建窗口的部分确保lpParam传入的是new出来的对象并在WM_NCDESTROY中删除避免内存泄漏。这是一种更健壮的管理方式。6. 功能扩展与下一步方向现在你的第一个WebView2窗口已经能够运行了。但这只是一个起点。基于这个基础框架你可以探索WebView2更强大的功能本地与Web通信这是混合开发的核心。通过ICoreWebView2的AddHostObjectToScript方法可以将C对象暴露给网页中的JavaScript让网页代码能调用本地方法。反过来也可以通过ICoreWebView2WebMessageReceived事件监听网页发送过来的消息。自定义导航与下载监听NavigationStarting、SourceChanged、ContentLoading等事件可以拦截或修改导航请求实现自定义路由逻辑。通过DownloadStarting事件可以管理文件下载。注入脚本与CSS使用AddScriptToExecuteOnDocumentCreated在页面加载初期注入JavaScript代码或者通过ExecuteScript在任意时刻执行JS。这可以用来增强网页功能或与页面交互。开发者工具我们已经通过settings-put_AreDevToolsEnabled(TRUE)启用了开发者工具。你还可以通过OpenDevToolsWindow方法以编程方式打开它方便调试。自定义用户数据文件夹如前所述为每个应用实例指定独立的userDataFolder可以实现多实例数据隔离或者便携式应用数据与exe放在一起。我个人在项目中的体会是WebView2的异步API模型一开始需要花点时间适应但一旦熟悉了回调模式其设计其实相当清晰和强大。最大的挑战往往在于初期环境配置和疑难问题排查一旦环境稳定后续的功能开发反而会比较顺畅。建议在完成这个基础窗口后立刻尝试实现一个最简单的“C调用JS”和“JS调用C”的示例这会让你对WebView2的核心价值有最直接的感受。
C++ Win32桌面应用集成WebView2控件:从零构建混合开发窗口
1. 项目概述从零到一构建WebView2窗口如果你已经跟着上一篇文章成功把WebView2的开发环境给搭起来了那恭喜你最磨人的一步已经过去了。接下来咱们要干点真正有成就感的事儿——创建第一个能跑起来的窗口。很多朋友学技术卡就卡在环境配置一旦环境通了后面看着窗口弹出来代码跑起来那个感觉是完全不一样的。这篇文章我们就专注一件事用C和Win32 API配合WebView2控件创建一个最基础的桌面应用程序窗口并在窗口里加载一个网页。别看目标简单这里面涉及到Win32窗口的生命周期管理、消息循环、以及WebView2核心对象的创建与交互是后续所有复杂功能的地基。我会把每一步为什么这么做、可能会遇到什么坑都掰开揉碎了讲清楚。无论你是刚接触Windows桌面开发还是从其他GUI框架转过来想试试WebView2这篇文章都能让你稳稳地上手。2. 核心思路与项目结构设计在动手写代码之前我们得先想明白这个程序要怎么组织。一个典型的、使用了WebView2的Win32桌面程序其核心架构可以看作“两层皮肉一个核心”。两层皮肉指的是Win32原生窗口这是程序的“外壳”负责显示窗口边框、标题栏处理移动、缩放、关闭等系统消息。我们通过注册窗口类、创建窗口实例、运行消息泵来完成这一部分。这是Windows桌面程序的传统手艺必须得会。WebView2控件这是程序的“内容”它作为一个子窗口嵌入到我们创建的Win32主窗口内部。它负责渲染网页内容处理HTML/CSS/JavaScript。WebView2本身是一个复杂的ActiveX/COM组件我们需要通过一套异步API来创建和管理它。一个核心就是连接这两者的桥梁代码。这部分代码负责在合适的时机通常是主窗口创建成功后创建WebView2控件实例并将其“贴”到主窗口的客户区。同时还要处理两者之间的通信比如窗口大小变化时需要通知WebView2控件同步调整大小。基于这个思路一个最小化的项目至少需要以下文件main.cpp程序的入口点包含WinMain函数负责启动消息循环。AppWindow.h/AppWindow.cpp定义和实现我们的主窗口类。我将它命名为AppWindow这个类封装了窗口创建、消息处理以及WebView2控件的初始化逻辑。将窗口逻辑封装成类比把所有代码堆在WinMain里要清晰得多也便于后续扩展。pch.h/pch.cpp(可选)预编译头文件用于加快编译速度。对于小型项目不是必须但养成好习惯很重要。我们的编码顺序将是先打造好Win32窗口这个“壳”确保它能正常显示和响应然后再把WebView2这个“芯”装进去并建立关联。3. 打造Win32应用程序窗口让我们先从最基础的Win32窗口开始。即使你有其他GUI框架的经验理解这套原始机制也大有裨益因为它能让你真正理解Windows桌面程序是如何运作的。3.1 定义窗口类与注册所有Win32窗口都基于一个“窗口类”。你可以把它理解为一个蓝图定义了窗口的外观图标、光标、背景色和行为处理消息的函数。我们需要先填写这个蓝图然后向系统注册它。// AppWindow.h #pragma once #include windows.h #include string class AppWindow { public: AppWindow(HINSTANCE hInstance, int nCmdShow); ~AppWindow(); bool Create(const std::wstring title, int width, int height); HWND GetWindowHandle() const { return m_hWnd; } HINSTANCE GetInstanceHandle() const { return m_hInstance; } // 消息处理函数的友元声明使其能访问私有成员 friend LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); private: HINSTANCE m_hInstance; // 应用程序实例句柄 HWND m_hWnd; // 窗口句柄 std::wstring m_ClassName LWebView2DemoWindowClass; // 自定义窗口类名 };在AppWindow.cpp中我们实现构造函数和Create方法。注册窗口类是Create方法的第一步。// AppWindow.cpp #include AppWindow.h #include stdexcept AppWindow::AppWindow(HINSTANCE hInstance, int nCmdShow) : m_hInstance(hInstance) { // 构造函数主要保存实例句柄窗口创建在Create方法中完成 } bool AppWindow::Create(const std::wstring title, int width, int height) { // 1. 定义并注册窗口类 WNDCLASSEX wc {}; wc.cbSize sizeof(WNDCLASSEX); wc.style CS_HREDRAW | CS_VREDRAW; // 窗口水平或垂直变化时重绘 wc.lpfnWndProc WindowProc; // 关键指定消息处理函数 wc.cbClsExtra 0; wc.cbWndExtra 0; wc.hInstance m_hInstance; wc.hIcon LoadIcon(nullptr, IDI_APPLICATION); // 使用默认应用程序图标 wc.hCursor LoadCursor(nullptr, IDC_ARROW); // 使用标准箭头光标 wc.hbrBackground (HBRUSH)(COLOR_WINDOW 1); // 默认窗口背景色 wc.lpszMenuName nullptr; // 无菜单 wc.lpszClassName m_ClassName.c_str(); // 使用我们定义的类名 wc.hIconSm LoadIcon(nullptr, IDI_APPLICATION); if (!RegisterClassEx(wc)) { MessageBox(nullptr, L窗口类注册失败, L错误, MB_ICONERROR); return false; } // ... 后续创建窗口代码 }注意WNDCLASSEX结构体中的lpfnWndProc成员必须指向一个正确的窗口过程函数。这里我们暂时使用一个全局函数WindowProc并通过友元声明让它可以修改AppWindow类的私有成员。这是一种常见的将C类与C风格回调函数结合的方法。另一种更面向对象的方法是使用GWLP_USERDATA来存储this指针在窗口过程中再取出来调用成员函数。3.2 创建窗口实例并显示注册完窗口类我们就可以用这个“蓝图”来创建具体的窗口实例了。bool AppWindow::Create(const std::wstring title, int width, int height) { // ... 接上面的注册窗口类代码 // 2. 创建窗口 m_hWnd CreateWindowEx( 0, // 扩展窗口样式 m_ClassName.c_str(), // 注册的窗口类名 title.c_str(), // 窗口标题 WS_OVERLAPPEDWINDOW, // 窗口样式可重叠、有标题栏、系统菜单、可调整大小等 CW_USEDEFAULT, CW_USEDEFAULT, // 初始x, y位置使用默认 width, height, // 窗口宽度和高度 nullptr, // 父窗口句柄无 nullptr, // 菜单句柄无 m_hInstance, // 应用程序实例句柄 this // 创建参数这里传入this指针后续在WM_CREATE中获取 ); if (!m_hWnd) { MessageBox(nullptr, L窗口创建失败, L错误, MB_ICONERROR); return false; } // 3. 显示并更新窗口 ShowWindow(m_hWnd, SW_SHOW); UpdateWindow(m_hWnd); return true; }这里有几个关键点WS_OVERLAPPEDWINDOW样式组合了最常用的窗口特性包括标题栏、边框、系统菜单、最小化/最大化按钮和可调整大小的边框。这是我们最熟悉的窗口样式。CreateWindowEx的最后一个参数是LPVOID lpParam。我们传入了this指针。这个指针会在窗口创建过程中随着WM_CREATE消息传递到窗口过程函数中。这是我们后续将窗口句柄与C对象实例关联起来的关键。3.3 实现消息处理循环窗口创建出来了但它现在是“静止”的。要让窗口能响应用户操作点击、拖动、关闭必须运行一个消息循环。同时我们需要实现窗口过程函数WindowProc来处理具体的消息。// AppWindow.cpp 中 WindowProc 的实现 LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { // 尝试从窗口的额外数据或创建参数中获取AppWindow实例指针 AppWindow* pThis nullptr; if (message WM_NCCREATE) { // WM_NCCREATE是窗口创建过程中最早的消息之一 // 从lParam中获取CREATESTRUCT其lpCreateParams保存了我们传入的this指针 CREATESTRUCT* pCreate reinterpret_castCREATESTRUCT*(lParam); pThis reinterpret_castAppWindow*(pCreate-lpCreateParams); // 将this指针存储到窗口的GWLP_USERDATA区域方便后续消息获取 SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_castLONG_PTR(pThis)); } else { // 对于其他消息从GWLP_USERDATA中取出this指针 pThis reinterpret_castAppWindow*(GetWindowLongPtr(hWnd, GWLP_USERDATA)); } // 如果成功获取到this指针并且该类有处理消息的方法可以在这里调用。 // 但为了简单和清晰我们目前先在这个全局函数中处理基础消息。 // 后续可以将消息分发到类的成员函数。 switch (message) { case WM_DESTROY: PostQuitMessage(0); // 发送WM_QUIT消息使主消息循环退出 return 0; case WM_SIZE: { // 窗口大小改变时我们需要在这里处理WebView2控件的大小调整 // 暂时留空后续补充 break; } case WM_PAINT: { PAINTSTRUCT ps; HDC hdc BeginPaint(hWnd, ps); // 如果需要自定义绘制可以在这里进行 // 对于WebView2应用客户区主要由WebView2控件覆盖通常不需要额外绘制 EndPaint(hWnd, ps); break; } default: // 其他未处理的消息交给默认窗口过程处理 return DefWindowProc(hWnd, message, wParam, lParam); } return 0; }现在我们需要在程序的入口点WinMain中将上述所有部分串联起来创建AppWindow实例运行消息循环。// main.cpp #include AppWindow.h int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { // 忽略未使用的参数 UNREFERENCED_PARAMETER(hPrevInstance); UNREFERENCED_PARAMETER(lpCmdLine); // 初始化COM库。WebView2是基于COM的所以这一步至关重要。 // COINIT_APARTMENTTHREADED 是Win32 GUI线程的标准选择。 HRESULT hr CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); if (FAILED(hr)) { // 错误处理 return -1; } // 创建应用程序窗口 AppWindow mainWindow(hInstance, nCmdShow); if (!mainWindow.Create(L我的第一个WebView2应用, 1024, 768)) { CoUninitialize(); // 清理COM return -1; } // 主消息循环 MSG msg {}; while (GetMessage(msg, nullptr, 0, 0)) { TranslateMessage(msg); // 翻译键盘消息 DispatchMessage(msg); // 将消息分发给窗口过程 } // 程序结束反初始化COM库 CoUninitialize(); return (int)msg.wParam; }编译并运行这段代码你应该能看到一个标准的、可移动、可缩放、有关闭按钮的空白Windows窗口。这是我们整个应用的骨架已经搭建完毕。实操心得在调试Win32窗口程序时如果窗口一闪而过通常是因为消息循环没有正确阻塞例如CreateWindow失败但没检查直接进入了很快退出的循环。务必检查CreateWindow的返回值并在WinMain开始处设置断点逐步执行确保窗口创建成功并进入了while (GetMessage(...))循环。4. 集成WebView2控件骨架有了现在要把“灵魂”——WebView2控件装进去。这是整个教程的核心。WebView2的创建是一个异步过程我们需要遵循“创建环境 - 创建控制器 - 创建核心对象 - 导航到网页”的流程。4.1 引入头文件与库首先确保你的项目配置正确。在AppWindow.h中添加WebView2的头文件并扩展我们的类。// AppWindow.h #pragma once #include windows.h #include string #include wrl.h // 微软的WRLWindows Runtime Library智能指针 #include WebView2.h // WebView2的核心头文件 #include WebView2EnvironmentOptions.h // 环境选项头文件 // 为方便使用WRL的智能指针 using namespace Microsoft::WRL; class AppWindow { public: // ... 之前的公共方法 ... bool InitializeWebView(); // 新增初始化WebView2 private: // ... 之前的私有成员 ... ComPtrICoreWebView2Controller m_controller; // WebView2控制器 ComPtrICoreWebView2 m_webView; // WebView2核心对象 // 异步初始化WebView2的辅助方法 HRESULT CreateWebViewEnvironment(); HRESULT CreateWebViewController(HWND hWnd); void OnWebViewInitializationCompleted(HRESULT result); };在你的项目属性中以Visual Studio为例需要确保C - 常规 - 附加包含目录添加WebView2 SDK的include目录路径。链接器 - 常规 - 附加库目录添加WebView2 SDK的lib目录路径通常包含x86和x64子目录。链接器 - 输入 - 附加依赖项添加WebView2Loader.lib。4.2 创建WebView2环境WebView2控件不能凭空产生它需要一个运行时环境。这个环境可能已经存在于用户的系统如果安装了Edge浏览器也可能需要我们从云端引导或使用固定版本的运行时。我们使用异步回调的方式来创建环境。// AppWindow.cpp #include AppWindow.h #include wil/com.h // 也可以使用WIL库简化COM操作这里我们用WRL演示 bool AppWindow::InitializeWebView() { // 步骤1创建WebView2环境 return SUCCEEDED(CreateWebViewEnvironment()); } HRESULT AppWindow::CreateWebViewEnvironment() { // 使用默认选项创建环境创建器 ComPtrICoreWebView2EnvironmentOptions options; // 在实际项目中你可以在这里设置一些选项比如语言、额外浏览器参数等。 // HRESULT hr CreateCoreWebView2EnvironmentOptions(nullptr, nullptr, nullptr, options); // 为了简单我们传递nullptr使用默认选项。 // 异步创建环境。CreateCoreWebView2EnvironmentWithOptions会立即返回。 // 当环境创建完成或失败时会调用我们提供的回调函数。 return CreateCoreWebView2EnvironmentWithOptions( nullptr, // browserExecutableFolder: nullptr表示使用已安装的或从云端引导的运行时。 nullptr, // userDataFolder: nullptr表示使用默认的用户数据文件夹。生产环境建议指定一个明确路径。 nullptr, // options: 环境选项我们使用nullptr默认。 Microsoft::WRL::CallbackICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler( [this](HRESULT result, ICoreWebView2Environment* env) - HRESULT { // 这个Lambda函数在环境创建完成后被调用 if (SUCCEEDED(result) env ! nullptr) { // 环境创建成功继续创建控制器 return CreateWebViewController(m_hWnd); } else { // 环境创建失败 OnWebViewInitializationCompleted(result); return result; } }).Get() // 获取回调接口的指针 ); }关键点解析CreateCoreWebView2EnvironmentWithOptions是入口函数。第一个参数browserExecutableFolder如果为nullptrSDK会尝试按以下顺序查找运行时1) 已安装的Microsoft Edge2) 固定版本的WebView2运行时如果存在3) 从云端引导下载。对于第一个教程我们使用这个最简单的模式。userDataFolder指定WebView2浏览器实例的用户数据如缓存、Cookie、历史记录存储位置。传入nullptr会使用一个默认路径在AppData目录下。在生产应用中强烈建议指定一个明确的、有读写权限的路径便于管理和清理。回调函数是一个Lambda它捕获了this指针以便访问AppWindow的成员方法。这是处理异步操作的标准模式。4.3 创建WebView2控制器与核心对象环境准备好之后我们需要在这个环境中为我们的特定窗口创建一个WebView2控制器。控制器负责管理WebView2控件与父窗口我们的Win32窗口之间的关系比如大小和位置。HRESULT AppWindow::CreateWebViewController(HWND hWnd) { if (!m_webviewEnvironment) { // 理论上不会走到这里因为环境创建成功才会调用此函数 return E_FAIL; } // 异步创建CoreWebView2Controller return m_webviewEnvironment-CreateCoreWebView2Controller( hWnd, // 父窗口句柄 Microsoft::WRL::CallbackICoreWebView2CreateCoreWebView2ControllerCompletedHandler( [this](HRESULT result, ICoreWebView2Controller* controller) - HRESULT { if (SUCCEEDED(result) controller ! nullptr) { // 1. 保存控制器和核心WebView2对象 m_controller controller; controller-get_CoreWebView2(m_webView); // 2. 调整WebView2控件大小使其充满窗口客户区 RECT bounds; GetClientRect(m_hWnd, bounds); m_controller-put_Bounds(bounds); // 3. 可选设置一些初始WebView2设置比如启用开发者工具 ComPtrICoreWebView2Settings settings; m_webView-get_Settings(settings); if (settings) { settings-put_AreDevToolsEnabled(TRUE); } // 4. 导航到一个初始网页比如Bing m_webView-Navigate(Lhttps://www.bing.com); // 5. 将焦点设置到WebView2控件 m_controller-MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); OnWebViewInitializationCompleted(S_OK); } else { OnWebViewInitializationCompleted(result); } return result; }).Get() ); } void AppWindow::OnWebViewInitializationCompleted(HRESULT result) { if (FAILED(result)) { // 初始化失败显示错误信息 std::wstring errorMsg LWebView2初始化失败。错误码: 0x std::to_wstring(result); MessageBox(m_hWnd, errorMsg.c_str(), L初始化错误, MB_ICONERROR); // 可以考虑关闭窗口或提供重试选项 DestroyWindow(m_hWnd); } else { // 初始化成功可以在这里执行一些后续操作比如更新UI状态 // 例如SetWindowText(m_hWnd, L我的WebView2应用 - 加载完成); } }现在我们需要在窗口创建成功后调用InitializeWebView方法。修改AppWindow::Create方法在ShowWindow之后调用它。bool AppWindow::Create(const std::wstring title, int width, int height) { // ... 之前的注册和创建窗口代码 ... ShowWindow(m_hWnd, SW_SHOW); UpdateWindow(m_hWnd); // 窗口显示后初始化WebView2 if (!InitializeWebView()) { MessageBox(nullptr, L启动WebView2初始化失败。, L错误, MB_ICONERROR); // 注意这里不能直接返回false因为窗口已经创建。 // 我们可以选择销毁窗口或者让用户决定。 DestroyWindow(m_hWnd); return false; } return true; }4.4 处理窗口大小变化当用户拖动窗口边框改变大小时我们需要同步调整WebView2控件的大小使其始终充满窗口的客户区。这需要在窗口过程函数中处理WM_SIZE消息。修改AppWindow.cpp中的WindowProc函数在WM_SIZE分支中添加处理逻辑LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { AppWindow* pThis reinterpret_castAppWindow*(GetWindowLongPtr(hWnd, GWLP_USERDATA)); switch (message) { case WM_SIZE: { if (pThis pThis-m_controller) { // 获取新的窗口客户区大小 RECT bounds; GetClientRect(hWnd, bounds); // 更新WebView2控件边界 pThis-m_controller-put_Bounds(bounds); } break; } // ... 其他case ... } // ... 默认处理 ... }至此一个完整的、集成了WebView2控件的基本应用就完成了。编译并运行你应该能看到一个窗口里面显示着Bing的首页。5. 常见问题与深度排查指南即使按照步骤操作第一次运行时也可能会遇到各种问题。这里我总结了一些最常见的坑和解决方法。5.1 运行时缺失或创建失败问题现象程序运行后窗口空白或者弹出错误对话框提示“无法创建WebView2环境”错误码可能是HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)或类似的。排查步骤检查运行时这是最常见的问题。我们的代码使用了CreateCoreWebView2EnvironmentWithOptions(nullptr, ...)这意味着SDK会尝试查找系统已安装的WebView2运行时或Edge浏览器。打开“设置”-“应用”-“应用和功能”搜索“Microsoft Edge WebView2 Runtime”查看是否已安装。如果未安装程序会尝试从微软服务器引导下载。这需要网络连接。如果网络环境受限可能会失败。使用固定版本运行时为了获得更稳定的体验特别是在离线环境或需要版本锁定的场景可以使用固定版本的运行时。从 Microsoft WebView2官方网站 下载“固定版本运行时”的引导程序或独立安装包。将下载的运行时文件例如Microsoft.WebView2.FixedVersionRuntime.xxx.x.x.x文件夹放置在你的项目目录下例如./runtimes。修改CreateWebViewEnvironment函数中的browserExecutableFolder参数指向该运行时文件夹的绝对路径。// 假设运行时放在项目根目录的 runtimes 子文件夹下 std::wstring runtimePath L.\\runtimes\\Microsoft.WebView2.FixedVersionRuntime.110.0.1900.0\\x64; // 注意路径需要根据实际架构x86/x64调整 return CreateCoreWebView2EnvironmentWithOptions( runtimePath.c_str(), // 指定固定版本运行时路径 nullptr, nullptr, ... // 回调 );注意指定路径后SDK将只使用该路径下的运行时不会尝试其他方式。请确保路径正确且包含对应架构Win32/x86 或 x64的二进制文件。5.2 编译或链接错误问题现象在Visual Studio中编译失败。“无法打开包括文件: ‘WebView2.h’”说明附加包含目录没有设置正确。请确认项目属性中附加包含目录指向了WebView2 SDK的include文件夹。“无法解析的外部符号 …CreateCoreWebView2EnvironmentWithOptions”说明链接器没有找到WebView2Loader.lib。请确认附加库目录指向了SDK的lib文件夹例如C:\path\to\WebView2Sdk\lib\x64。附加依赖项中添加了WebView2Loader.lib。项目平台x86/x64与库的路径架构匹配。5.3 窗口显示但WebView2区域空白问题现象窗口正常出现但客户区是灰色的没有加载网页。排查步骤检查初始化流程在CreateWebViewEnvironment和CreateWebViewController的回调函数中设置断点或添加日志输出确认异步创建流程是否成功走到了Navigate那一步。检查窗口句柄确保创建控制器时传入的hWnd是有效的、已创建的窗口句柄并且该窗口具有WS_VISIBLE样式。检查DPI感知在高DPI显示器上如果应用程序不是DPI感知的窗口坐标和实际像素可能不匹配导致WebView2控件位置或大小计算错误。可以在应用程序清单文件(app.manifest)中声明DPI感知或调用SetProcessDpiAwareness函数。检查网络与URL确认Navigate的URL字符串是否正确以及设备是否有网络权限可以访问该URL。可以尝试导航到一个简单的本地HTML文件或about:blank来测试。5.4 内存管理与资源释放WebView2对象是COM对象理论上使用ComPtr智能指针管理当AppWindow析构、ComPtr超出作用域时会自动调用Release。但窗口销毁顺序需要注意。最佳实践在窗口的WM_DESTROY消息处理中主动释放WebView2资源。因为WebView2控件作为子窗口需要在父窗口销毁前清理。修改WindowProc中的WM_DESTROY处理case WM_DESTROY: { if (pThis) { // 先关闭WebView2停止所有活动 if (pThis-m_webView) { pThis-m_webView-Stop(); // 有些操作可能需要调用 Close() } // 智能指针ComPtr会在pThis析构时自动释放资源。 // 但为了确保顺序可以显式重置。 if (pThis-m_controller) { pThis-m_controller-Close(); pThis-m_controller.Reset(); } pThis-m_webView.Reset(); // 注意不要在这里delete pThis因为pThis的生命周期可能由其他地方管理。 // 在我们的简单设计中pThis是随着主窗口创建而new出来的需要在WM_NCDESTROY中删除。 } PostQuitMessage(0); return 0; } case WM_NCDESTROY: { // 这是窗口被销毁前的最后一个消息 AppWindow* pThis reinterpret_castAppWindow*(GetWindowLongPtr(hWnd, GWLP_USERDATA)); if (pThis) { SetWindowLongPtr(hWnd, GWLP_USERDATA, 0); delete pThis; // 安全删除窗口类实例 } return DefWindowProc(hWnd, message, wParam, lParam); }同时需要修改AppWindow::Create中创建窗口的部分确保lpParam传入的是new出来的对象并在WM_NCDESTROY中删除避免内存泄漏。这是一种更健壮的管理方式。6. 功能扩展与下一步方向现在你的第一个WebView2窗口已经能够运行了。但这只是一个起点。基于这个基础框架你可以探索WebView2更强大的功能本地与Web通信这是混合开发的核心。通过ICoreWebView2的AddHostObjectToScript方法可以将C对象暴露给网页中的JavaScript让网页代码能调用本地方法。反过来也可以通过ICoreWebView2WebMessageReceived事件监听网页发送过来的消息。自定义导航与下载监听NavigationStarting、SourceChanged、ContentLoading等事件可以拦截或修改导航请求实现自定义路由逻辑。通过DownloadStarting事件可以管理文件下载。注入脚本与CSS使用AddScriptToExecuteOnDocumentCreated在页面加载初期注入JavaScript代码或者通过ExecuteScript在任意时刻执行JS。这可以用来增强网页功能或与页面交互。开发者工具我们已经通过settings-put_AreDevToolsEnabled(TRUE)启用了开发者工具。你还可以通过OpenDevToolsWindow方法以编程方式打开它方便调试。自定义用户数据文件夹如前所述为每个应用实例指定独立的userDataFolder可以实现多实例数据隔离或者便携式应用数据与exe放在一起。我个人在项目中的体会是WebView2的异步API模型一开始需要花点时间适应但一旦熟悉了回调模式其设计其实相当清晰和强大。最大的挑战往往在于初期环境配置和疑难问题排查一旦环境稳定后续的功能开发反而会比较顺畅。建议在完成这个基础窗口后立刻尝试实现一个最简单的“C调用JS”和“JS调用C”的示例这会让你对WebView2的核心价值有最直接的感受。