资讯详情

资讯详情

VS2010 下 CefSharp WinForms 嵌入与 JS 互调

简介面向仍在使用 VS2010 与 .NET Framework 4.0 的 WinForm 开发者这份示例工程演示如何借助 CefSharp.WinForms 49.0.1 把 Chromium 内核嵌入桌面程序替代老旧的 WebBrowser 控件解决网页兼容性与脚本交互能力不足的问题属于可直接上手的中初级实战参考。压缩包共 319 个文件、约 189.28MB其中 174 个 pak 与 52 个 dll 构成 CEF 运行时依赖与本地化资源另有 7 个 cs 源文件、2 个 csproj 工程及 xml、config、bin、exe 等配套项。代码覆盖 NuGet 引用配置、Cef.Initialize 初始化、ChromiumWebBrowser 控件创建、加载完成与状态变更事件监听、ExecuteScriptAsync 执行脚本、CefSettings 定制如禁用 GPU 加速、设置缓存路径以及退出时调用 Cef.Shutdown 释放资源。工程可直接编译运行也可作为二次开发骨架已有 3432 人学习下载适合需要快速验证内嵌浏览器方案的 .NET 桌面开发者参考。1. 从一次白屏说起VS2010 .NET 4.0 下还能用的 CefSharp.WinForms 示例这份 C# WinForm CefSharp 示例代码解决的是 VS2010 .NET 4.0 老项目里嵌一个 Chromium 浏览器控件的刚需。前阵子维护一个模拟项目X客户端只能跑在 .NET 4.0 上业务页面已经是现代前端构建出来的WebBrowser 控件一打开就脚本报错换成 CefSharp 又连续白屏。后来把版本对齐、初始化顺序、输出目录和 x86/x64 这几件事拆开验证才整理出一套能编译、能加载本地 HTML、能做 C# 与 JS 互调的最小骨架。它适合还在维护老 WinForm 客户端、不能随便升级框架的人也适合想先跑通 CefSharp 再叠业务逻辑的开发者。下面按“选包、起工程、调生命周期、部署、排错”的顺序拆开讲代码按 VS2010 的 C# 4.0 语法写不依赖 async/await。2. 版本对齐先确认 CefSharp 包里有 net40再谈 WinForm 嵌浏览器2.1 为什么 VS2010 下不能随便拿最新 CefSharp 包VS2010 自带的是 .NET 4.0 和 C# 4.0这两个约束会同时卡住 CefSharp 的托管层和示例代码。CefSharp 的托管程序集有目标框架要求较新的包往往把 lib 目录切到 net45、net452 甚至更高直接引用会提示程序集依赖不兼容C# 4.0 又不支持 async/await网上大量 CefSharp 示例是 C# 5 以后的写法复制进 VS2010 会直接编译不过。另一个容易被忽略的是原生层CefSharp 依赖 libcef.dll、CefSharp.BrowserSubprocess.exe 和一整套 CEF 资源文件它们分 x86 和 x64项目平台目标设成 AnyCPU 时很容易把错误架构的原生 DLL 带进进程。我一般选包时先做三件事解压 nupkg看 lib 目录下有没有 net40 或 net4.0 文件夹看 nuspec 里对 .NET Framework 的依赖再看 cef.redist.x86 / cef.redist.x64 包里是不是同时带了托管 DLL 和原生文件。没有 net40 目录的包不要硬上换一个能对上的旧分支比后期改程序集绑定省事。版本号不必记直接看包内目录和 nuspec 最稳。选好之后把 nupkg 当压缩包解压手工引用 DLL比在 VS2010 里折腾老版本 NuGet 更可控。2.2 手工还原一个可编译的工程骨架在 VS2010 里新建 Windows Forms Application目标框架选 .NET Framework 4.0。进配置管理器把平台从 AnyCPU 改成 x86新建平台时不要勾“创建解决方案的平台”直接把当前项目改成 x86。然后把 CefSharp 包里的 CefSharp.dll、CefSharp.Core.dll、CefSharp.WinForms.dll 加到引用引用属性里的 Copy Local 设为 True。原生文件不通过引用走直接复制到项目下的 CefSharp 目录再在项目里包含这些文件属性设为“如果较新则复制”。!-- 项目文件里重点看这几项等价于在 VS2010 属性面板里设置 -- PropertyGroup TargetFrameworkVersionv4.0/TargetFrameworkVersion PlatformTargetx86/PlatformTarget Prefer32Bitfalse/Prefer32Bit /PropertyGroup这段配置的核心是 TargetFrameworkVersion 和 PlatformTarget。VS2010 的 WinForm 项目模板默认可能是 AnyCPUCefSharp 在 64 位系统上会按进程位数去找原生 DLL如果输出目录里只有 x86 的 libcef.dll就会出现进程启动后白屏或直接退出。Prefer32Bit 是给后续高版本 MSBuild 看的VS2010 下主要靠 PlatformTarget 控制。平台目标定下来之后输出目录只放对应架构的 CEF 文件不要 x86 和 x64 混放混放时谁先被加载看运气玄学问题会很多。2.3 最小验证先加载 about:blank再加载本地 HTML工程骨架起来后不要急着接业务页面。先写一个最小窗体在 Load 事件里动态创建 ChromiumWebBrowser加载 about:blank确认能出空白页而不是白屏。这个步骤能过滤掉大部分原生 DLL 和子进程路径问题。确认 about:blank 正常后再把 URL 换成 Application.StartupPath 下的 index.html。本地文件路径用 file:/// 前缀反斜杠换成斜杠中文目录先避开。private void MainForm_Load(object sender, EventArgs e) { // 先加载 about:blank确认 CefSharp 原生层和子进程都正常 var browser new ChromiumWebBrowser(about:blank); browser.Dock DockStyle.Fill; this.Controls.Add(browser); // 原生层确认后再换成本地 HTML string htmlPath Application.StartupPath.Replace(\\, /) /index.html; browser.Load(file:/// htmlPath); }这段代码没有用设计器拖控件原因是 ChromiumWebBrowser 在设计时初始化会拉子进程VS2010 设计器经常直接崩。动态创建放在 Form.Load 里设计器只看到普通窗体打开设计器不会触发 Cef 初始化。Dock 设为 Fill 让浏览器铺满窗体Load 方法传 URL 即可。如果 about:blank 都白屏先别改代码去输出目录检查 libcef.dll、CefSharp.BrowserSubprocess.exe 和 locales 是否齐全再看项目平台目标和 DLL 架构是否一致。3. 把浏览器控件用起来初始化顺序、生命周期和 C# 与 JS 互调3.1 Cef.Initialize 和 Cef.Shutdown 的配对Cef.Initialize 建议放在 Program.Main 里、Application.Run 之前并且全进程只调用一次。常见错误是在窗体构造函数里初始化每开一个窗体调一次第二次就抛异常或者初始化放在 Load 里子进程路径还没准备好。初始化参数里 BrowserSubprocessPath 必须指向输出目录下的 CefSharp.BrowserSubprocess.exeCachePath 和 UserDataPath 指向有写权限的目录。老项目装在 Program Files 下时Application.StartupPath 往往不可写缓存会静默失败表现就是页面加载一半或登录态丢失。[STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); // 旧分支的 JS 绑定开关必须在 Cef.Initialize 之前设置 CefSharpSettings.LegacyJavascriptBindingEnabled true; var settings new CefSettings(); settings.BrowserSubprocessPath Path.Combine(Application.StartupPath, CefSharp.BrowserSubprocess.exe); settings.CachePath Path.Combine(Application.StartupPath, Cache); settings.UserDataPath Path.Combine(Application.StartupPath, UserData); settings.Locale zh-CN; settings.LogSeverity LogSeverity.Info; settings.LogFile Path.Combine(Application.StartupPath, cef.log); try { Cef.Initialize(settings); } catch (Exception ex) { MessageBox.Show(Cef 初始化失败 ex.Message); return; } Application.Run(new MainForm()); }参数里 BrowserSubprocessPath 是最容易漏的不设时 CefSharp 会按默认规则找子进程部署目录一变就找不到。CachePath 和 UserDataPath 分开缓存和用户数据不要塞进只读目录。Locale 设成 zh-CN 对中文界面和输入法更稳。LogSeverity 和 LogFile 是排错用的白屏时先看 cef.log 里有没有“failed to load libcef”或子进程启动失败。CefSharpSettings.LegacyJavascriptBindingEnabled 只对旧版 RegisterJsObject 生效新版绑定 API 不需要但老分支不设置会报对象未定义。3.2 ChromiumWebBrowser 的创建时机与释放ChromiumWebBrowser 的创建放在窗体 Load 事件里比较稳FormClosing 里先 Dispose 浏览器再 Cef.Shutdown。顺序反了会出现关闭窗体后 CefSharp.BrowserSubprocess.exe 还在任务管理器里第二次打开程序连不上子进程。Dispose 之后把引用置空避免 FormClosing 被多次触发时重复释放。如果窗体里有多个浏览器实例每个实例都要 Dispose但 Cef.Shutdown 全进程只调一次。private ChromiumWebBrowser browser; private void MainForm_Load(object sender, EventArgs e) { browser new ChromiumWebBrowser(about:blank); browser.Dock DockStyle.Fill; this.Controls.Add(browser); } private void MainForm_FormClosing(object sender, FormClosingEventArgs e) { if (browser ! null) { browser.Dispose(); browser null; } Cef.Shutdown(); }这段代码的关键是生命周期配对。Load 里创建FormClosing 里释放Cef.Shutdown 放在窗体关闭时而不是 Application.Exit 事件里避免窗体还没释放就先关 Cef。Dock 设为 Fill 后不需要再调 Anchor。如果窗体里还有其他控件建议给浏览器一个 Panel 容器浏览器 Dock 到 Panel业务控件放在 Panel 外面后面换皮肤或加工具栏时不用改浏览器布局。3.3 JS 调 C#RegisterJsObject 的绑定时机与命名旧分支常用 RegisterJsObject 把 C# 对象暴露给页面。绑定要放在浏览器创建之后、页面 Load 之前绑定太晚页面脚本已经执行完window.bridge 就是 undefined。第三个参数 camelCaseJavascriptNames 决定 C# 方法名是否转成首字母小写传 false 时 JS 里保持 C# 原名传 true 时 GetVersion 变成 getVersion。这个参数和前端约定必须一致否则前端按小写调、后端按大写绑控制台只会报未定义。public class JsBridge { public string GetVersion() { return 1.0; } public void Notify(string message) { // JS 调用可能不在 UI 线程更新控件前用 BeginInvoke } } private void MainForm_Load(object sender, EventArgs e) { browser new ChromiumWebBrowser(about:blank); browser.Dock DockStyle.Fill; this.Controls.Add(browser); // 第三个参数 falseJS 里用 window.bridge.GetVersion() browser.RegisterJsObject(bridge, new JsBridge(), false); browser.Load(file:/// Application.StartupPath.Replace(\\, /) /index.html); }JsBridge 里的方法尽量简单参数用 string、int、bool 这类基础类型复杂对象让前端序列化成 JSON 字符串再传减少绑定层序列化差异。Notify 里如果要更新 WinForm 控件不要直接改JS 调用线程不一定是 UI 线程用 BeginInvoke 包一层。注册对象名不要用 window、document 这类保留字也不要和页面已有全局变量重名重名时页面自己的对象会被覆盖排查起来很费时间。3.4 C# 调 JS不用 async/await 的 EvaluateScriptAsync 写法VS2010 的 C# 4.0 不支持 async/await但 EvaluateScriptAsync 返回的是 Task可以用 ContinueWith 接。回调里判断 task.IsFaulted、task.IsCanceled再取 task.Result.Success 和 task.Result.Result。ContinueWith 的第二个参数传 TaskScheduler.FromCurrentSynchronizationContext()让回调回到 UI 线程避免在回调里直接操作控件时抛跨线程异常。这个写法比 Thread.Sleep 等页面加载可靠得多。private void ReadDocumentTitle() { browser.EvaluateScriptAsync(document.title).ContinueWith(task { if (task.IsFaulted || task.IsCanceled) { return; } var response task.Result; if (response.Success) { MessageBox.Show(Convert.ToString(response.Result)); } }, TaskScheduler.FromCurrentSynchronizationContext()); }EvaluateScriptAsync 的参数是 JavaScript 表达式返回值会走 JSON 序列化字符串、数字、布尔值都能直接转DOM 对象不要直接返回先转成 JSON.stringify 再传回来。ContinueWith 里不要假设 task.Result 一定不为空脚本抛异常时 Success 为 falseResult 可能是 null。需要连续调多个脚本时不要嵌套 ContinueWith容易把线程调度绕晕拆成多个方法按业务顺序调或者在前端用一个初始化完成标记C# 侧只探这一个标记。4. 部署不是复制一个 exe输出目录、x86/x64 与 VC 运行时的核对表4.1 输出目录该放哪些文件CefSharp 的部署目录不是只有一个 exe托管 DLL、原生 DLL、子进程、资源文件、语言包都要在。不同分支文件名单会变最稳的办法是看 cef.redist 包里的 README 或示例输出目录把那一套整体复制过来再删掉不需要的语言包。下面这张表是常见必须项用来核对有没有漏不是固定清单。文件或目录作用常见漏法CefSharp.dll托管公共接口只引用了 WinForms 包漏了公共接口CefSharp.Core.dll托管核心引用有输出目录没有CefSharp.WinForms.dllWinForms 控件项目引用正常发布时漏复制CefSharp.BrowserSubprocess.exe浏览器子进程没设 BrowserSubprocessPath或没复制libcef.dllCEF 原生核心架构不对或被杀软隔离chrome_elf.dll加载与崩溃辅助只复制 libcef.dll漏了它icudtl.datICU 数据页面中文乱码或直接白屏natives_blob.bin、snapshot_blob.binV8 快照启动时报快照加载失败locales 目录语言包只放 en-US.pak中文环境异常swiftshader 或 d3dcompiler_47.dll软件渲染与图形依赖虚拟机或无 GPU 环境黑屏核对时不要只看项目引用列表要看 bin 目录实际输出。VS2010 里非引用文件靠“复制到输出目录”属性控制有时候文件在项目里属性还是“不复制”调试时从 obj 目录能跑发布就缺文件。建议在项目里建一个 CefSharp 文件夹把原生文件全部放进去属性统一设成“如果较新则复制”发布时整目录带走。4.2 平台目标与原生 DLL 架构AnyCPU 为什么容易翻车AnyCPU 在 64 位系统上默认跑 64 位进程但很多 CefSharp 包默认带的是 x86 原生文件进程位数和 DLL 架构对不上libcef.dll 加载失败表现就是窗体出来但浏览器区域全白。反过来项目设了 x64输出目录却是 x86 的 cef.redist也一样白屏。排查时先看任务管理器里进程有没有“32 位”标记再用 dumpbin 看 libcef.dll 的 machine 字段两边必须一致。:: 在 Visual Studio 命令提示符里执行查看 libcef.dll 是 x86 还是 x64 dumpbin /headers libcef.dll | findstr machine输出里 14C 表示 x868664 表示 x64。项目平台目标设 x86 时输出目录只放 x86 的 CEF 文件设 x64 时只放 x64。不要为了兼容两种机器在一个目录里混放CefSharp 不会自动按位数切目录混放时加载到哪个看文件系统顺序。确实要同时支持 32 位和 64 位就出两个安装包或者启动器里判断 IntPtr.Size 后切工作目录普通业务客户端没必要做这么复杂。4.3 VC 运行时与日志白屏时先看 cef.logCEF 原生层依赖 VC 运行时缺运行库时进程可能直接退出也可能弹一个系统错误框后白屏。不要凭记忆装年份用 Dependencies 或 dumpbin /dependents 看 libcef.dll 依赖哪些 msvcp、vcruntime缺哪个装哪个x86 和 x64 分开装。装完重启一下客户端再做验证有时候运行库刚装完当前进程还没重新加载。var settings new CefSettings(); settings.LogSeverity LogSeverity.Info; settings.LogFile Path.Combine(Application.StartupPath, cef.log);日志文件要放在有写权限的目录Program Files 下普通用户不可写时日志不会生成看起来像“没日志”实际是写不进去。cef.log 里重点看 libcef.dll 加载路径、BrowserSubprocess 启动参数、GPU 初始化失败、字体加载失败。GPU 相关报错可以先加settings.DisableGpu true验证是不是显卡驱动或虚拟机渲染问题确认后再决定是否长期禁用。日志级别不要长期开 Verbose文件涨得很快排完障改回 Info 或 Disable。5. 避坑与排查白屏、设计器闪退、JS 未定义、进程残留的 5 条记录5.1 设计器一打开就崩别把 ChromiumWebBrowser 拖进设计器现象是双击 MainForm.cs 进设计器VS2010 停几秒后直接退出或者弹“未将对象引用设置到对象的实例”。原因是 ChromiumWebBrowser 在设计时也会走构造函数触发 Cef 初始化和子进程创建设计器进程里没有完整的 CEF 运行环境直接崩。解决方式是窗体上不放浏览器控件在 Load 事件里用代码创建设计器里只留一个 Panel 占位。已经拖进去的把控件从设计器删掉构造函数里的初始化代码移到 Load 里重新生成后再开设计器。5.2 运行后白屏或黑屏先查 libcef.dll 和架构现象是窗体正常出来浏览器区域全白cef.log 里可能有“failed to load libcef.dll”或“browser subprocess exited”。原因按概率排输出目录没有 libcef.dlllibcef.dll 架构和进程位数不一致BrowserSubprocessPath 没设置或路径不对VC 运行时缺失。解决顺序是先看输出目录文件齐不齐再用 dumpbin 看架构然后确认 BrowserSubprocessPath 指向实际 exe最后用 Dependencies 查 libcef.dll 的依赖。四项都过还白屏把 LogSeverity 开到 Verbose看日志里第一个 error 出现在哪一步不要从最后一个报错往前猜。5.3 编译报 async/await 错误VS2010 的 C# 4.0 不认现象是从网上复制的 CefSharp 示例粘贴进项目后async、await关键字下面全是红线错误提示“找不到类型或命名空间名称 async”之类。原因是 VS2010 默认 C# 4.0async/await 是 C# 5 才有的语法。解决方式不是硬升框架而是把异步调用改成 ContinueWith 写法EvaluateScriptAsync 返回 Task用 TaskScheduler.FromCurrentSynchronizationContext() 回 UI 线程。如果项目里大量 async 代码考虑单独抽一个 C# 5 的辅助程序集但老项目里通常不值得按最小改动改成回调更稳。5.4 JS 调用 C# 报 undefined绑定时机和命名踩坑现象是页面控制台报window.bridge is undefined或者bridge.GetVersion is not a function。原因常见三种RegisterJsObject 在页面 Load 之后才调用camelCaseJavascriptNames 参数和前端调用大小写不一致LegacyJavascriptBindingEnabled 没在 Cef.Initialize 之前打开。解决方式是先确认绑定代码在 Load 之前执行再在页面里打印Object.keys(window.bridge)看实际暴露了哪些方法最后核对注册时第三个参数。旧分支的 RegisterJsObject 对绑定对象名敏感名字里不要带点、横线前端调用时对象名和方法名分开检查。5.5 关闭窗体后进程残留Cef.Shutdown 与 Dispose 的顺序现象是主窗体关了任务管理器里 CefSharp.BrowserSubprocess.exe 还在再次启动程序时页面加载失败或直接白屏。原因是 ChromiumWebBrowser 没 Dispose或者 Cef.Shutdown 调用太早/太晚子进程没收到退出信号。解决方式是在 FormClosing 里先 browser.Dispose()把引用置空再 Cef.Shutdown()。如果窗体是多文档结构每个浏览器实例都要 DisposeCef.Shutdown 只调一次。调试时可以在 Shutdown 前后打日志确认顺序不要用 Application.Exit 直接杀进程。6. 进阶技巧用 BrowserInitializedChanged 和请求头把老控件跑稳老项目里最容易写坏的是“等页面加载”这段逻辑。很多人用 Thread.Sleep(3000) 然后 EvaluateScriptAsync机器慢的时候页面没好机器快的时候白等三秒。我一般用 BrowserInitializedChanged 事件确认浏览器底层初始化完成再用 FrameLoadEnd 判断主框架加载结束最后用 EvaluateScriptAsync 探 document.readyState。三层判断比固定睡眠稳也不会把 UI 线程卡住。下面这段只保留关键分支事件签名以你包内 API 为准。browser.BrowserInitializedChanged (sender, args) { if (!browser.IsBrowserInitialized) { return; } // 初始化完成后再加载业务页面避免在 about:blank 阶段抢跑 browser.Load(file:/// Application.StartupPath.Replace(\\, /) /index.html); }; browser.FrameLoadEnd (sender, args) { if (!args.Frame.IsMain) { return; } // 主框架加载完再探 readyState比固定 Sleep 可靠 browser.EvaluateScriptAsync(document.readyState).ContinueWith(task { if (task.IsFaulted || task.IsCanceled || !task.Result.Success) { return; } // 这里的返回值可以记日志也可以触发后续业务初始化 }, TaskScheduler.FromCurrentSynchronizationContext()); };另一个实用技巧是给所有请求加自定义头。老客户端经常需要后端识别“这是 WinForm 内嵌浏览器”但又不想改页面代码。在自定义 IRequestHandler 的 OnBeforeResourceLoad 里给 request.Headers 塞一个固定键值返回 false 表示继续请求。这样后端按头分流前端页面不用动。注意旧分支 IRequestHandler 接口成员较多除了关键方法其余成员按接口定义返回默认值不要只实现一个方法就编译。public class CustomRequestHandler : IRequestHandler { public bool OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { // 给每个请求打标后端据此识别客户端类型 request.Headers[X-Client] WinFormCef; return false; } // 其余接口成员按包内定义补全默认返回 false 或继续 }缓存目录也值得单独说一下。默认把 CachePath 放在 Application.StartupPath 下调试时没问题装到 Program Files 后普通用户没有写权限缓存写入失败不一定报错表现是页面加载慢、登录态丢、静态资源重复下载。我一般把 CachePath 和 UserDataPath 改到 LocalApplicationData 下按产品名建子目录启动时先 Directory.CreateDirectory再传给 CefSettings。这样升级安装包不会把缓存清掉也不会因为权限问题出现“有时好有时坏”的玄学。验证时加载一个带版本号的静态页刷新两次看 Network 里有没有 304或者看缓存目录有没有实际文件生成。从那以后我每次接老 WinForm 嵌浏览器的活都先做一个只有 about:blank 的最小 Demo确认 libcef.dll 架构、BrowserSubprocessPath、VC 运行时和 Cef.Shutdown 这四件事都过再往里面叠业务页面和 JS 互调。最小 Demo 跑不通时不要改业务代码先看 cef.log 和输出目录能省掉大量来回猜的时间。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →