尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

WinForm + WebView2 自用浏览器开发实战:从环境部署到双向通信

WinForm + WebView2 自用浏览器开发实战:从环境部署到双向通信 简介这是一份基于 WebView2 内核的个性化浏览器桌面程序源码面向具备一定 C# 与 WinForm 基础的开发者用于快速搭建类似 Edge、Chrome 的定制浏览器。项目使用 Visual Studio 2019 编写编译即可运行适合学习桌面端浏览器架构、WebView2 集成与界面定制等场景。压缩包共 134 个文件约 15.33MB包含 47 个 dll 依赖库、32 个 png 界面素材、8 个 cs 源码文件及 sln、csproj 工程文件另有 xml、config、resx 等配置与资源文件结构完整便于二次开发。目前已有 1097 人学习下载。通过源码可掌握 WebView2 初始化、窗体布局、资源加载与依赖引用的完整流程并参考配套指导文章理解关键实现是自用浏览器与桌面程序开发的一份实用参考。1. 用 WinForm WebView2 搭一个自用浏览器为什么这套组合比你想的能打很多人第一次听到「WinForm 套 WebView2 做浏览器」第一反应是这玩意儿能行吗——WinForm 不是早就被贴上「老古董」标签了但如果你真的动手做过一个自用桌面程序就会发现这套组合的性价比高得离谱WinForm 负责窗口、托盘、快捷键、文件系统这些本地能力WebView2 负责渲染现代网页底层是 Chromium 内核HTML5、CSS3、ES6 全都不在话下。你不需要从零写一个排版引擎也不需要跟 CEF 那套庞大的依赖搏斗。标题里说的「自用-个性化浏览器-桌面程序开发-程序源码」核心就是这件事用最少的代码量拿到一个你能完全掌控的浏览器外壳。适合谁适合需要内嵌网页做工具面板的 C# 开发者、想给自己做一个专属信息聚合入口的运维、以及被 Electron 打包体积劝退的桌面端选手。下面从环境到源码结构一步步拆开讲。2. 环境准备与 WebView2 Runtime 的离线部署2.1 为什么 WebView2 不是 NuGet 装完就能跑这是新手翻车率最高的地方。你在 Visual Studio 里通过 NuGet 装了Microsoft.Web.WebView2包F5 一跑弹出一个空白窗体或者直接抛异常提示Could not find the WebView2 Runtime。原因很简单NuGet 包只是 .NET 侧的托管封装真正干活的渲染引擎是独立安装的 WebView2 Runtime它跟你的程序是两回事。开发机上通常已经装了Edge 浏览器自带但你要分发给别人、或者部署到一台干净的 Windows 机器上就必须自己处理 Runtime 的安装。常见做法有三种一是让用户自己去微软官网下载 Evergreen Bootstrapper 在线安装二是用 Fixed Version 模式把整个 Runtime 打包进你的程序目录三是在安装包里静默调用 Bootstrapper。自用场景我一般推荐第二种因为版本锁定不会因为用户机器上 Edge 自动更新导致你的程序行为突变。2.2 离线包获取与 Fixed Version 目录结构Fixed Version 的 Runtime 可以从微软官方渠道获取压缩包解压后是一个包含msedgewebview2.exe和大量 dll 的目录。你需要在项目里建一个文件夹比如WebView2Runtime把解压内容放进去并确保编译时复制到输出目录。!-- 在 .csproj 里加上这段保证 Runtime 文件被复制到输出目录 -- ItemGroup Content IncludeWebView2Runtime\**\*.* CopyToOutputDirectoryPreserveNewest/CopyToOutputDirectory /Content /ItemGroup这段配置的作用是让 MSBuild 把WebView2Runtime目录下的所有文件当作内容资源编译时复制到bin\Debug或bin\Release下。PreserveNewest表示只复制比目标新的文件避免每次全量拷贝拖慢编译。注意路径里的反斜杠在 Windows 下没问题但如果你用命令行 msbuild要确认工作目录正确。2.3 初始化代码指定 Runtime 路径并创建环境光有文件还不够你得在代码里告诉 WebView2 去哪里找 Runtime。默认它会去注册表找 Evergreen 版本找不到就报错。用 Fixed Version 需要显式创建CoreWebView2Environment。// 在 Form 的 Load 事件或构造函数里调用 private async Task InitWebViewAsync() { // 指向你的 Runtime 目录注意是包含 msedgewebview2.exe 的那一层 string runtimePath Path.Combine( AppDomain.CurrentDomain.BaseDirectory, WebView2Runtime); // 用户数据目录缓存、Cookie 都放这里自用程序建议放在程序目录下方便迁移 string userDataPath Path.Combine( AppDomain.CurrentDomain.BaseDirectory, UserData); var env await CoreWebView2Environment.CreateAsync( browserExecutableFolder: runtimePath, userDataFolder: userDataPath, options: null); // webView21 是拖到窗体上的 WebView2 控件 await webView21.EnsureCoreWebView2Async(env); // 初始化完成后就可以导航了 webView21.CoreWebView2.Navigate(https://www.bing.com); }CreateAsync的第一个参数browserExecutableFolder就是 Fixed Version 的路径传 null 则走 Evergreen。第二个参数userDataFolder很关键如果不指定WebView2 会默认写到%LocalAppData%下跟你的程序名相关的目录自用程序想绿色化、想备份登录状态就自己指定到程序目录。EnsureCoreWebView2Async是异步的必须 await否则后面直接调Navigate会空引用。注意Fixed Version 的 Runtime 体积不小压缩后也有 100MB 以上解压后更大。如果你的程序要分发给别人打包时得考虑这个体积。自用的话无所谓扔在 D 盘一个固定目录所有自用工具共用一份也行。3. 浏览器核心功能地址栏、导航、新窗口拦截与下载3.1 地址栏与导航的最小闭环一个浏览器最基础的三件事输入网址能跳、前进后退能点、刷新能按。WinForm 里拖一个 TextBox 做地址栏一个 Panel 放几个 Button 做工具栏剩下的交给 WebView2 的事件。// 地址栏回车导航 private void addressBar_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.Enter) { e.SuppressKeyPress true; // 防止回车 ding 一声 string url addressBar.Text.Trim(); // 简单判断没有协议头就补 https if (!url.StartsWith(http://) !url.StartsWith(https://)) { // 如果看起来像域名就补协议否则当搜索词处理 if (url.Contains(.) !url.Contains( )) url https:// url; else url https://www.bing.com/search?q Uri.EscapeDataString(url); } webView21.CoreWebView2.Navigate(url); } } // 页面导航完成后同步地址栏和标题 private void webView21_NavigationCompleted( object sender, CoreWebView2NavigationCompletedEventArgs e) { if (e.IsSuccess) { addressBar.Text webView21.Source?.ToString() ?? ; this.Text webView21.CoreWebView2.DocumentTitle; } else { // 导航失败显示一个简单的错误页 webView21.CoreWebView2.NavigateToString( h2页面加载失败/h2p错误码 e.WebErrorStatus /p); } }KeyDown里SuppressKeyPress是为了阻止 WinForm 默认的提示音。地址栏输入判断逻辑是自用浏览器的常见做法带点且不带空格的当域名否则当搜索词。NavigationCompleted里同步地址栏和窗体标题失败时用NavigateToString显示错误页比弹 MessageBox 体验好。3.2 拦截新窗口让 target_blank 在当前窗口打开WebView2 默认行为是网页里target_blank的链接会触发NewWindowRequested事件如果你不处理它什么都不会发生——用户点了没反应以为程序卡了。自用浏览器一般不希望弹出一堆新窗口直接在当前视图打开最省事。// 在 EnsureCoreWebView2Async 之后订阅 webView21.CoreWebView2.NewWindowRequested (s, e) { // 取消默认的新窗口行为 e.Handled true; // 在当前 WebView 里打开目标地址 webView21.CoreWebView2.Navigate(e.Uri); };e.Handled true告诉 WebView2 你已经处理了不要自己再开窗口。然后手动 Navigate 到e.Uri。如果你确实想要新窗口比如自用场景下想同时看两个页面可以动态 new 一个 Form里面再放一个 WebView2但要注意每个 WebView2 实例最好共享同一个CoreWebView2Environment否则用户数据目录会冲突。3.3 下载行为与文件保存WebView2 的下载默认会弹出一个下载栏但那个 UI 是 Edge 风格的跟你的 WinForm 界面不搭。自用程序可以接管下载自己决定存哪里。webView21.CoreWebView2.DownloadStarting (s, e) { // 自用场景直接存到 Downloads 目录不弹对话框 string downloads Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), Downloads); e.ResultFilePath Path.Combine(downloads, e.ResultFilePath); // 不设置 Cancel让它继续下载 };DownloadStarting事件里可以改ResultFilePath也可以设e.Cancel true直接拒绝。自用浏览器我一般直接扔到系统下载目录省去每次选路径的麻烦。如果你要做下载管理面板可以在这里记录文件名和状态配合BytesReceivedChanged事件更新进度条。4. 个性化定制注入脚本、快捷键与界面美化4.1 用 AddScriptToExecuteOnDocumentCreated 做全局注入自用浏览器最大的乐趣就是「魔改」网页。比如你想让所有页面背景变暗、去掉某些烦人的浮动元素、或者自动填充登录表单都可以通过注入 JavaScript 实现。关键是注入时机必须在页面脚本执行之前注入否则你的代码可能被页面覆盖。// 在初始化完成后调用一次之后每个页面加载前都会执行 await webView21.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync( // 去掉所有 position:fixed 的悬浮广告简单粗暴版 (function() { var style document.createElement(style); style.textContent *[style*position:fixed] { display: none !important; } ; document.head.appendChild(style); })(); );AddScriptToExecuteOnDocumentCreatedAsync注册的脚本会在每个新文档创建时执行包括 iframe。注意脚本里用了 IIFE 包裹避免变量污染。style.textContent里用模板字符串写 CSS反引号在 C# 的逐字字符串里没问题。这段代码的效果是隐藏所有内联样式里带position:fixed的元素对很多小网站的悬浮广告有效但对用 class 控制定位的无效——那需要更复杂的匹配。4.2 快捷键绑定F5 刷新、F12 开发者工具、CtrlT 新标签WinForm 窗体默认的KeyPreview是 falseWebView2 会吃掉大部分键盘事件。你需要设置KeyPreview true并在KeyDown里处理或者用CoreWebView2的AcceleratorKeyPressed事件。// 在 Form 构造函数里 this.KeyPreview true; private void MainForm_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode Keys.F5) { webView21.CoreWebView2.Reload(); e.Handled true; } else if (e.KeyCode Keys.F12) { // 打开开发者工具自用调试必备 webView21.CoreWebView2.OpenDevToolsWindow(); e.Handled true; } else if (e.Control e.KeyCode Keys.T) { // 自用简化版新标签就是新开一个窗体 var newForm new MainForm(); newForm.Show(); e.Handled true; } }KeyPreview true让窗体先于控件收到按键。OpenDevToolsWindow是 WebView2 自带的开发者工具跟 Chrome 的 F12 几乎一样调网页兼容性时非常有用。CtrlT 这里简单处理成新开一个 MainForm 实例每个实例有自己的 WebView2 和用户数据目录——注意如果多个实例共用同一个userDataFolder会报错所以要么每个实例用不同目录要么用单例模式管理。4.3 界面美化无边框窗口与自定义标题栏WinForm 默认的标题栏又老又丑自用浏览器可以做成无边框自己画标题栏。但无边框之后拖动、最大化、最小化、关闭都要自己实现。// 窗体属性设置 this.FormBorderStyle FormBorderStyle.None; this.DoubleBuffered true; // 减少闪烁 // 实现拖动在标题栏 Panel 上监听鼠标事件 private Point _dragStart; private bool _dragging; private void titleBar_MouseDown(object sender, MouseEventArgs e) { if (e.Button MouseButtons.Left) { _dragging true; _dragStart e.Location; } } private void titleBar_MouseMove(object sender, MouseEventArgs e) { if (_dragging) { this.Location new Point( this.Location.X e.X - _dragStart.X, this.Location.Y e.Y - _dragStart.Y); } } private void titleBar_MouseUp(object sender, MouseEventArgs e) { _dragging false; }FormBorderStyle.None去掉系统边框DoubleBuffered减少重绘闪烁。拖动逻辑是记录鼠标按下时的相对位置移动时按差值更新窗体 Location。最大化按钮可以设this.WindowState FormWindowState.Maximized但无边框最大化会盖住任务栏需要用Screen.FromControl(this).WorkingArea手动计算尺寸。这些细节比较琐碎但做出来之后界面完全由你掌控配合 WebView2 里加载的网页整体感很强。5. 避坑与排查WebView2 在 WinForm 里的五个血泪教训5.1 现象程序在开发机正常拷到别人电脑报「Could not find the WebView2 Runtime」原因开发机装了 EdgeEvergreen Runtime 已存在目标机器没有且你的程序没有附带 Fixed Version 或安装引导。解决要么在安装包里集成 Bootstrapper 静默安装要么用 Fixed Version 模式并把 Runtime 目录一起拷贝。判断方法在目标机器上检查注册表HKLM\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}是否存在或者直接看程序目录下有没有WebView2Runtime文件夹。5.2 现象多个 WebView2 控件同时初始化第二个报「用户数据目录已被占用」原因每个 WebView2 实例默认使用同一个用户数据目录Chromium 内核不允许两个进程同时写同一个目录。解决要么所有 WebView2 共享同一个CoreWebView2Environment推荐要么给每个实例指定不同的userDataFolder。共享 Environment 的写法是创建一个静态的CoreWebView2Environment实例所有控件用EnsureCoreWebView2Async(sharedEnv)。5.3 现象注入的 JavaScript 不生效或者时灵时不灵原因AddScriptToExecuteOnDocumentCreatedAsync是异步的如果你在EnsureCoreWebView2Async之后立刻调用但没有 await可能在页面开始加载后才注册成功导致第一个页面漏掉。解决确保 await 这个调用并且最好在NavigationStarting之前完成注册。如果脚本依赖 DOM 结构用DOMContentLoaded事件包一层不要直接在脚本顶层操作 DOM。5.4 现象WebView2 控件在窗体缩放时闪烁或黑屏原因WinForm 的绘制机制和 WebView2 的合成器不同步尤其是无边框窗体拖动大小时。解决设置窗体的DoubleBuffered true并且在 Resize 过程中用SuspendLayout和ResumeLayout包住。如果还不行考虑在 Resize 时暂时隐藏 WebView2结束后再显示——虽然粗暴但有效。另外WebView2 控件的DefaultBackgroundColor可以设成白色或透明减少黑屏感。5.5 现象F5 刷新时页面滚动位置丢失用户体验差原因Reload()是完整重新加载浏览器不会保留滚动位置。解决如果只是想刷新数据可以用ExecuteScriptAsync(location.reload())效果一样。要保留滚动位置需要在刷新前用脚本记录window.scrollY加载完成后恢复。更优雅的做法是用 SPA 式的局部更新但这取决于目标网页是否支持。自用浏览器如果经常刷同一个页面可以考虑用CoreWebView2.ExecuteScriptAsync定时拉取数据而不是整页刷新。6. 进阶技巧用 WebMessage 打通 C# 与网页的双向通信前面讲的都是「壳」的层面真正让自用浏览器变得独一无二的是 C# 和网页之间的数据通道。WebView2 提供了WebMessageReceived和PostWebMessageAsJson让你可以在网页里调 C# 方法也可以在 C# 里往网页推数据。这个能力用好了你就能把任意网页改造成你的专属工具面板。先看网页端怎么发消息给 C#// 在网页的 JavaScript 里 window.chrome.webview.postMessage({ action: saveNote, content: document.querySelector(#note).value });然后在 C# 端接收webView21.CoreWebView2.WebMessageReceived (s, e) { string json e.WebMessageAsJson; // 用 System.Text.Json 解析 using var doc System.Text.Json.JsonDocument.Parse(json); var root doc.RootElement; string action root.GetProperty(action).GetString(); if (action saveNote) { string content root.GetProperty(content).GetString(); File.WriteAllText(note.txt, content); // 回复网页 webView21.CoreWebView2.PostWebMessageAsJson( {\status\:\ok\}); } };反过来C# 主动往网页发消息// 假设网页里注册了监听 webView21.CoreWebView2.PostWebMessageAsJson( {\type\:\theme\,\value\:\dark\});网页端监听window.chrome.webview.addEventListener(message, event { if (event.data.type theme) { document.body.classList.add(dark-theme); } });这套机制的关键在于postMessage发送的对象会被序列化成 JSONC# 端用WebMessageAsJson拿到原始 JSON 字符串自己解析。PostWebMessageAsJson发送的也必须是合法 JSON 字符串不能直接传 C# 对象。自用场景下我习惯定义一个简单的协议{ action: string, payload: any }C# 端用 switch 分发网页端用事件监听。这样你可以让网页上的一个按钮触发本地文件保存、让 C# 定时抓取的数据推送到网页表格里、甚至用网页做设置界面C# 只负责读写配置文件。一个具体的例子我给自己做的信息聚合面板左侧是 WebView2 加载的一个本地 HTML 页面右侧是 WinForm 的 TreeView。点击 TreeView 节点时C# 通过PostWebMessageAsJson把节点对应的 URL 推给网页网页用 JavaScript 动态创建 iframe 或者更新内容区域。网页里的「收藏」按钮则通过postMessage把当前 URL 发回 C#C# 写入 SQLite。整个过程中网页负责展示和交互C# 负责持久化和系统调用分工明确。提示WebMessageReceived的事件参数里WebMessageAsJson和TryGetWebMessageAsString二选一。如果你确定网页发的是字符串用后者更方便如果发的是对象用前者拿 JSON 自己解析。不要混用否则会抛异常。最后说一个我踩过的坑AddScriptToExecuteOnDocumentCreatedAsync注入的脚本里window.chrome.webview对象在页面加载初期可能还不存在。如果你在注入脚本里直接调postMessage会报undefined。解决办法是用DOMContentLoaded或者setTimeout包一层确保 WebView2 的通信桥接已经就绪。这个细节在官方文档里没有特别强调但实际开发中很容易遇到。这套 WinForm WebView2 的方案我从第一个自用工具到现在已经迭代了七八个版本最大的体会是不要试图一次性做成一个「完整浏览器」而是先跑通「打开一个页面、能导航、能注入脚本」这个最小闭环然后根据你自己的实际需求往上加。需要下载管理就加下载事件需要暗色模式就注入 CSS需要跟本地文件交互就用 WebMessage。每加一个功能都是解决一个具体问题而不是为了功能而功能。希望帮到你。本文还有配套的精品资源点击获取
返回列表