简介:这是一份面向UE5开发者的在线浏览器功能实现源码,基于Edge WebView2 Runtime组件封装,适合需要在虚幻引擎项目中嵌入网页浏览、展示在线内容或实现混合UI交互的开发者,开箱即用,无需从零搭建底层通信。压缩包共158个文件,约17.85MB,包含12个cpp源文件与55个头文件构成核心逻辑,另有uasset、umap等UE资产、ini配置、sln与vcxproj工程文件,以及dll、lib、pdb等WebView2运行时依赖,结构完整可直接编译调试。内容覆盖WebView控件封装、消息收发与进程通信、Edge管理器等模块,便于理解UE与WebView2的集成方式。目前已有392人学习下载,适合想快速验证方案或二次开发的中高级UE开发者参考。
1. 在 UE5 里塞进一个能跑网页的浏览器:为什么我选了 Edge WebView2 而不是内置方案
如果你在 UE5 项目里做过 UI,迟早会碰到一个绕不开的需求:把网页内容嵌进游戏界面。可能是运营活动页、可能是内嵌的文档中心、也可能是一个需要实时刷新的数据看板。UE5 自带的 Web Browser 插件基于 CEF,能用,但打包体积大、版本更新慢、和系统 WebView 的兼容性经常出玄学问题。我前后试过三种方案,最后稳定下来的路线是:用 Edge WebView2 Runtime 作为渲染后端,在 UE5 里封装一个可复用的浏览器组件。
这个方案的核心价值在于:Windows 系统自带或可静默安装 WebView2 Runtime,你的 UE5 包不需要再背一个几百 MB 的 Chromium。渲染质量、JS 执行性能、对现代前端框架的兼容性都直接对齐 Edge。适合谁?适合做 Windows 平台工具类应用、数据可视化面板、内嵌运营页的 UE5 开发者。不适合需要跨平台(Mac/Linux)或者对浏览器内核有强定制需求的场景。下面把我踩过的路和能直接抄的代码拆开讲。
2. 环境准备与 WebView2 Runtime 的三种落地方式
2.1 为什么必须区分「开发机」和「用户机」的 Runtime 状态
WebView2 的运行依赖一个关键前提:目标机器上必须存在 Edge WebView2 Runtime。开发机上通常已经装了 Edge 浏览器,Runtime 大概率也在,所以你本地跑得好好的,打包发给测试就白屏。这是最常见的翻车点。
Runtime 的落地方式有三种,我按推荐度排:
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 系统已预装 | Win11 及部分 Win10 更新后 | 零成本 | 不可控,老系统可能没有 |
| 静默安装包随应用分发 | 面向外部用户的产品 | 可控,离线可用 | 包体增加约 130MB |
| Evergreen Bootstrapper 在线引导 | 有网络环境的安装器 | 包体小 | 依赖网络,首次启动慢 |
我一般会:开发阶段直接用系统已有的 Runtime,打包时把 Evergreen Standalone Installer 放进安装目录,首次启动检测缺失就静默调用。检测逻辑用注册表读,比文件探测可靠。
# 检测 WebView2 Runtime 是否安装(注册表方式,比找文件夹靠谱) reg query "HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" /v pv # 返回 pv 值即已安装,返回错误码则缺失这段命令查的是 WebView2 Runtime 的固定 GUID。注意 32 位和 64 位注册表路径不同,WOW6432Node 下的是 32 位视图,64 位系统上 Runtime 通常注册在这里。如果你在代码里做检测,用 Windows API 读注册表比调命令行更干净。
2.2 在 UE5 插件里加载 WebView2Loader.dll 的正确姿势
WebView2 的 C++ API 依赖一个动态库 WebView2Loader.dll。这个 DLL 必须和你的可执行文件在同一目录,或者放在系统能找到的路径。UE5 的插件结构里,我把它放在Plugins/YourBrowserPlugin/Binaries/Win64/下,然后在模块启动时手动 LoadLibrary。
// 模块启动时加载 WebView2Loader,避免运行时找不到 DLL void FWebView2Module::StartupModule() { FString DllPath = FPaths::Combine( IPluginManager::Get().FindPlugin(TEXT("YourBrowserPlugin"))->GetBaseDir(), TEXT("Binaries/Win64/WebView2Loader.dll") ); // 用绝对路径加载,避免依赖当前工作目录 WebView2LoaderHandle = FPlatformProcess::GetDllHandle(*DllPath); if (!WebView2LoaderHandle) { UE_LOG(LogTemp, Error, TEXT("WebView2Loader.dll 加载失败,检查路径: %s"), *DllPath); } }逻辑说明:GetDllHandle是 UE5 封装的跨平台动态库加载接口,Windows 下最终调 LoadLibraryEx。参数是 DLL 的绝对路径,用IPluginManager拿到插件根目录再拼,比硬编码相对路径稳。失败时打日志,别静默吞掉——我见过有人把 DLL 放错目录,结果运行时直接崩在 WebView2 的 CreateCoreWebView2Environment 回调里,堆栈完全看不出是 DLL 缺失。
参数上唯一要注意的是:如果你的项目开了 Shipping 配置,确保 DLL 被正确拷贝到打包输出。UE5 的 Build.cs 里可以用RuntimeDependencies.Add声明,让 UAT 自动处理。
2.3 初始化 WebView2 环境的异步回调链
WebView2 的初始化是异步的,这是新手最容易写错的地方。你不能创建完环境就立刻创建 WebView,必须等回调。整个链路是:CreateCoreWebView2Environment → 回调里 CreateCoreWebView2Controller → 回调里拿 WebView 接口。
// 初始化 WebView2 环境,异步链式回调 void UWebBrowserWidget::InitWebView() { // 用户数据目录放在项目 Saved 下,避免污染系统目录 FString UserDataFolder = FPaths::Combine( FPaths::ProjectSavedDir(), TEXT("WebView2Data") ); CreateCoreWebView2EnvironmentWithOptions( nullptr, // 浏览器版本,nullptr 用系统默认 TCHAR_TO_WCHAR(*UserDataFolder), nullptr, // 环境选项,可设代理、语言等 Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>( [this](HRESULT Result, ICoreWebView2Environment* Env) -> HRESULT { if (FAILED(Result) || !Env) return Result; // 环境创建成功,接着创建 Controller Env->CreateCoreWebView2Controller( GetNativeWindowHandle(), Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>( [this](HRESULT Result, ICoreWebView2Controller* Controller) -> HRESULT { if (FAILED(Result) || !Controller) return Result; WebViewController = Controller; WebViewController->get_CoreWebView2(&WebView); // 到这里才能真正 Navigate WebView->Navigate(L"https://example.com"); return S_OK; } ).Get() ); return S_OK; } ).Get() ); }逻辑说明:CreateCoreWebView2EnvironmentWithOptions第一个参数传 nullptr 表示用系统安装的 Runtime 版本。第二个参数是用户数据目录,必须传,否则默认写到 exe 同目录,打包后可能没写权限。回调里用 UE5 的Callback<>模板包装 COM 回调,这是微软推荐的写法,比裸实现 COM 接口省事。
参数说明:GetNativeWindowHandle()返回的是 UE5 窗口的 HWND,WebView2 会把这个窗口作为父窗口。如果你在 UMG 里用,需要拿到 Slate 窗口的 HWND,通常通过GEngine->GameViewport->GetWindow()->GetNativeWindow()->GetOSWindowHandle()这条链。注意这个 HWND 在 PIE 和 Standalone 下不一样,PIE 下是编辑器窗口的子窗口,调试时别搞混。
3. 把网页渲染到 UMG:纹理拷贝与输入事件转发
3.1 从 WebView2 抓帧到 UE5 纹理的完整链路
WebView2 本身不直接给你 UE5 纹理,它通过CapturePreview或者GetWebView2CompositionController提供帧数据。我走的是 CompositionController 路线,性能比 CapturePreview 好,因为 CapturePreview 是截图式的,每次都要编码成流。
核心思路:注册add_CompositionFrameRendered回调,在回调里拿到 ID3D11Texture2D,然后用 UE5 的 D3D11 RHI 接口拷贝到一张 UTexture2D。
// 注册帧渲染回调,把 WebView2 的 D3D 纹理拷到 UE5 纹理 WebViewCompositionController->add_CompositionFrameRendered( Callback<ICoreWebView2CompositionControllerFrameRenderedEventHandler>( [this](ICoreWebView2CompositionController* Sender, IUnknown* Args) -> HRESULT { ComPtr<ID3D11Texture2D> WebViewTexture; // 从 CompositionController 拿当前帧的纹理 Sender->get_CompositionTexture(&WebViewTexture); if (!WebViewTexture) return S_OK; // 用 UE5 RHI 的 D3D11 接口拷贝 ID3D11DeviceContext* Context = static_cast<ID3D11DeviceContext*>(GDynamicRHI->RHIGetNativeDevice()); // 这里需要自己维护一张 Staging 纹理做中转 Context->CopyResource(StagingTexture, WebViewTexture.Get()); // 再 Map 到 CPU 更新 UTexture2D,或者用 CopySubresourceRegion 直接拷到 GPU 纹理 return S_OK; } ).Get(), &Token );逻辑说明:get_CompositionTexture返回的是 WebView2 内部维护的 D3D11 纹理,你不能直接把它当 UE5 资源用,必须拷贝。拷贝分两条路:走 CPU 用 Map/Unmap 更新 UTexture2D 的 PlatformData,简单但帧率上不去;走 GPU 用 CopySubresourceRegion 直接拷到 UE5 的纹理资源,快但需要你拿到 UE5 纹理的底层 D3D 指针。
参数说明:StagingTexture 的格式要和 WebView2 输出一致,通常是 DXGI_FORMAT_B8G8R8A8_UNORM。尺寸按 WebView 的 ClientRect 来,别用固定值,否则窗口拉伸时画面会糊。我一般会在 Resize 事件里重建 Staging 纹理。
3.2 鼠标和键盘事件怎么从 UMG 传到 WebView2
渲染通了只是第一步,用户点网页没反应才是真尴尬。WebView2 的输入事件需要你手动转发:UMG 的 OnMouseButtonDown 里拿到坐标,转成 WebView2 的 client 坐标,调SendMouseInput。
// UMG 鼠标按下事件转发给 WebView2 FReply UWebBrowserWidget::NativeOnMouseButtonDown( const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { if (!WebViewCompositionController) return FReply::Unhandled(); // 把 UMG 局部坐标转成 WebView2 的 client 坐标 FVector2D LocalPos = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); POINT WebViewPoint = { (LONG)LocalPos.X, (LONG)LocalPos.Y }; // 构造 WebView2 的鼠标事件 ICoreWebView2MouseEvent* MouseEvent = nullptr; WebViewCompositionController->SendMouseInput( COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN, COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, 0, // 鼠标数据,滚轮时用 WebViewPoint, &MouseEvent ); return FReply::Handled(); }逻辑说明:AbsoluteToLocal把屏幕坐标转成控件局部坐标,这个坐标原点在控件左上角,正好对应 WebView2 的 client 坐标系。SendMouseInput的第一个参数是事件类型,左键按下、抬起、移动、滚轮各有枚举。第三个参数在滚轮事件里传滚轮 delta,其他事件传 0。
参数说明:COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE表示没有按修饰键,如果用户按着 Ctrl 点,要传对应的 flag,否则网页里的 Ctrl+点击新窗口打开就失效。键盘事件类似,用SendKeyInput,但要注意 UE5 的键盘事件是 FKey,需要映射到 Windows 虚拟键码。
3.3 分辨率自适应与 DPI 缩放的处理
WebView2 默认按 96 DPI 渲染,如果你的 UE5 窗口在高 DPI 显示器上,网页会显得很小。解决办法是在创建环境时设置AdditionalBrowserArguments里的--force-device-scale-factor,或者在 Controller 上设put_RasterizationScale。
// 根据当前 DPI 缩放设置 WebView2 的渲染比例 float DPIScale = FPlatformApplicationMisc::GetDPIScaleFactorAtPoint( WindowPosition.X, WindowPosition.Y ); WebViewController->put_RasterizationScale(DPIScale); // 同时通知 WebView2 窗口尺寸变化 WebViewController->put_Bounds({0, 0, (LONG)WidgetWidth, (LONG)WidgetHeight});逻辑说明:put_RasterizationScale告诉 WebView2 按什么比例渲染,传 1.0 是 100%,传 1.5 是 150%。put_Bounds设置 WebView2 在父窗口里的位置和大小,这个必须和 UMG 控件的实际尺寸同步,否则鼠标坐标对不上。
参数说明:GetDPIScaleFactorAtPoint是 UE5 提供的接口,传窗口左上角的屏幕坐标。注意这个值在窗口跨显示器拖动时会变,所以要在 OnWindowMoved 里重新设。我见过有人只在初始化时设一次,结果用户把窗口拖到副屏就错位。
4. 避坑与排查:WebView2 在 UE5 里最容易翻车的五个点
4.1 打包后白屏,日志里没有任何报错
现象:编辑器里跑得好好的,打包成 Shipping 后打开浏览器区域一片白,Output Log 干净得像没事发生。
原因:WebView2Loader.dll 没被拷进打包目录。UE5 的打包系统默认只处理 Build.cs 里声明的 RuntimeDependencies,你手动放的 DLL 它不管。
解决:在插件的 Build.cs 里加RuntimeDependencies.Add("$(PluginDir)/Binaries/Win64/WebView2Loader.dll"),然后重新生成项目文件再打包。验证方法是打包后去 Binaries/Win64 下看 DLL 在不在。
4.2 网页能显示但点击没反应,坐标偏移
现象:网页渲染正常,鼠标点按钮没反应,或者点 A 位置触发的是 B 位置。
原因:UMG 控件的坐标系和 WebView2 的 client 坐标系原点不一致,或者 DPI 缩放没同步。
解决:在 NativeOnMouseButtonDown 里打日志,把 LocalPos 和 WebView2 的 Bounds 都打出来对比。常见情况是 UMG 控件有 Padding 或者 Slot 偏移,AbsoluteToLocal 的结果需要再减去 Padding。另一个可能是 RasterizationScale 设了但 Bounds 没跟着改。
4.3 初始化回调里访问 UObject 导致崩溃
现象:WebView2 环境创建成功的回调里,访问 Widget 的成员变量时随机崩溃。
原因:WebView2 的回调在它自己的线程上执行,不是 UE5 的 GameThread。在非 GameThread 访问 UObject 是未定义行为。
解决:回调里只做数据拷贝,把结果存到线程安全的变量里,然后用AsyncTask(ENamedThreads::GameThread, ...)切回主线程再操作 UObject。我一般会在回调入口就切线程,后面所有逻辑都在 GameThread 上跑。
4.4 用户数据目录权限不足导致初始化失败
现象:CreateCoreWebView2EnvironmentWithOptions 返回 E_ACCESSDENIED。
原因:UserDataFolder 指向了 Program Files 下的安装目录,普通用户没写权限。
解决:把 UserDataFolder 设到FPaths::ProjectSavedDir()下,这个路径在打包后指向%LOCALAPPDATA%或者用户文档目录,一定有写权限。如果项目要求数据跟着安装目录走,那就得在安装器里给目录提权,不推荐。
4.5 网页里的视频播放不了或者花屏
现象:普通网页正常,一放视频就黑屏或者绿屏。
原因:WebView2 的视频解码走的是 GPU 硬解,和 UE5 的 D3D 设备冲突,或者纹理格式不匹配。
解决:在 AdditionalBrowserArguments 里加--disable-gpu-compositing试试,能软解但性能下降。更好的做法是确保 WebView2 和 UE5 用同一个 D3D 设备,这需要在创建环境时传COREWEBVIEW2_HOSTING_MODE相关的选项。我一般先试禁用 GPU 合成确认问题范围,再决定要不要做设备共享。
5. 进阶:把浏览器组件做成可复用的 UE5 插件
5.1 插件目录结构与模块依赖的规划
一个能复用的浏览器插件,目录结构要清晰。我习惯这样分:
YourBrowserPlugin/ ├── Source/ │ └── YourBrowserPlugin/ │ ├── Public/ │ │ ├── UWebBrowserWidget.h // UMG 控件对外接口 │ │ └── WebBrowserTypes.h // 枚举和结构体 │ ├── Private/ │ │ ├── WebView2Impl.cpp // WebView2 封装 │ │ └── WebView2Impl.h │ └── YourBrowserPlugin.Build.cs ├── Binaries/Win64/WebView2Loader.dll └── YourBrowserPlugin.upluginBuild.cs 里要声明依赖:PublicDependencyModuleNames加UMG、Slate、SlateCore,PrivateDependencyModuleNames加D3D11RHI、RHI。WebView2 的 SDK 头文件我一般放在 ThirdParty 下,用PublicIncludePaths引。
5.2 暴露给蓝图的异步节点设计
让策划也能用,就得暴露蓝图节点。我设计了一个LoadURL的异步节点,带完成回调和失败回调。
// 蓝图异步节点:加载 URL 并返回结果 UCLASS() class UAsyncLoadURL : public UBlueprintAsyncActionBase { GENERATED_BODY() public: DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLoadComplete, const FString&, FinalURL); DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnLoadFailed, const FString&, ErrorMsg); UPROPERTY(BlueprintAssignable) FOnLoadComplete OnComplete; UPROPERTY(BlueprintAssignable) FOnLoadFailed OnFailed; UFUNCTION(BlueprintCallable, meta=(BlueprintInternalUseOnly="true")) static UAsyncLoadURL* LoadURL(UWebBrowserWidget* Browser, const FString& URL); virtual void Activate() override; };逻辑说明:继承UBlueprintAsyncActionBase,蓝图里就能用异步节点的形式调用。Activate里发起 Navigate,然后注册 WebView2 的 NavigationCompleted 回调,成功走 OnComplete,失败走 OnFailed。
参数说明:BlueprintInternalUseOnly让这个节点在蓝图里显示为异步节点而不是普通函数。UWebBrowserWidget*参数传你要操作的浏览器控件,URL 传完整地址。回调里记得把委托清掉,否则多次调用会重复触发。
5.3 用命令行参数控制 WebView2 行为的几个实用开关
WebView2 支持通过AdditionalBrowserArguments传 Chromium 命令行参数,有几个我常用的:
| 参数 | 作用 | 适用场景 |
|---|---|---|
--disable-web-security | 关闭同源策略 | 本地调试跨域接口 |
--autoplay-policy=no-user-gesture-required | 允许自动播放 | 内嵌视频页 |
--force-device-scale-factor=1.5 | 强制缩放 | 高 DPI 适配 |
--disable-gpu | 禁用 GPU 加速 | 排查花屏问题 |
这些参数在创建环境时通过ICoreWebView2EnvironmentOptions的put_AdditionalBrowserArguments设置。注意--disable-web-security只在开发阶段用,上线前一定去掉,否则是安全漏洞。
5.4 一个我反复用的调试技巧:把 WebView2 的日志接出来
WebView2 内部有日志,但默认不输出。在环境变量里设WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS=--enable-logging --v=1,日志会写到用户数据目录下的EBWebView文件夹里。排查初始化失败、白屏、崩溃时,这个日志比 UE5 的 Output Log 有用得多。
我现在的习惯是:插件初始化时先检查 Runtime 是否存在,不存在就弹窗提示并给下载引导;存在就正常走。每次遇到白屏,第一件事是去看EBWebView下的日志,十有八九能直接定位到是 DLL 缺失还是权限问题。这个习惯帮我省了无数个对着空白窗口发呆的下午。希望帮到你。
本文还有配套的精品资源,点击获取