CEF 89 Windows32编译包与Qt 5.14.2集成:从sln到产品化实战
发布时间:2026/9/25 4:12:28来源:尧图网络
简介面向VS2017与Qt5.14.2环境的CEF二进制89版Windows 32位编译包适合需要在Qt客户端中嵌入Chromium浏览器的C开发者。包内已通过CMake生成.sln解决方案可直接打开运行省去手动配置编译的繁琐步骤。资源共2275个文件包含大量obj、h、cc源文件与dll、lib、pak等运行及资源文件另有tlog等构建日志记录整体压缩包约916.69MB。目前已有620人学习下载。此包针对不会自行编译CEF的用户提供了现成的编译产物并附有清晰的工程结构可直接集成到Qt项目中用于实现带网页渲染能力的客户端程序。相比从源码编译可显著降低环境配置门槛适合快速启动浏览器类项目开发。1. 为什么我劝你先拿这个 cef 89 的 windows32 编译包省掉两个星期做客户端浏览器的同事应该都有过这种经历Qt 里想嵌一个真正的 Chromium 内核搜了一圈发现 CEF 编译包动辄几个 G自己拿 CMake 生成工程时又掉进“版本对不上、变量没配对、链接失败”的连环坑。这个cef_binary_89.0.18gb36241dchromium-89.0.4389.114_windows32包的好处在于它已经把 VS2017 Qt5.14.2 环境下需要用到的二进制、.sln工程和依赖都配好了打开就能编编完就能跑。它适合两种人一是只想在客户端里快速跑通 CEF 的 Qt 用户二是手里有旧业务必须锁在 32 位、Chromium 89 这条线的团队。下面我直接讲怎么用以及我实际踩过的那些坑。2. CEF/Chromium 89 与 Qt 集成先搞清这套包的边界2.1 CEF 不是浏览器是“能嵌进客户端的渲染内核”CEF 全称 Chromium Embedded Framework本质是把 Chromium 的渲染、JS 执行、网络栈拆出来作为组件嵌入到原生程序里。它和“一个独立安装的 Chrome”完全不同CEF 不提供完整浏览器界面只提供你需要的网页显示区域以及一套回调接口让你控制窗口、右键菜单、按键事件、下载行为、权限申请等等。所以拿到这个包你要把它当成“渲染引擎 浏览器主进程 子进程管理器”的组合体而不是一个浏览器安装包。在这个包里几个核心文件决定了它的运行方式libcef.dll是主渲染引擎libcef_dll_wrapper是链接层cefsimple是自带的极简示例程序还有很多v8_context_snapshot.bin和snapshot_blob.bin。这些.bin文件不是垃圾是 V8 引擎的启动快照用来加速 JS 上下文初始化。如果你把这些文件放到子目录或者改名运行时大概率会报Failed to load v8 context snapshot我第一次看到这个错误时还以为是编译问题后来才发现是文件路径被折腾坏了。2.2 与 Qt 的组合为什么选择 CEF 而不是 native 浏览器组件Qt 自带的QWebEngine本身也是 Chromium 内核的封装但版本跟随 Qt 发行周期而且对 32 位支持、个性化定制、子进程策略的控制都比较受限。CEF 的优势是内核和 UI 完全分离你可以把 CEF 渲染的窗口嵌入到任意 Qt 控件的winId上而 QWebEngine 更接近“一个控件”。实际做客户端项目时我更看重 CEF 的这几个能力可以单独控制子进程的启动参数和沙箱策略也就是热搜里常说的“CEF 用自己的子进程”可以定制资源加载逻辑拦截http/https以及自定义协议可以拿到更细的渲染事件比如OnBeforePopup、OnLoadError、OnCertificateError版本独立Chromium 内核升级时不用等 Qt 发版。不过代价也很明显CEF 的二进制体积大内存占用比 QWebEngine 高而且你必须自己管理浏览器进程的生命周期。对于客户端浏览器项目来说这些代价通常可以接受因为你要的是 Chromium 的兼容性和可控性。2.3 版本配套说明这个包的版本号是89.0.18gb36241dchromium-89.0.4389.114对应 Chromium 8932 位。标题里写的环境是 VS2017 Qt5.14.2这在 CEF 89 时代是相当典型的组合。要注意几个硬约束CEF 89 是支持 32 位 Windows 构建的较新版本之一后面版本逐渐淡化 32 位支持所以如果你必须做 x86 客户端这个版本是重要备选VS2017 的 C 工具集需要包含对 Windows SDK 10 的支持否则 CMake 生成的文件在编译时可能找不到windows.hQt 的构建位数必须和 CEF 一致都是 32 位。如果是 Qt 5.14.2 的 64 位库链接时会出现LNK1112这种模块机器类型冲突而且不太容易一眼看出来。我需要提醒的是这个包已经到了“CMake 生成 .sln”这一步意味着你拿到的不是 CMake 源码树而是工程文件。所以接下来章节我会从打开.sln开始讲而不是从 CMake 配置开始。3. 打开项目到跑起来CMake 生成的 sln 到底怎么用3.1 目录结构与关键文件识别解压后你能看到类似下面的结构我挑重点说cef_binary_89.0.18gb36241dchromium-89.0.4389.114_windows32/ ├── CMakeLists.txt ├── cefsimple/ │ ├── cefsimple.aps │ ├── cefsimple.cpp │ ├── cefsimple.h │ └── resource.h ├── cefclient/ ├── include/ │ ├── cef_app.h │ └── ... ├── lib/ │ └── Debug/ 和 Release/ 下的 .lib 文件 ├── Resources/ │ ├── v8_context_snapshot.bin │ ├── snapshot_blob.bin │ ├── icudtl.dat │ └── chrome_100_percent.pak ├── Release/ │ └── 编译输出的 dll 和 exe 所在目录 └── cefsimple.slncefsimple.aps是 VS2017 使用的资源文件备份不影响编译但说明这个包的源文件是从 CEF 官方示例中带过来的。cefsimple.cpp是入口程序里面包含CEFMain和简单的窗口创建逻辑。在编译前我建议先看一眼include/cef_version.h确认CEF_VERSION_MAJOR是不是 89以及CHROME_VERSION_BUILD是否匹配。这个动作不是为了走流程而是防止你手里这个包和另一个版本混在一起。3.2 用 VS2017 打开和编译 cefsimple 的完整步骤我一般按下面这些步骤做每一步都有明确目的# 1. 先检查环境变量 where cmake where devenv第一步先确认 CMake 和 VS2017 的devenv是否已经可用。CMake 版本不需要很高但 3.15 以下可能在解析CMakeLists.txt时出现变量找不到的情况虽然不常见但 32 位环境下值得留意。确认后用 VS2017 直接打开cefsimple.slndevenv.exe cefsimple.sln如果没有把 VS 的启动路径加入 Path也可以通过开始菜单里的 “Developer Command Prompt for VS 2017” 进入命令行再执行上面的命令。打开后把解决方案配置切到Release还是Debug我的建议是先用Release验证全链路能通然后再切换Debug排查问题。原因是 CEF 的Debug构建会输出大量日志运行速度慢并且调试启动时如果子进程超时反而容易误判成包坏了。切换到Release后直接生成解决方案devenv.com cefsimple.sln /Build Release|Win32Win32表示 32 位目标。如果你是 64 位系统VS 也会默认提供x64平台但这里 CEF 包是 windows32所以必须选Win32。如果生成时提示找不到stdafx.h或resource.h多半是项目文件中的相对路径被改动过建议保持解压目录完整不要自行把include或Resources挪走。编译完成后重点检查Release目录下是否同时生成了cefsimple.exe、libcef.dll、icudtl.dat、以及所有.pak文件。一个常见误区是只把 exe 拷走忽略了配套资源文件运行瞬间就白屏或闪退。3.3 一个最小可运行的 QtCEF 客户端骨架cefsimple本身是纯 Win32 程序还没接 Qt。要接到 Qt5.14.2 上我习惯不直接在cefsimple项目里改而是新建一个 Qt Widgets 项目把 CEF 的初始化逻辑放到 QApplication 启动之后。下面是一个能说明思路的最小骨架// main.cpp #include QApplication #include QWidget #include QTimer #include include/cef_app.h class CefQtWidget : public QWidget { public: explicit CefQtWidget(QWidget *parent nullptr) : QWidget(parent) { setWindowTitle(QStringLiteral(Qt CEF 89)); resize(1024, 768); } }; int main(int argc, char *argv[]) { QApplication app(argc, argv); // CEF 的初始化参数在 Qt 主窗口显示前配置 CefSettings settings; settings.multi_threaded_message_loop true; settings.no_sandbox true; // 32 位客户端常用配置沙箱会导致子进程问题 CefInitialize(argc, argv, settings, nullptr, nullptr); CefQtWidget w; w.show(); // 在窗口显示后创建 browser确保窗口句柄有效 QTimer::singleShot(0, []() { // 这里省略 CefBrowserHost::CreateBrowser 的具体窗口参数 // 实际要用 w.winId() 作为 parent_wnd }); int result app.exec(); CefShutdown(); return result; }这段代码的关键在settings.multi_threaded_message_loop true它能让你把 CEF 的消息循环交给 Qt 的app.exec()统一驱动避免两个事件循环互相抢占。如果你不设置这个参数就需要额外在 Qt 里启动一个线程调用CefDoMessageLoopWork而不是我这种更省事的写法。no_sandbox true是 32 位 Windows 客户端常见的处理方式。CEF 的沙箱在 Windows 32 位下需要额外的 manifest 和权限配置普通业务场景直接关掉反而省事。注意这个设置只对浏览器进程有效子进程是否带沙箱还取决于启动参数和主进程的调用方式。4. 关键配置与参数从窗口尺寸到子进程跑法4.1 浏览器窗口参数创建浏览器窗口时CefWindowInfo是最直接的参数入口。官方示例里这个结构体在不同平台上的字段差异很大Windows 32 位下我一般这样填CefWindowInfo window_info; #if defined(OS_WIN) window_info.SetAsChild(hwnd, RECT{0, 0, width, height}); #else window_info.SetAsChild(hwnd, CefRect{0, 0, width, height}); #endif CefBrowserSettings browser_settings; browser_settings.javascript_access_clipboard STATE_DISABLED; browser_settings.universal_access_from_file_urls STATE_DISABLED; CefBrowserHost::CreateBrowser(window_info, browser_client, url, browser_settings, nullptr);SetAsChild会把 CEF 的渲染区域嵌到你传入的父窗口句柄上。对于 Qt 来说这个句柄就是某个 QWidget 的winId()。很多刚接触 CEF 的人会在这里犯迷糊winId()必须是在窗口已经显示后才能拿到的如果你在构造函数里调用可能拿到的是 0。我一般放在showEvent里或者用QTimer::singleShot(0, ...)延后。browser_settings里的两个开关值得说明。现场如果做客户端浏览器默认不应该允许网页里的 JS 读剪贴板但现实中一些内部系统需要复制粘贴功能就需要把javascript_access_clipboard设成STATE_ENABLED。另一个universal_access_from_file_urls关系到你加载file:///页面时是否能跨域请求接口内部工具类项目可以打开面向公开业务要关掉。4.2 子进程与多进程模式CEF 用自己的子进程CEF 的进程模型和 Chrome 一样主进程负责窗口和 IO子进程负责渲染、网络、GPU 等。当你运行cefsimple.exe时任务管理器里会看到多个同名进程这就是“CEF 用自己的子进程”的直观表现。在 Windows 32 位下子进程的启动方式尤其值得注意因为位数不同会直接影响崩溃转储和调试。在 Qt 项目中你无法直接控制 CEF 子进程的入口因为子进程的 main 函数由 CEF 自己实现。正确的做法是在进程启动的最开始分流#include include/cef_app.h int main(int argc, char* argv[]) { // 如果这个进程是 CEF 子进程就交给 CEF 处理 CefMainArgs main_args(argc, argv); CefRefPtrCefApp app(new MyCefApp()); void* sandbox_info nullptr; if (CefInitialize(main_args, settings, app, sandbox_info) false) { return 1; } // 如果不是子进程继续 Qt 主流程 QApplication qtApp(argc, argv); ... }更严谨的做法是在CefExecuteProcess上判断。CEF 官方示例中的cefsimple.cpp是直接用CefExecuteProcess(main_args, app, sandbox_info)判断返回值如果返回了非负值说明该进程是子进程直接返回该值否则进入浏览器进程逻辑。我建议你保持官方这个循环不要自作聪明地把所有进程都拉到 Qt 的 main 里。子进程参数方面有两个高频参数--no-sandbox和--disable-gpu。32 位客户端在部分低配机器上GPU 进程容易崩溃表现就是一闪而过的黑窗。这时在CefSettings里加上settings.command_line_args_disabled false然后在创建浏览器前手动加--disable-gpu或者直接在启动参数里写--no-sandbox和--disable-gpu。这个做法不算优雅但很有效尤其对远程桌面和虚拟机场景。4.3 与 Qt 事件循环的协作CEF 的事件循环和 Qt 是两级关系。前面提到设置multi_threaded_message_loop true的话CEF 会在独立线程处理自身任务Qt 只管 UI 事件两者通过 Windows 消息WM_*和 CEF 的回调间接通信。这样做的好处是 UI 不卡坏处是某些 CEF 回调发生时你需要在 Qt 主线程中更新控件否则 Qt 会因跨线程操作崩溃。我常用的消息桥接思路是这样的class MyCefClient : public CefClient, public CefLoadHandler { public: void OnLoadEnd(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, int httpStatusCode) override { // CEF 回调线程不能直接操作 QWidget QMetaObject::invokeMethod(qApp, []() { // 在主线程里执行 UI 更新 statusBarLabel-setText(load finished); }, Qt::QueuedConnection); } CefRefPtrCefLoadHandler GetLoadHandler() override { return this; } private: IMPLEMENT_REFCOUNTING(MyCefClient); };这里的QMetaObject::invokeMethod配合Qt::QueuedConnection把跨线程调用转成 Qt 主线程事件。这是我在 QtCEF 项目里最常用的桥接手段比手动发 PostMessage 要少写很多代码也避免了你持有裸QWidget指针被delete后还去访问的崩溃风险。事件循环的另一个坑是退出顺序。关闭 Qt 主窗口时如果直接CefShutdown()而不先销毁所有 browser 对象CEF 会报 “The application is currently shutting down and cannot be restarted”。正确做法是让 CEF 收到关闭命令后等待浏览器进程销毁再退出 Qt 事件循环。5. 避坑与常见问题编译过了不等于跑起来5.1 现象编译通过双击 exe 白屏或者闪退原因一般是资源文件缺失或路径不对。icudtl.dat和v8_context_snapshot.bin必须和 exe 在同级目录CEF 启动时是按相对路径找的。如果你把 exe 单独复制到另一个文件夹资源文件没跟上就会出现白屏。解决检查Release目录下是否有这些文件icudtl.dat、v8_context_snapshot.bin、snapshot_blob.bin、所有.pak文件以及libcef.dll。另外要注意snapshot_blob.bin和v8_context_snapshot.bin在 32 位包中不能和 64 位包混用否则运行到 JS 初始化时直接崩。5.2 现象链接错误找不到libcef_dll_wrapper原因是你使用了错误的.lib文件。CEF 的libcef_dll_wrapper根据编译配置分成 debug 和 release 版本名字一样但实际文件内容不同。如果你在 Release 项目里链接了 Debug 的.lib或者反过来链接器不一定报名字错误而是报一堆未解析的外部符号。解决在项目属性的链接器输入里确认附加依赖项指向lib/Release/libcef_dll_wrapper.lib同时把“忽略特定默认库”里的libcmt配置好避免和 Qt 的运行时库冲突。还有一个细节libcef_dll_wrapper是静态库而libcef.lib是导入库两者都要加。5.3 现象中文乱码和路径带中文失败原因VS2017 项目默认使用本地字符集如果源代码文件不是 UTF-8 with BOM中文字符串在编译后会被解释成 GBK 或其他编码CEF 加载页面时请求的 URL 也会被错误编码。解决把源文件另存为 UTF-8 with BOM并在项目属性里把“字符集”设为“使用 Unicode 字符集”。跨平台时我统一在代码里写wstring或者u8...尽量避免裸char*中文。路径带中文的问题建议直接把整个工程放到纯英文路径下CEF 对含中文的 exe 路径支持不算友好尤其是子进程带参数时可能会出现启动失败。5.4 现象Debug / Release 混用导致崩溃原因CEF 和 Qt 都有自己的宏定义来区分 debug 和 release混用会破坏内存布局。比如 Qt 在 debug 下会定义QT_DEBUGCEF 可能会定义_DEBUG如果 CEF 的 lib 是 release 的而你的 Qt 程序是 debug 的调试信息对齐不上运行时在第一次回调时就会崩溃。解决整个解决方案全部用 Release 或全部用 Debug。不要图方便在 Debug 项目里链接 release 的libcef_dll_wrapper.lib即使编译通过运行也会在某个随机时间点崩。这类问题很难查我后来直接用 cmake 配置统一生成两个配置并在每次切换配置前彻底重新生成一次。5.5 现象下载慢playwright 也一样慢但这里已经有现成包很多同事用playwright install chromium下载浏览器内核在 CentOS 7 或者其他网络环境下一等就是半天最后还可能因为国内网络问题卡住。这个 CEF 包内置的已经是 89 版本内核不需要再走 playwright 那种在线下载方式因为libcef.dll本身就是 Chromium 89 的渲染内核。如果你只是缺一个可以嵌到客户端里的内核没必要再额外拉取一个完整浏览器。相比之下这个包是在本地编译好的省去的正好是网络等待和版本不对应的痛苦。6. 把 CEF 用出产品感接管右键菜单和外部协议6.1 生命周期接管如果你只停留在跑通cefsimple那还差得远。产品化的第一步是接管浏览器的生命周期。cefsimple示例会在澳客窗口关闭时直接退出但真实项目里你通常需要拦截关闭事件询问是否有未保存表单、下载任务是否在继续。我用CefLifeSpanHandler的子类来接管class MyLifeSpanHandler : public CefLifeSpanHandler { public: bool DoClose(CefRefPtrCefBrowser browser) override { // 返回 true 表示由自己处理关闭CEF 不会自动退出 QMetaObject::invokeMethod(qApp, []() { if (mainWindow) { mainWindow-close(); } }, Qt::QueuedConnection); return true; } void OnBeforeClose(CefRefPtrCefBrowser browser) override { // 在这里清理 Qt 侧的关联窗口 } private: IMPLEMENT_REFCOUNTING(MyLifeSpanHandler); };这样关闭事件就从浏览器进程传递到了 Qt 侧你可以决定是弹出确认框还是直接保存状态。注意DoClose返回 true 之后浏览器不会立即销毁避免回调里访问已释放对象。6.2 右键菜单CEF 默认的右键菜单是英文的“Back / Forward / Reload”那一套这在客户端里往往不合适。接管右键菜单需要继承CefContextMenuHandlerclass MyContextMenuHandler : public CefContextMenuHandler { public: void OnBeforeContextMenu(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefContextMenuParams params, CefRefPtrCefMenuModel model) override { model-Clear(); // 清掉默认菜单 model-AddItem(1001, L重新加载); model-AddItem(1002, L打开开发者工具); CefRefPtrCefMenuModel sub model-AddSubMenu(1003, L页面操作); sub-AddItem(1004, L打印); sub-AddItem(1005, L查看页面源码); } bool OnContextMenuCommand(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefContextMenuParams params, int command_id, CefEventFlags event_flags) override { if (command_id 1001) { browser-Reload(); return true; } else if (command_id 1002) { CefRefPtrCefBrowserHost host browser-GetHost(); host-ShowDevTools(CefWindowInfo(), CefBrowserSettings(), CefPoint(), CefPoint()); return true; } return false; } };model-Clear()是很容易忽略的一步。如果不清默认菜单项会一直存在而且可能和你自定义项混排在一起。菜单项 ID 建议用一个较大的常量避免和 CEF 内置 ID 冲突。开发调试时ShowDevTools很有用但在发布版里记得要屏蔽掉否则用户能打开控制台查看网络请求和JS状态。6.3 验证建议接手这样一个编译包我建议先跑通三个验证用Release配置编译原版cefsimple确认它能在目标 Windows 机器上启动并打开默认的http://www.google.com或任意内网页面。如果这一步失败先排除资源文件和 VC 运行库再查代码。写一个小 Qt 窗口调用CefInitialize和CreateBrowser再通过信号把页面标题回传到 Qt 状态栏。这个验证能确认事件循环和信号跨线程通信没有断。用dumpbin检查cefsimple.exe的导入表确认它只依赖libcef.dll而不是系统 IE 内核或其他不认识的 dll。最后提一个我自己养成的习惯每次拿到新的 CEF 包我会先打一个只包含Resources、Release、include的干净副本不改路径、不手工拷文件所有构建都在这个副本里完成。因为很多奇奇怪怪的“编译失败”和“运行闪退”其实都源于人为改动了目录结构。从那以后我每次接手 CEF 相关项目都强制走一遍这套流程打开.sln之前先核对位数和版本编译前先model-Clear() 运行时先看资源文件齐全没希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网