WinForm + WebView2 开发自用浏览器:从初始化到脚本注入的完整实践 简介这是一份基于WebView2内核的WinForm桌面浏览器程序源码使用Visual Studio 2019开发产品形态接近Edge、Chrome等主流浏览器适合希望定制个性化浏览器界面的桌面端开发者参考与二次开发。压缩包共134个文件整体约15.33MB主要包含47个dll浏览器内核及运行依赖、32个png界面图标、8个cs窗体与业务逻辑、9个xml配置文件另有sln、csproj、exe、ico等工程与启动文件目录结构清楚可直接打开解决方案编译调试。目前已有1097人学习/下载说明这套源码在WebView2集成与自用浏览器场景中具备一定参考价值。除Form1主窗体、设计器代码、资源文件和App.config等工程主体外还保留了可执行程序与完整依赖库拿到后可以查看浏览器内核加载流程、窗体布局与事件处理方式也能在此基础上改动图标、菜单和默认主页快速形成个性化桌面浏览器工具。1. 自用浏览器还得自己写WinForm 加 WebView2 才是日常最优解Chrome 装再多插件Edge 开再多标志位也替代不了一款真正按自己工作习惯定制的浏览器。我最早自用浏览器用的是 IE 内核套壳后来换 CefSharp都被内存和启动速度劝退。直到 WebView2 稳定下来用 WinForm 做壳、WebView2 做内核才觉得这条路能长期走——代码量不大界面完全自己控制默认走 Edge 的 Chromium 内核兼容性不用像 IE 时代那样迁就。这个源码工程就是一套能直接编译运行的 WinForm WebView2 桌面程序适合日常有特定浏览场景的开发者也适合想学桌面程序开发和 WebView2 集成的人拿来当骨架改。整个工程不复杂但坑不少尤其是 Runtime 分发和初始化时序后面会详细拆。2. 初始化 WebView2 环境运行时检测、离线包与按需分发方案2.1 运行时依赖Evergreen 与 Fixed Version 怎么选WebView2 不是随 Windows 自带的组件它依附于 Edge 的 Chromium 运行时。开发前先明确一个概念WebView2 Runtime 有两种分发模式Evergreen 和 Fixed Version。Evergreen 是常青模式用户机器上如果没有安装向导会去微软服务器拉最新版Fixed Version 则把特定版本的运行时文件打包进你的程序目录不依赖联网但需要自己维护版本更新。自用程序我选 Evergreen原因很实际体积小、更新省心微软会随 Edge 自动升级。这个源码工程里也是默认走 Evergreen 路线。但要注意Evergreen 不等于不用管开发机上装了 Edge 不一定代表 WebView2 Runtime 可用——新版 Win11 自带Win10 和旧版 Win11 经常缺失这就是热词里“could not find the webview2 runtime”这个报错频繁出现的大背景。Fixed Version 适合什么场景离线环境、内网机器、需要锁定内核版本的自动化测试。代价是 SDK 里那 200 多 MB 的运行时文件全得跟着分发包走。自用程序没必要但你发布的程序如果给了不懂技术的朋友用Evergreen 的联网安装失败率会让你头疼。我的习惯是默认 Evergreen检测失败时提示用户手动装离线包而不是自动触发安装——后面会详细说原因。2.2 初始化流程从检测到创建 CoreWebView2 环境WebView2 的初始化是异步的核心对象是 CoreWebView2Environment它负责管理运行时进程和用户数据文件夹。先看最简初始化代码private async void InitializeWebView2() { // 指定用户数据目录自用浏览器一定要独立目录避免和 Edge 互相干扰 var userDataFolder Path.Combine( Application.StartupPath, WebView2UserData); // 创建环境Evergreen 模式传 null会自动去找系统里的 WebView2 Runtime var environment await CoreWebView2Environment.CreateAsync( null, userDataFolder); // 把环境绑定到 WinForms 里的 WebView2 控件 await webView21.EnsureCoreWebView2Async(environment); // 到这一步才算初始化完成可以挂导航事件和注入脚本 webView21.CoreWebView2.NavigationStarting OnNavigationStarting; }这段代码有两个关键参数第一个参数是浏览器 executable 路径传 null 就是 Evergreen 模式第二个是 userDataFolder决定 Cookie、缓存、LocalStorage 存哪。自用浏览器必须配独立目录不然 Debug 期间会把你的 Edge 登录态搞乱。还有个细节async void不能乱用UI 初始化用没问题但按钮点击事件里用就要自己在方法内 try-catch异常会直接崩掉进程。初始化完成前webView21.CoreWebView2是 null任何调用都会抛 NullReferenceException。正确做法是初始化完成后在回调里挂事件、注入脚本而不是在 Form_Load 里同步操作。这个时序问题是新手最容易翻车的地方后面避坑章节展开。2.3 按需分发离线安装包与错误提示的兜底策略Runtime 缺失时程序启动就是白屏加一个异常弹窗。自用程序可以粗暴地弹个提示让你去官网下载但你如果分发给同事用体验就太糙了。比较合理的兜底流程是启动时检查 Runtime缺失则弹窗让用户选择“在线安装”或“手动安装”。private bool CheckRuntimeInstalled() { // 通过环境创建结果判断不依赖注册表注册表方式在多版本并存时不准 try { var envTask CoreWebView2Environment.GetAvailableBrowserVersionString(); // GetAvailableBrowserVersionString 返回 null 表示没检测到可用运行时 return !string.IsNullOrEmpty(envTask); } catch (WebView2RuntimeNotFoundException) { return false; } }GetAvailableBrowserVersionString是检测运行时最直接的方式它返回类似“109.0.1518.78”的版本号字符串没装则抛异常。这里注意版本号里 109 是 Chromium 主版本WebView2 的版本号和 Chrome 大版本基本同步特殊情况下会和 Edge 的版本不一致——系统装了老 Edge 但并发了新的 Evergreen Runtime 是完全可能的所以别依赖“装了 Edge 就等于有 WebView2”这个假设。离线安装包在分发场景下几乎是必须准备的。微软官网的 Evergreen 独立安装器有两种Bootstrapper 小包在线拉取和 Standalone 大包离线完整安装。给同事用直接给 Standalone 包宁可体积大点也别让人家卡在下载失败上。还有个容易被忽略的点Standalone 安装包需要管理员权限普通用户双击会弹 UAC程序里要预留“以管理员身份运行安装器”的提示逻辑。3. 导航与多标签事件模型和 UI 线程的协作方式3.1 导航事件NavigationStarting 与 NavigationCompleted 的配合导航事件是自用浏览器功能扩展的核心挂载点。NavigationStarting在请求发出前触发适合做拦截和参数改写NavigationCompleted在加载完成后触发适合收尾处理。两者之间还有SourceChanged用于判断是用户操作还是页面 JS 触发的导航。private void OnNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { // e.Uri 是目标地址e.IsRedirected 表示是否是重定向请求 if (e.Uri.StartsWith(https://example.com)) { // 拦截自定义协议转交给本地逻辑处理 e.Cancel true; HandleCustomProtocol(e.Uri); } } private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e) { // e.IsSuccess 为 false 时e.WebErrorStatus 给出具体失败原因 if (!e.IsSuccess) { statusLabel.Text $加载失败{e.WebErrorStatus}; } }WebErrorStatus 枚举值非常多常见的有 ConnectionAborted、HostNameNotResolved、Timeout。这个枚举有个坑它不会把所有证书错误都拦下来某些 TLS 错误会直接显示浏览器错误页而不触发这个事件自用浏览器过滤广告和恶意站点时要注意这点。另外NavigationStarting里做同步拦截没问题但别做耗时操作它运行在 UI 线程卡了就是页面白屏。3.2 多标签实现TabControl 加 UserControl 的封装方式多标签是浏览器的基本操作。WinForms 里做多标签核心思路不复杂TabControl 的每个 TabPage 里放一个独立的 WebView2 控件实例关键是每个实例必须用自己的用户数据目录否则多标签之间 Cookie 相互污染。public class BrowserTab : UserControl { private WebView2 _webView; private string _userDataFolder; public BrowserTab(string seedUrl, int tabIndex) { // 每个标签独立数据目录目录名带唯一标识避免冲突 _userDataFolder Path.Combine( Application.StartupPath, TabData, $tab_{tabIndex}); _webView new WebView2 { Dock DockStyle.Fill }; Controls.Add(_webView); InitializeWebViewAsync(seedUrl); } private async void InitializeWebViewAsync(string url) { var env await CoreWebView2Environment.CreateAsync(null, _userDataFolder); await _webView.EnsureCoreWebView2Async(env); _webView.CoreWebView2.Navigate(url); } }标签页关闭时有个内存回收问题直接把 TabPage 从集合里移除不够WebView2 内部有独立的浏览器进程必须显式释放。调用_webView.Dispose()后还要等浏览器进程退出不然多次开关标签后任务管理器里会堆一堆 msedgewebview2.exe。用 TabControl 的TabControl.ControlRemoved事件在移除的时候 Dispose 对应 UserControl 里的 WebView2。3.3 下载与打印等的内置行为怎么接管WebView2 对下载、打印、JS 弹窗这些行为有默认处理但自用浏览器往往需要自定义。下载管理是常见定制点默认点击下载链接会弹系统下载框但你可以接管这个事件自己做静默下载。webView21.CoreWebView2.DownloadStarting (sender, e) { // e.DownloadOperation 可以拿到文件大小和状态 var downloadPath Path.Combine(GetDownloadFolder(), e.DownloadOperation.SuggestedFileName); e.DownloadOperation.Cancel(); // 取消默认下载 e.Handled true; // 标记事件已处理 // 此处启动自己的下载逻辑可配合 HttpClient 等完成 BeginCustomDownload(e.Uri, downloadPath); };DownloadStarting事件里e.Handled true只是告诉 WebView2 你别弹默认界面了真正取消下载要调e.DownloadOperation.Cancel()。这俩容易混淆只设 Handled 不 Cancel下载还是会继续只是没有界面。如果你做下载管理最好把DownloadOperation对象存到列表里它的BytesReceivedChanged事件可以实时刷新进度条。4. 个性化定制入口JS 互操作、注入脚本和下载拦截4.1 页面脚本注入ExecuteScriptAsync 和 InitScript 的区别个性化浏览器最大的价值点在于能往每个页面里注入自己的 JS。WebView2 提供了两种注入方式AddScriptToExecuteOnDocumentCreatedAsync在页面创建时执行早于页面的任何脚本ExecuteScriptAsync在调用时对当前页面立即执行。// 页面创建时就注入适合重写全局函数、拦截 API string initScript // 屏蔽页面里的自动跳转某些站点会强制外链 Object.defineProperty(window, location, { set: function(value) { console.log([Blocked] location change:, value); } }); ; await webView21.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(initScript); // 运行时手动执行适合读取页面状态、触发点击 string result await webView21.CoreWebView2.ExecuteScriptAsync( document.querySelector(#login-btn)?.click(); clicked;);注意AddScriptToExecuteOnDocumentCreatedAsync注入的脚本执行时机是 DocumentCreated也就是 DOM 结构刚建完但外部 JS 还没跑。这个时机适合覆盖全局对象但如果页面的脚本在 script 标签里内联执行顺序上内联脚本在 DocumentCreated 之后你的注入会被覆盖。要彻底拦截得在 NavigationStarting 事件里用WebResourceRequested换掉响应内容工作量直接上一个量级自用浏览器做到注入层就够了。4.2 托管对象互操作AddHostObjectToScript 的使用与约束更高级的玩法是把 C# 对象暴露给页面 JS 调用。这样可以在浏览器里写 JS 直接干桌面的事情比如读写本地文件、调系统通知。AddHostObjectToScript是官方推荐的方式。// 定义暴露给页面的类必须标记 ComVisible [ComVisible(true)] public class NativeBridge { public string GetAppVersion() { return Application.ProductVersion; } public void Notify(string title, string message) { // 调用 WinForms 通知 notifyIcon1.ShowBalloonTip(3000, title, message, ToolTipIcon.Info); } } // 注册到页面注意 name 参数在 JS 里的用法 webView21.CoreWebView2.AddHostObjectToScript(nativeBridge, new NativeBridge());JS 里调用的写法有讲究// 正确的调用方式 window.chrome.webview.hostObjects.nativeBridge.GetAppVersion(); // 常见的翻转错误把对象当普通 JS 对象直接赋值 // const bridge window.chrome.webview.hostObjects.nativeBridge; // bridge.getAppVersion(); // 这样会失败原因是 hostObjects 返回的不是普通 JS 对象是一个代理对象每次调用都要走消息通道。直接把代理赋给变量再调用属性会丢上下文除非调nativeBridge.xxx的完整链。另外一个版本差异某些旧版 Runtime 里方法名首字母大小写敏感C# 里的GetAppVersion在 JS 里写getAppVersion会报 undefined。源码工程里用一个 helper 包一层最保险或者干脆全部方法名小写。4.3 拦截与改写自定义筛选规则的实现思路如果你要给浏览器加“屏蔽指定元素”这类功能有两种实现路线。第一种是 CSS 注入AddScriptToExecuteOnDocumentCreatedAsync里插入 style 标签。第二种是网络拦截用AddWebResourceRequestedFilter配合WebResourceRequested事件。// 拦截图片请求替换成本地占位图适合流量敏感的场景 webView21.CoreWebView2.AddWebResourceRequestedFilter( *://*.example.com/*, CoreWebView2WebResourceContext.Image); webView21.CoreWebView2.WebResourceRequested (sender, e) { if (e.Request.Uri.Contains(ads/) || e.Request.Uri.EndsWith(.gif)) { // 构造一个空的响应体返回 204 可以骗过大部分页面逻辑 e.Response webView21.CoreWebView2.Environment .CreateWebResourceResponse(new MemoryStream(), 204, No Content, ); } };AddWebResourceRequestedFilter的匹配模式支持通配符但匹配逻辑不是正则它按 URL 的 host 和 path 做前缀通配。想精细控制就自己在事件里写规则。CreateWebResourceResponse还能返回自定义 HTML这意味着你可以做“页面 A 被本地页面 B 替换”的操作比如把某个慢速站点整体换成无广告版把返回流换成预写的 HTML。5. 常见问题排查WebView2 翻车现场的四个高频坑5.1 运行时找不到could not find the webview2 runtime现象程序启动报Could not find the WebView2 Runtime或WebView2RuntimeNotFoundException异常。原因目标机器没有装 WebView2 Runtime且系统里也没有 Edge 提供可复用的运行时。Win10 早期版本和精简版系统特别容易出现。另一个隐蔽原因是 Windows 更新把 Edge 卸载或降级了Runtime 跟着被清掉。解决开头提到的检测逻辑要放在程序入口最早的位置检测失败时引导下载离线安装包。还有一条血泪经验不要仅依赖注册表判断HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}这个键在 32 位程序跑在 64 位系统时会读不到用 API 检测最稳。检测通过后如果还报错检查是不是用管理员权限装了 Runtime但程序以普通权限运行——权限不一致在某些精简系统下会有诡异表现。5.2 安装 WebView2 失败exit code 2现象离线安装包双击运行提示Installing WebView2 failed with exit code 2安装立即失败或中途回滚。原因exit code 2 在 Windows Installer 体系里通常意味着“没有足够权限”或“已有更高版本”。前者出现在标准用户账户后者出现在你之前装过较新版本的 Edge 但没完全卸载干净。还有一种情况是安装包位数不匹配x86 程序装了 arm64 的安装包。解决先右键安装包选择“以管理员身份运行”确认失败后检查系统里 Edge 的版本如果 Edge 版本高于要装的 Runtime 版本直接用edge://settings/profiles里的更新把 Edge 升上去Runtime 会自动带上。如果还是有残留用系统自带的“程序和功能”里搜 WebView2卸载后重新装。这个坑最磨人的点多在权限上自用程序直接静默安装也行——setup.exe /silent /install强制管理员提权但要提前弹 UAC。5.3 初始化未完成就调用NullReferenceException 与事件不触发现象Form 加载代码里写着webView21.CoreWebView2.Navigate(https://...)但程序跑起来要么抛空引用异常要么页面不跳转、事件不触发。原因EnsureCoreWebView2Async是异步方法初始化完成前CoreWebView2属性是 null。WinForms 里常见的误用是在Form_Load里同步调用或者用async void但不 await后续代码在初始化完成前就执行了。解决把初始化做成一个状态机。定义一个InitializeStarted标志位所有依赖CoreWebView2的操作都放在EnsureCoreWebView2Async完成之后。更稳妥的做法是初始化完成后触发一个自定义事件所有功能模块订阅这个事件再开始挂事件、注入脚本。不要试图在初始化方法里做所有事那个方法会膨胀到你不想维护。5.4 白屏与界面卡死浏览器进程崩溃和 UI 线程等待现象页面打开后长时间白屏或者拖动窗口时界面卡住任务管理器里msedgewebview2.exe的 CPU 占用很高。原因白屏有两个来源一是浏览器进程崩了二是页面主线程在同步执行大量脚本UI 卡死则常见于在NavigationCompleted或 JS 互操作回调里做了耗时操作比如ExecuteScriptAsync的返回值很大几 MB 的 JSON还直接往控件上填。解决区分崩没崩挂CoreWebView2.ProcessFailed事件能抓到的崩溃基本都是 Render 进程问题抓不到但白屏多半是页面本身的问题自用浏览器可以在白屏超时后强制刷新。UI 卡死这个要自我克制凡是在 JS 互操作回调里拿到的数据一律先拷贝到局部变量、丢后台线程处理处理完再Invoke回 UI 线程。之前我一个编译日志展示功能把 5MB 的字符串直接塞进 TextBox界面肉眼可见地卡了 3 秒改成分页读取之后才感觉得心应手。6. 性能优化与打包从开发机到干净环境验证的完整习惯6.1 启动白屏时长优化精简根目录与首屏策略自用浏览器启动就该有浏览器的样子。首次创建运行时环境时WebView2 会生成用户数据目录和一堆内部文件这个初始化过程是白屏的主要原因。常见优化手段是启动时先展示一个极简的等待页或者显示 Logo 的显示面板等EnsureCoreWebView2Async完成后再切到浏览器界面另一招是把用户数据目录放内存盘或 SSD 的特定分区机械硬盘上首次初始化能差出两秒多。页面打开速度上还有个小技巧CoreWebView2Environment.CreateAsync的userDataFolder参数别用系统临时目录临时目录会被系统清理导致每次启动都是全新环境Cookie 和缓存全丢每天第一次启动尤其慢。把这些路径固定到程序目录或AppData下第二次启动相当于热缓存页面打开速度和 Chrome 的“继续上次浏览”体验基本相当。6.2 打包成安装程序文件清单与干净环境验证WinForms 程序打包成安装程序我惯用 Visual Studio Installer 项目或 Inno Setup。前者适合 Windows 平台后者跨版本兼容更稳。关键不是工具而是你要知道 WebView2 程序打包时必须带上哪些文件程序集、任何引用的第三方 DLL、WebView2Loader.dll 会由 NuGet 包自动拷贝到输出目录自用程序几乎都是一站式构建分发给别人时要注意目标机器上是否有对应的 .NET Framework 运行时。写死一次血的教训我发过一版只在自己机器上测过同事装上后直接白屏弹 Runtime 错误原因是我把 Evergreen 离线安装包漏在安装清单里了。从那以后每次发布前都强制走一遍干净虚拟机流程全新系统、装程序、跑所有功能、看事件查看器有没有异常顺带记录下首次启动到可用状态的秒数。这套流程看起来耗时间实际上能挡掉八成“在我机器上好好的”这类翻车。自用浏览器做到能编译、能跑、能注入脚本基本就是一个顺手的工具了。更进阶的玩法还有拦截网页请求做本地调试、把 JS 采集的数据通过互操作灌进桌面表格都是这个骨架顺带的。如果你手头正好需要这样一个基线工程这份源码顺着初始化、标签、注入这三条主线改起来半天内就能出一个自己顺手的小工具。希望帮到你。本文还有配套的精品资源点击获取