
简介面向使用VS2010与.NET 4.0的C# WinForm开发者CefSharp.WinForms 49.0.1示例包提供内嵌Chromium浏览器工程适合需要补充网页浏览、脚本交互与加载监控能力的初、中级开发者。压缩包共319个文件、约189.28MB以pak资源、dll动态库、xml与config配置为主并含cs源码、csproj项目、sln解决方案及nupkg依赖兼顾运行与二次开发。已有3433人学习下载。示例围绕Cef初始化、Chromium控件、导航与加载事件、执行JavaScript、CefSettings配置及Cef.Shutdown释放展开保留工程结构便于排查引用缺失、运行库放置和资源加载问题搭建WinForm浏览器原型。1. 一个不能升级 .NET 4.0 的老项目怎么把 Chromium 塞进 WinForm场景很具体一套跑了好几年的 C# WinForm 系统目标框架锁死在 .NET 4.0开发机上是 VS2010业务方要求界面里嵌一个现代浏览器因为原来那个 WebBrowser 控件用的是老 IE 内核前端随便用点新语法就白屏。这种条件下在 WinForm 里内嵌 Chromium 最直接的方案就是 CefSharp控件拖上去、Dock 设成 Fill用法和普通 WinForm 控件差别不大。真正麻烦的是版本VS2010 的编译器只到 C# 4.0运行时只到 .NET 4.0能选的 CefSharp 版本被夹在一个很窄的区间里。装错版本、平台目标留成 AnyCPU、输出目录少一个 dll最后的表现都是同一副面孔——黑屏、白屏或者启动直接抛异常。提示后面所有代码都按 C# 4.0 语法写没有async/await没有字符串插值没有?.。你在网上抄到的示例大概率是 C# 5 以上的写法粘进 VS2010 直接编译不过。2. 版本对表CefSharp 与 .NET 4.0、VS2010 的可用组合2.1 判断依据是 lib 目录不是博客里的版本号CefSharp 的版本号跟着 Chromium 主版本走43.x、45.x、49.x这些数字背后其实是三件不同的事Chromium 内核版本、CefSharp 托管层 API、以及最低 .NET 框架要求。这三者里只有最后一个决定你能不能用。.NET 4.0 这条线大致落在 43 及更早的分支上45 之后最低目标基本抬到了 .NET 4.5再往后还有 4.5.2 的门槛。不要记版本号记方法。把 NuGet 包拉下来之后看包里的目录结构packages\CefSharp.Common.43.0.1\lib\ net40\ - 有这个目录.NET 4.0 才有戏 CefSharp.dll CefSharp.Core.dll net45\ - 只有这个说明你得升框架lib下没有net40就换下一个更早的主版本继续试。这个方法比任何一篇博客都可靠因为包的目录结构是编译器认的硬事实。我一般会在动手写代码之前先把这一步确认掉否则后面调半天初始化回头发现是框架版本不对纯属浪费时间。还有一个容易被忽略的点VS2010 新建 WinForm 项目时默认的目标框架经常是.NET Framework 4 Client Profile不是完整的.NET Framework 4。Client Profile 裁掉了不少程序集CefSharp 依赖的东西不全编译期可能就报缺引用或者运行期抛一些莫名其妙的类型加载失败。项目属性里把目标框架明确改成完整版这一步别省。2.2 用 nuget.exe 在 VS2010 里拉包VS2010 自带的扩展管理器对 NuGet 的支持很有限插件版本一高就装不上。稳妥的办法是绕开 IDE直接用命令行把包装进packages目录再手动往 csproj 里加引用。:: 在解决方案同级目录执行输出到 packages 文件夹 nuget.exe install CefSharp.WinForms -Version 43.0.1 -OutputDirectory packages nuget.exe install CefSharp.Common -Version 43.0.1 -OutputDirectory packages-Version里的数字换成你在上一步确认过、确实带net40目录的那个 patch 版本-OutputDirectory建议就用packages和 VS 自带的包管理习惯保持一致后面写 HintPath 的时候路径好推算。CefSharp.WinForms会带一个对CefSharp.Common的依赖两个都显式写出来是为了让版本严格对齐——一个 43.0.1 配一个 43.0.0运行时会出现程序集版本冲突。装完之后建一个packages.config放在项目目录下内容按包的实际版本填?xml version1.0 encodingutf-8? packages package idCefSharp.Common version43.0.1 targetFrameworknet40 / package idCefSharp.WinForms version43.0.1 targetFrameworknet40 / /packages然后手工编辑 csproj把三个托管程序集引进来ItemGroup Reference IncludeCefSharp HintPath..\packages\CefSharp.Common.43.0.1\lib\net40\CefSharp.dll/HintPath /Reference Reference IncludeCefSharp.Core HintPath..\packages\CefSharp.Common.43.0.1\lib\net40\CefSharp.Core.dll/HintPath /Reference Reference IncludeCefSharp.WinForms HintPath..\packages\CefSharp.WinForms.43.0.1\lib\net40\CefSharp.WinForms.dll/HintPath /Reference /ItemGroupHintPath是相对 csproj 文件的路径多一层少一层目录都会导致引用变成黄色感叹号。CefSharp.Core.dll是个混合程序集托管 本地它运行时还要去加载libcef.dll所以它不能只躺在 packages 目录里必须随生成一起进输出目录——这件事由包的 targets 文件负责前提是你的项目属性配置正确。2.3 平台目标AnyCPU 是这份示例代码最容易翻车的地方VS2010 新建项目的默认配置是AnyCPU32 位系统上跑 32 位进程64 位系统上跑 64 位进程。而 CefSharp 的 natives 是按平台分开打包的一个进程里只能加载一种位数的libcef.dll。两边对不上启动时就是那句经典的未能加载文件或程序集“CefSharp.Core.dll”或它的某一个依赖项。 试图加载格式不正确的程序。 (异常来自 HRESULT:0x8007000B)解决办法是把平台目标钉死。在项目属性 → 生成 → 目标平台里选x86或者直接改 csprojPropertyGroup Condition $(Configuration)|$(Platform) Debug|x86 DebugSymbolstrue/DebugSymbols OutputPathbin\x86\Debug\/OutputPath PlatformTargetx86/PlatformTarget /PropertyGroup PropertyGroup Condition $(Configuration)|$(Platform) Release|x86 OutputPathbin\x86\Release\/OutputPath PlatformTargetx86/PlatformTarget Optimizetrue/Optimize /PropertyGroupPlatformTarget决定编译器生成 32 位还是 64 位的宿主进程OutputPath顺手分开避免 x86 和 x64 的 natives 混在同一个bin\Debug里互相覆盖。如果你的目标机全是 64 位系统也可以反过来选x64但要注意解决方案配置管理器里那个平台下拉框必须和项目属性里的PlatformTarget一致我见过只改了其中一个、另一个还是 AnyCPU 的情况结果是编译出来的 exe 依然是 AnyCPU异常照报不误。选 x86 的另一个理由跟老项目有关很多 VB6 组件、老式 COM 控件、32 位数据库驱动只提供 32 位版本。这种项目改成 x64 会牵连出一堆别的问题不如整个进程统一在 32 位下跑CefSharp 也一起用 x86 的 natives省事。3. 跑起第一个窗口Cef.Initialize 与 ChromiumWebBrowser 的装配顺序3.1 CefSettings 里真正影响成败的几个参数CefSettings字段很多但在 .NET 4.0 WinForm 这个组合里真正会咬人的就那么几个。参数建议值作用与不设的后果MultiThreadedMessageLooptrueCEF 自带消息循环否则需要自己在 WinForm 的消息泵里调Cef.DoMessageLoopWork()漏调就卡住CachePath%LOCALAPPDATA%\应用名\Cache缓存、Cookie、LocalStorage 的落脚点不设会落到系统临时目录重装即丢LogFile%LOCALAPPDATA%\应用名\cef.log出问题时唯一的黑匣子排查全靠它LogSeverityInfo排障期/Disable上线后日志级别排障时开低了等于没开AcceptLanguageListzh-CN影响navigator.language和部分站点返回的语言版本Localezh-CN影响 CEF 自带界面右键菜单、错误页的语言BrowserSubprocessPath一般不设默认会在 exe 同级找CefSharp.BrowserSubprocess.exe改目录结构时才需要指定CefCommandLineArgs是个字符串字典用来给 Chromium 追加命令行开关。在虚拟机、远程桌面、老集成显卡这几类环境里GPU 合成经常出问题表现是窗口一片黑但页面其实已经加载完了。加这两个开关是最省事的绕过方式settings.CefCommandLineArgs.Add(disable-gpu, 1); settings.CefCommandLineArgs.Add(disable-gpu-compositing, 1);代价是页面渲染走软件路径复杂动画会掉帧。所以我的习惯是先不加跑起来看是不是黑屏确认是 GPU 的锅再加回去而不是一上来就关掉把性能问题留给以后。3.2 窗体代码从 Program.Main 到 DockFill初始化顺序是硬性的Cef.Initialize必须在第一个ChromiumWebBrowser实例创建之前调用而且只能调一次。using System; using System.IO; using System.Windows.Forms; using CefSharp; namespace CefSharpDemo { internal static class Program { [STAThread] private static void Main() { string root Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), CefSharpDemo); Directory.CreateDirectory(root); var settings new CefSettings(); settings.CachePath Path.Combine(root, Cache); settings.LogFile Path.Combine(root, cef.log); settings.LogSeverity LogSeverity.Info; settings.AcceptLanguageList zh-CN; settings.Locale zh-CN; settings.PersistSessionCookies true; settings.MultiThreadedMessageLoop true; // WinForm 下必须显式打开 if (!Cef.Initialize(settings)) { MessageBox.Show(CEF 初始化失败检查 settings.LogFile); return; } Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new MainForm()); Cef.Shutdown(); // 只调一次放在 Application.Run 之后 } } }Cef.Initialize返回 false 通常意味着 natives 加载失败或者参数冲突真正的错误信息在cef.log里。PersistSessionCookies打开之后登录态能跨进程保留对一个内嵌业务系统的客户端来说是刚需不打开的话每次重启都要重新登录。窗体这边关键在 Dock 顺序和控件添加顺序。using System; using System.Windows.Forms; using CefSharp; using CefSharp.WinForms; namespace CefSharpDemo { public partial class MainForm : Form { private ChromiumWebBrowser _browser; private TextBox _address; private Button _go; public MainForm() { InitializeComponent(); Text CefSharp Demo; Width 1024; Height 700; BuildLayout(); } private void BuildLayout() { _address new TextBox { Dock DockStyle.Top, Height 24 }; _go new Button { Dock DockStyle.Top, Height 26, Text 打开 }; _browser new ChromiumWebBrowser(about:blank); _browser.Dock DockStyle.Fill; // 添加顺序决定 Dock 的填充顺序先加 Fill再加 Top Controls.Add(_browser); Controls.Add(_go); Controls.Add(_address); _go.Click delegate { _browser.Load(_address.Text); }; _browser.LoadingStateChanged Browser_LoadingStateChanged; _browser.ConsoleMessage Browser_ConsoleMessage; } private void Browser_LoadingStateChanged(object sender, LoadingStateChangedEventArgs e) { if (InvokeRequired) { BeginInvoke(new Actionobject, LoadingStateChangedEventArgs( Browser_LoadingStateChanged), sender, e); return; } _go.Enabled !e.IsLoading; if (!e.IsLoading) _address.Text _browser.Address; } private void Browser_ConsoleMessage(object sender, ConsoleMessageEventArgs e) { System.Diagnostics.Debug.WriteLine([JS] e.Message e.Source : e.Line); } } }Controls.Add的顺序直接影响布局WinForm 的 Dock 是后加的先占位如果先加 Top 的控件再加 Fill 的浏览器浏览器会把顶部那一块盖掉表现为地址栏看不见。把浏览器第一个加进去、或者加完之后调SetChildIndex(_browser, 0)两种写法都行。LoadingStateChanged和ConsoleMessage这两个事件都是从 CEF 的线程抛上来的不是 UI 线程。第一行InvokeRequired的判断不能省否则偶发的跨线程访问异常会在你不注意的时候崩掉整个进程。这类异常最难查因为它跟操作时序有关开发机上跑一天都不出现客户那边一点就崩。3.3 退出时的 Cef.Shutdown 与顺序问题Cef.Shutdown()调用时机不对后果是进程退不干净任务管理器里留着CefSharp.BrowserSubprocess.exe或者主窗口关了但进程常驻。规则有两条一是放在Application.Run返回之后二是整个进程只调一次。有一个常见的错误写法是在MainForm.FormClosing里调Cef.Shutdown()。这时候浏览器控件还没 DisposeCEF 的渲染进程还在跑Shutdown 会被阻塞或者直接抛异常。如果真的需要在窗体关闭时做清理比如保存 Cookie正确做法是在窗体的FormClosed里先_browser.Dispose()把 Shutdown 留给 Main。还有Environment.Exit式的强杀这个会绕过 CEF 的所有清理逻辑下次启动时缓存目录可能是半损坏状态表现为页面加载异常缓慢或者某些静态资源一直 404。只要用正常的关窗流程这些问题都不会出现。4. 双向通信C# 调 JS 与 JS 调 C# 的两条通道4.1 C# 4.0 里没有 await脚本调用怎么写CefSharp 的脚本执行 API 返回的是Task网上示例清一色await browser.EvaluateScriptAsync(...)。VS2010 编译不了这个语法得换成ContinueWith。两种情况分开处理不关心返回值用ExecuteScriptAsync需要拿结果用EvaluateScriptAsync。// 不关心返回值执行完就完了 private void CallJsNoResult(string method, string arg) { string literal ToJsString(arg); _browser.ExecuteScriptAsync(window. method ( literal );); } // 需要返回值用 ContinueWith 接住结果不要 .Result 阻塞 UI 线程 private void CallJsWithResult(string expression, Actionstring onDone) { _browser.EvaluateScriptAsync(expression).ContinueWith(t { if (!t.IsFaulted t.Result ! null t.Result.Success) onDone(t.Result.Result null ? string.Empty : t.Result.Result.ToString()); else onDone(string.Empty); }, TaskScheduler.FromCurrentSynchronizationContext()); } // 把 C# 字符串安全地拼进 JS 字面量这一步千万别省 private static string ToJsString(string s) { if (s null) return null; return \ s.Replace(\\, \\\\).Replace(\, \\\) .Replace(\r, \\r).Replace(\n, \\n) \; }TaskScheduler.FromCurrentSynchronizationContext()是这段代码里最关键的一个参数。没有它ContinueWith里的回调跑在线程池线程上你在回调里更新控件就会触发跨线程异常。加上之后回调会自动回到 UI 线程的同步上下文写法和同步代码一样自然。ToJsString那个函数看着土但必须写。业务数据里带个引号、反斜杠、换行直接拼进脚本就是语法错误而且报错信息只会在ConsoleMessage里冒一行红字很容易被忽略。血泪经验是所有拼进ExecuteScriptAsync的外部字符串一律先过这个函数。4.2 RegisterJsObject 的注册时机与签名约束反向通道是 JS 调 C#靠RegisterJsObject把一个托管对象挂到window上。它有两个硬约束注册对象的方法签名必须是简单类型进、可序列化类型出注册动作必须在页面开始加载之前完成。public class JsBridge { private readonly MainForm _owner; public JsBridge(MainForm owner) { _owner owner; } // 无参返回字符串 public string GetVersion() { return 1.0.0; } // 单参返回 JSON 字符串由 JS 自己 parse public string QueryOrder(string orderNo) { if (_owner.InvokeRequired) { // 桥方法在 CEF 的线程被调用必须切回 UI 线程 return (string)_owner.Invoke(new Funcstring, string(QueryOrder), orderNo); } return _owner.BuildOrderJson(orderNo); } // 通知类用 BeginInvoke 避免 UI 线程等 CEF 造成死锁 public string Notify(string message) { if (_owner.InvokeRequired) { _owner.BeginInvoke(new Actionstring(m _owner.ShowTip(m)), message); return {}; } _owner.ShowTip(message); return {}; } }注册放在浏览器控件构造之后、Load之前_browser new ChromiumWebBrowser(about:blank); // 必须在 Load 之前注册页面里的脚本才拿得到 window.bridge _browser.RegisterJsObject(bridge, new JsBridge(this)); _browser.Load(http://127.0.0.1:8080/index.html);时机错了的表现是 JS 里bridge is not defined但页面本身一切正常非常容易被误判成前端问题。我一开始就是把注册写在了FrameLoadEnd事件里调试了半小时才发现是顺序问题。返回值我统一用 JSON 字符串而不是直接返回复杂对象。老版本 CefSharp 对复杂对象的自动序列化有边界遇到DateTime、泛型集合、循环引用的时候行为不好预测。统一在 C# 端用 JSON 序列化好JS 端JSON.parse一下两边都清爽。JS 侧的调用长这样// 同步拿字符串简单直接 var version bridge.GetVersion(); // 返回 JSON 的自己解析 var order JSON.parse(bridge.QueryOrder(SO20240115001)); console.log(order.status);4.3 跨线程回 UI别在 CEF 回调里直接摸控件除了桥对象所有 CEF 事件回调都在非 UI 线程。前面LoadingStateChanged里那段InvokeRequired判断应该是所有事件处理函数的肌肉记忆。判断做漏了短时间测试不出来因为跨线程访问控件在 .NET 2.0 之后默认不抛异常只是行为未定义——可能是界面不刷新可能是布局错乱也可能在某个低概率路径上崩掉。Invoke和BeginInvoke的选择也有讲究Invoke是同步等待 UI 线程执行完再继续如果 UI 线程此刻正在等 CEF 的某个结果双方互等就死锁了。桥对象的查询类方法要拿返回值给 JS用Invoke通知类方法发个消息给界面显示用BeginInvoke并直接返回一个空对象。这个区分在功能简单的 Demo 里看不出差别一旦业务上有大文件加载、模态对话框差别就是卡死和不卡死。还有一个坑桥对象的方法不要做耗时操作。JS 调用是同步阻塞的在 JS 侧看起来像同步函数你在里面查个数据库、等个接口页面直接僵住用户会以为程序死了。耗时逻辑要么改成事件回调要么在 C# 端异步跑完之后通过ExecuteScriptAsync回推给页面。5. 部署排查从 BadImageFormatException 到白屏的五个现场5.1 输出目录到底要拷哪些文件CefSharp 的 NuGet 包带 targets 文件生成时会自动把 natives 拷到输出目录。前提是项目配置正确平台目标不是 AnyCPU。如果你像我一样是手工加引用、没用包管理器的完整流程那就得知道输出目录里到底该有什么。文件/目录缺失后的表现libcef.dll报“找不到指定的模块”进程直接起不来CefSharp.Core.dll报找不到程序集或 BadImageFormatExceptionCefSharp.BrowserSubprocess.exe主窗口能开页面一片空白日志里提示子进程启动失败cef.pak/cef_100_percent.pak/cef_200_percent.pak页面能渲染但样式错乱图标全丢devtools_resources.pak不影响显示F12 开发者工具打不开icudtl.dat中文乱码、排序异常locales\目录至少含zh-CN.pak右键菜单英文部分页面日期格式不对natives_blob.bin/snapshot_blob.bin启动即崩日志里是 V8 初始化失败最后两个 blob 文件名在不同主版本之间会变新版本合并成了v8_context_snapshot.bin所以升级版本之后不要只替换 dll整个输出目录重新生成一遍最保险。还有一个细节locales目录里的 pak 文件是按需加载的settings.Locale设成zh-CN就必须有对应的zh-CN.pak。只拷en-US.pak而 Locale 设成中文CEF 会回退到默认语言然后在日志里写一条 warning不影响主流程但很难看。5.2 五条踩坑记录现象、原因、解决第一条启动即抛 BadImageFormatException提示格式不正确的程序。现象是双击 exe 立刻弹异常框堆栈指向CefSharp.Core.dll的静态构造。原因是进程位数和 natives 位数对不上多数情况是项目平台目标留成了 AnyCPU在 64 位系统上跑出了 64 位进程而拷进去的是 32 位的libcef.dll。解决项目属性里把平台目标改成x86解决方案配置管理器的平台也改成x86清空bin和obj重新生成最后用任务管理器确认进程没有“(32 位)”以外的标记。第二条FileNotFoundException提示找不到指定的模块。注意是“找不到指定的模块”不是“找不到文件”。这句话的真实含义是libcef.dll找到了但它自己依赖的某个系统 dll 没找到。十有八九是目标机器没装对应的 Visual C 运行库。CefSharp 各版本依赖的 VC 运行库版本不同43 这条线对应的是 2013 版。解决把运行库安装包一并放进部署包装完再启动确认手段是用 Process Monitor 过滤libcef.dll的Load Image事件看它在哪个路径上返回了NAME NOT FOUND。第三条窗口出来了页面一片白什么都不显示。先看cef.log如果日志停在 “Requesting navigation” 之后就没有下文通常是资源加载被拦了。常见的三个来源杀毒软件的网页防护把本地 HTTP 请求掐了、页面本身依赖的接口超时、GPU 合成在特定显卡上卡死。排查顺序是先Load(about:blank)确认 CEF 本身能初始化再换成本地一个纯文本 HTML 文件确认渲染链路通最后才怀疑网络和 GPU。确认是 GPU 问题就加disable-gpu开关这个开关救过我不止一次。第四条中文全是乱码或者方块。直接检查两处settings.Locale是否设成了zh-CN以及输出目录的locales下有没有zh-CN.pak。只设 Locale 不拷 pak 文件是最常见的组合错误。另外AcceptLanguageList影响的是页面请求头里的Accept-Language和后端返回什么语言的内容有关跟界面乱码不是一回事两个都要设。第五条JS 里调用桥对象报 undefined。两种可能注册时机晚于页面加载或者桥对象的方法签名里有 CefSharp 不支持的类型。前者的判断方法是打开 F12 控制台敲window.bridge如果是undefined就往注册时机上查如果对象存在但方法报错就看方法参数里有没有DateTime、自定义类、泛型嵌套这些改成字符串传递再在 C# 端解析。5.3 打开 CEF 日志定位问题cef.log是这套东西唯一的黑匣子。日志级别设成Info之后能看到页面请求、进程启动、GPU 初始化这几条主线。几个值得记住的日志特征出现Failed to create a Chromium subprocess—— 子进程 exe 缺失或被安全软件拦截出现GPU process launch failed—— 显卡驱动或者远程桌面环境导致加 disable-gpu出现Unable to load locale—— 缺 pak 文件日志文件根本不存在 ——Cef.Initialize就没成功走到写日志那一步问题在更前面日志路径我固定在%LOCALAPPDATA%下而不是 exe 同级目录原因是很多客户把程序装在Program Files里普通用户权限写不进去日志静默失败排查时还以为程序没报错。6. 进阶本地页面加载、缓存目录与一个稳定收尾的写法内嵌浏览器的业务系统页面资源基本都是随程序分发的本地文件。直接用file:///加载会遇到跨域限制XHR 请求本地 JSON 会被拦localStorage的域也变得不可控。我一般起一个轻量的本地 HTTP 服务把页面挂在一个固定的虚拟域名下再配合请求拦截把域名映射到安装目录。public class LocalResourceHandler : RequestHandler { private readonly string _root; public LocalResourceHandler(string root) { _root root; } protected override bool OnBeforeBrowse(IWebBrowser browser, IRequest request, bool isRedirect, bool isMainFrame) { // 只放行虚拟域名其余交给默认处理 return !request.Url.StartsWith(http://app.local/); } public override bool ProcessRequest(IWebBrowser browser, IRequest request, out string redirectUrl, out bool isHandled) { redirectUrl null; isHandled false; if (!request.Url.StartsWith(http://app.local/)) return false; // 剥掉虚拟域名映射到安装目录下的 web 文件夹 string relative request.Url.Substring(http://app.local/.Length); string full Path.Combine(_root, relative.Replace(/, \\)); if (!File.Exists(full)) return false; // 这里只演示思路实际用自定义 SchemeHandler 返回文件流 isHandled true; return true; } }接口签名在不同主版本之间有出入ProcessRequest在较新的版本里已经被IResourceHandler体系取代。老版本上的稳妥做法是别自己造轮子直接起一个本地 HTTP 监听把http://127.0.0.1:端口/作为起始地址页面资源、接口代理一并走这个端口。这样做的好处是调试时可以用外部浏览器打开同一个地址对照出问题时能立刻分清是 CEF 的锅还是页面自己的锅。缓存目录这块我给每个主版本单独一个子目录比如Cache\43、Cache\49。不同 Chromium 版本的缓存格式不兼容共用一个目录会在升级后出现各种诡异的加载失败;分目录之后升级时旧目录留着不动新版本自己建新的,回滚也方便。最后说个习惯。这一类内嵌浏览器的项目我每次改完配置都会强制走一遍固定流程清bin和obj、重新生成、在没有装过任何运行库的干净虚拟机上跑一次、看cef.log有没有 warning、关窗确认进程完全退出。这五步加起来不到十分钟但能挡掉我见过的绝大多数“开发机上好好的、客户那边起不来”。最开始我省掉这一步结果一份部署包在客户现场卡了三天最后发现只是少装了一个运行库。这份示例代码的完整结构就是上面这几块Program 里的初始化和清理、窗体里的控件装配和事件、桥对象的双向通信、以及部署时该拷的文件清单。把它当成一个骨架把你自己的业务页面和接口填进去剩下的就是调参数和看日志的耐心活。希望帮到你。本文还有配套的精品资源点击获取