资讯动态

CEF入门到实战:桌面应用嵌入Chromium浏览器的完整指南

发布时间:2026/10/18 14:43:02 来源:尧图企业网站定制
简介面向微软MFC/C程序员的谷歌CEF浏览器控件集成实例解决Windows桌面应用嵌入Chromium内核、呈现现代网页与交互功能的需求。示例以完整工程为主线覆盖CEF初始化、窗口尺寸变化、浏览器实例创建、网页加载回调及多线程调度等关键环节并演示生命周期处理器、加载处理器、请求处理器等接口的接入方式能有效避开线程上下文和资源释放的常见问题。压缩包共450个文件约166.98MB以250个头文件、116个pak资源文件、14个动态库为主体配套C源码、导入库、可执行程序与解决方案工程。头文件与动态库用于搭建CEF编译和运行环境pak资源文件用于网页本地化及内置资源源码与工程可直接重新编译学习。目前已有1210人学习包内附带完整的浏览器运行时资源与调试生成文件工程中的回调接口实现可直接替换适合需要快速上手、按自身业务定制桌面端浏览器的开发人员。1. CEF浏览器控件桌面软件里嵌浏览器的首选之一CEFChromium Embedded Framework即Chromium嵌入式框架是桌面软件中嵌入浏览器控件时绕不开的一套开源库。它的价值在于把整个Chromium内核放进普通窗体让HTML/CSS/JS与C直接对话常见于客户端网页壳、混合开发、数据可视化面板。适合两类人一是给既有Win32/MFC程序补网页界面二是需要精确控制浏览器版本的桌面团队。它比WebView2更可控比Electron更轻但前提是你能接受首次编译的复杂度。看完这篇你应该能跑通第一个CEF窗口并知道与进程相关的坑在哪。2. 方案对比与资源账WebView2、Electron和CEF怎么选2.1 三种嵌入方案的实际边界很多开发者一开始纠结的是用WebView2、Electron还是CEF。我接手的某跨平台桌面工具里因为加载远程Web页面又要求C调用JS三种方案都做过预研结论很明确没有绝对优劣只有适不适合当前工程。方案内核打包体积C接入成本典型场景WebView2系统提供的Edge内核小但运行时依赖系统安装低简单管理界面、登录页Electron自带ChromiumNode大约200MB起步高需引入Node侧全新桌面应用、前后端同构CEF自带Chromium中等约100MB中纯C接口既有C程序嵌网页、需要精确控制内核版本WebView2最大的不确定性在于客户机器上有没有装对应运行时。即使微软允许离线打包版本更新策略仍取决于系统更新内存占用也受系统Edge的回收策略影响。Electron的优势是生态成熟劣势是它本质上是整套Node运行时加浏览器的叠加对传统MFC/Win32项目来说侵入感太强要把业务逻辑从C迁到JavaScript也不现实。CEF则直接以库形式存在你还是写C只不过多了一个浏览器窗口这正好满足“只用浏览器做界面业务还在C”的需求。2.2 CEF的多进程模型与资源开销CEF沿用Chromium的多进程架构启动后至少会出现浏览器主进程、GPU进程、网络进程和若干渲染进程。这里有一个容易误判的地方很多人以为CEF只多一个进程实际打开任务管理器会发现至少三到五个进程页面越多渲染进程越多。每个渲染进程的内存基线在80MB到200MB之间GPU进程在复杂页面上也会占到50MB以上。所以在选型阶段就要掂量清楚目标用户机器如果只有4GB内存CEF不是好选择如果是一个运行在工控机或开发机上的内部工具CEF很划算。多进程也带来一个排错思路——白屏时先看进程列表渲染进程崩溃了主窗口还在这在第五章节里细说。调试时还可以通过命令行参数--remote-debugging-port开调试端口用Chromium的DevTools查看渲染进程内部状态这是排查前端问题的核心手段。2.3 版本选型与二进制获取CEF的官方构建版本是二进制分发包Automated Build同时也有源码仓库。官方分发包会标明对应Chromium版本、CEF版本号和平台。我一般直接下载分发包不自己编译源码因为一次完整编译需要拉取十几GB的depot_tools仓库耗时数小时甚至十几个小时中途网络或磁盘一出问题就要重来非必要不值得。下载时注意三点第一选择stable分支而不是beta或者canary稳定分支的API变更少第二确认平台是Windows 64位还是32位这和你的目标应用一致第三解压后要保管好版本号后续升级时头文件和libcef.dll必须完全匹配混用高版本头文件和低版本dll会出现莫名其妙的崩溃这在CEF里属于“血泪经验”。3. 从下载二进制到跑通首屏最小CEF工程与CMake生成VS项目3.1 二进制分发包的目录结构与必需文件解压CEF分发包后核心文件分布在几个目录下include/放C头文件libcef_dll_wrapper/是官方封装层源码Debug/和Release/分别存放对应配置的libcef.dll和导入库Resources/里是icudtl.dat、v8_context_snapshot.bin、*.pak等资源文件。工程运行时这些文件必须完整放在exe同级目录或指定路径下特别是缺少icudtl.dat时程序会在启动阶段直接闪退没有任何友好提示。把libcef_dll_wrapper源码加入工程这一步很多新手会漏。它的作用是封装CEF的C API成C接口如果不链接它代码里CefRefPtr这类类模板都用不了。官方示例工程里用CMake添加这个子目录我推荐沿用官方方式而不是手动把几十个cpp文件拖进VS工程否则后面升级CEF版本时维护成本会非常大。3.2 用CMake生成Visual Studio工程官方分发包自带cmake/目录包含构建所需的CMake脚本。我习惯新建一个独立目录存放自己的CMakeLists.txt与CEF解压目录分开这样能随时切换CEF版本。以下是一个最小CMake配置cmake_minimum_required(VERSION 3.16) project(cef_demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 改成你自己的CEF解压路径 set(CEF_ROOT D:/cef/cef_binary_130.0.1) # 引入CEF的CMake定义会生成libcef_dll_wrapper等目标 add_subdirectory(${CEF_ROOT}/cmake cef) add_executable(cef_demo WIN32 main.cpp) # 链接CEF封装层和基础库 target_link_libraries(cef_demo libcef_dll_wrapper)用CMake生成VS工程时在构建目录执行命令cmake -G Visual Studio 17 2022 -A x64 .. cmake --build . --config Release-A x64指定64位架构这里和下载的CEF分发包位数必须一致。--config Release决定最终链接Release模式的libcef.dll调试时可以用--config Debug但Debug版本体积大且堆栈信息多运行速度明显慢。我一般只保留Release构建前端问题通过DevTools定位C问题用日志定位这样发布和调试接近一致。3.3 初始化CEF并显示页面最小C代码骨架CEF的初始化流程有固定顺序先创建CefMainArgs再设置CefSettings然后调用CefInitialize最后进入消息循环。下面这段代码是Windows窗口程序的最小骨架#include include/cef_browser.h #include include/cef_command_line.h #include include/cef_sandbox_win.h class SimpleApp : public CefApp, public CefBrowserProcessHandler { public: void OnContextInitialized() override { CefWindowInfo window_info; // 将浏览器窗口作为子窗口嵌入宿主窗口 RECT rect {0, 0, 1024, 768}; window_info.SetAsChild(GetActiveWindow(), rect); CefBrowserSettings browser_settings; std::string url file:///D:/cef_demo/index.html; // 创建浏览器实例 CefBrowserHost::CreateBrowser( window_info, nullptr, url, browser_settings, nullptr, nullptr); } IMPLEMENT_REFCOUNTING(SimpleApp); }; int main(int argc, char* argv[]) { CefMainArgs main_args; CefSettings settings; settings.no_sandbox true; // 先关掉沙箱避免权限问题 settings.multi_threaded_message_loop false; CefRefPtrSimpleApp app(new SimpleApp()); CefInitialize(main_args, settings, app.get(), nullptr); // 运行CEF消息循环 CefRunMessageLoop(); CefShutdown(); return 0; }这段代码里的OnContextInitialized是浏览进程上下文初始化完成的回调CEF官方推荐在此时创建浏览器窗口。window_info.SetAsChild把页面嵌入你的窗口句柄这里为了演示直接用了GetActiveWindow()实际工程里应换成你主窗口的HWND。CefRunMessageLoop会阻塞当前线程直到调用CefQuitMessageLoop这是一个需要特别注意的约束——你的程序不能再用自己的消息循环否则键盘和鼠标事件会有延迟。3.4 消息循环与线程模型的硬约束CEF对线程模型有非常强制的约束CefInitialize必须在主线程调用浏览器相关操作必须在IO线程TID_IO或UI线程TID_UI执行渲染进程的事只能在渲染线程做。最常见的问题是开发者把CEF初始化放在自己封装的线程类里或者在OnContextInitialized里执行阻塞式等待这会导致白屏或直接不创建窗口。设置里有一个multi_threaded_message_loop选项官方默认是false。如果设成trueCEF会自己起一个线程跑消息循环你的主线程可以继续做别的但对既有MFC程序来说这会让界面句柄的时序变得不好控制。我建议保持false直接用CefRunMessageLoop简单可靠。退出时注意CefShutdown必须在CefQuitMessageLoop返回后再调用并且只能调用一次否则进程会卡住或崩溃。4. 前端与C的双向通道CefQuery、V8Handler与自定义Scheme4.1 CefQuery实现JS异步请求C再回调页面和C的通信最推荐的方式是CefQuery。它类似AJAX请求只是请求的接收方从服务器变成了C代码。使用前要加载消息路由器主进程和渲染进程都要各创建一个。主进程里创建路由器和处理器#include include/wrapper/cef_message_router.h class MyQueryHandler : public CefMessageRouterBrowserSide::Handler { public: bool OnQuery(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, const CefString request, bool persistent, CefRefPtrCefMessageRouterBrowserSide::Callback callback) override { if (request get:user) { // 模拟从C侧读取数据 std::string json {\name\:\demo\,\age\:20}; callback-Success(json); return true; // 返回true表示该请求已处理 } return false; // 返回false则继续传给其他Handler } };同时在SimpleApp里注册路由器void SimpleApp::OnContextInitialized() override { // 创建并注册消息路由器主进程侧 CefMessageRouterConfig config; config.js_query_function cefQuery; config.js_cancel_function cefCancel; message_router_ CefMessageRouterBrowserSide::Create(config); message_router_-AddHandler(new MyQueryHandler()); // 创建浏览器窗口的代码略同上 }渲染进程侧需要把路由器挂到V8上下文里实现CefRenderProcessHandlerclass SimpleRenderApp : public CefApp, public CefRenderProcessHandler { public: void OnContextCreated(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) override { if (message_router_) { message_router_-OnContextCreated(browser, frame, context); } } void OnContextReleased(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefV8Context context) override { if (message_router_) { message_router_-OnContextReleased(browser, frame, context); } } CefRefPtrCefRenderProcessHandler GetRenderProcessHandler() override { return this; } private: CefRefPtrCefMessageRouterRendererSide message_router_; };JS端调用形式与AJAX非常接近window.cefQuery({ request: get:user, persistent: false, onSuccess: function(response) { const data JSON.parse(response); console.log(data.name); // 输出demo }, onFailure: function(err, msg) { console.error(请求失败, err, msg); } });persistent参数很关键它决定回调是否长期保留。设置为false时这次请求的上下文在回调完成后释放适合一次性查询频繁调用的接口建议设置true复用回调上下文避免反复创建V8对象带来的性能损耗。如果同一页面存在多个HandlerOnQuery里返回true表示该请求被自己处理了返回false会交给下一个Handler。4.2 用CefV8Handler把C函数绑到window对象CefQuery能满足大多数异步场景但有些计算密集的纯工具函数比如字符串处理、数据转换用V8Hanler会更直接。它的原理是在渲染进程里把C函数包装成JS函数挂到全局对象上。下面是把一个getVersion函数挂到window.capi对象上的示例class MyV8Handler : public CefV8Handler { public: bool Execute(const CefString name, CefRefPtrCefV8Value object, const CefV8ValueList arguments, CefRefPtrCefV8Value retval, CefString exception) override { if (name getVersion) { retval CefV8Value::CreateString(1.2.3); return true; } exception 未知函数; return false; } };绑定需要拿到CefV8Context所以这段代码只能写在渲染进程的OnContextCreated里void SimpleRenderApp::OnContextCreated(...) { // 先创建对象再挂函数最后挂到全局window.capi CefRefPtrCefV8Value obj CefV8Value::CreateObject(nullptr, nullptr); obj-SetValue(getVersion, CefV8Value::CreateFunction(getVersion, new MyV8Handler()), V8_PROPERTY_ATTRIBUTE_NONE); context-GetGlobal()-SetValue(capi, obj, V8_PROPERTY_ATTRIBUTE_NONE); }前端调用时就像调用普通JS函数console.log(window.capi.getVersion()); // 输出1.2.3这里一个很容易踩的坑是V8Handler所在的线程是渲染线程所以执行耗时的C逻辑会直接卡住页面。比如在getVersion里做文件读取或者数据库查询页面就会出现明显的闪烁和失去响应。我一般只用V8Handler做轻量同步函数涉及磁盘、网络或线程等待的场景一律用CefQuery。4.3 自定义Scheme拦截请求让资源从本地加载如果你的页面要加载加密资源或者想把HTML打包进exe自定义Scheme是CEF提供的一个讨巧手段。所谓Scheme就是URL前缀比如http://、file://你完全可以注册一个自己的前缀比如local://demo/index.html。CEF在渲染进程请求这类URL时会回调到主进程的CefSchemeHandlerFactory由C代码决定返回什么数据。注册自定义Scheme需要实现两个类一个处理请求一个提供工厂实例。class LocalSchemeHandler : public CefResourceHandler { public: bool ProcessRequest(CefRefPtrCefRequest request, CefRefPtrCefCallback callback) override { // 这里可以统一返回一个内存中的HTML response_data_ htmlbodyh1Local Demo/h1/body/html; callback-Continue(); return true; } void GetResponseHeaders(CefRefPtrCefResponse response, int64_t response_length, CefString redirect_url) override { response-SetMimeType(text/html; charsetutf-8); response-SetStatus(200); response_length response_data_.size(); } bool ReadResponse(void* data_out, int bytes_to_read, int bytes_read, CefRefPtrCefCallback callback) override { // 把数据拷贝给调用方 int available static_castint(response_data_.size()) - offset_; bytes_read (available bytes_to_read) ? bytes_to_read : available; if (bytes_read 0) { memcpy(data_out, response_data_.c_str() offset_, bytes_read); offset_ bytes_read; return true; } return false; } private: std::string response_data_; int offset_ 0; };在OnContextInitialized里注册这个Scheme同时定义一个工厂类返回上述Handlerclass LocalSchemeHandlerFactory : public CefSchemeHandlerFactory { public: CefRefPtrCefResourceHandler Create(...) override { return new LocalSchemeHandler(); } }; // 注册到CEF CefRegisterSchemeHandlerFactory(local, demo, new LocalSchemeHandlerFactory());注册后前端访问local://demo/index.html就会走这段C代码。实际工程里我会把前端打包后的dist目录整个映射到自定义Scheme再做一层压缩解压这样发布时前端资源就被封装进了exe不会散落一堆文件。不过要留意的是自定义Scheme里的Cookie和LocalStorage行为与http不同涉及到需要持久化存储的场景要另行处理。5. CEF使用高频问题排查进程崩溃、中文乱码、白屏与证书拦截5.1 渲染进程反复崩溃先关沙箱再往下查现象页面打开后短暂显示内容几秒后整个窗口变灰白或者弹出“页面已崩溃”的提示任务管理器里能看到渲染进程不断重启。原因CEF在Windows上默认启用沙箱但沙箱依赖系统安全策略正确配置。如果程序以管理员权限运行又没正确处理沙箱或者你的程序用到了CreateProcess之类的自启动逻辑渲染进程很容易被误杀。还有一种常见情况是下载的libcef.dll与头文件版本不一致导致IPC消息格式错乱进程频繁崩溃。解决最直接的办法是在初始化里设置settings.no_sandbox true这是排除沙箱干扰最快的方式。确认崩溃不是沙箱引起后再把no_sandbox去掉测试。同时检查分发包的头文件和dll是否来自同一个版本替换时严格要求整个include/目录和libcef.dll一起更换。我遇到过只有dll被误覆盖、头文件还是旧版本的情况表现为代码能编译但运行时每次打开新页面都崩溃花费了一天时间才定位到是版本串了。5.2 中文乱码与JSON字符串转义现象页面显示的内容出现类似测试的乱码或者C传给JS的中文变成问号反过来JS提交的中文在C侧接收后打印出来全是乱码。原因CEF内部统一使用UTF-8编码处理URL和页面内容但C侧的CefString::ToString()默认按UTF-8转换如果原始字符串是GBK/GB2312编码转换后当然是乱的。另一个常见原因是页面HTML缺少meta charsetutf-8声明浏览器按本地系统编码解析中文直接变成乱码。解决HTML里务必添加meta charsetutf-8CSS、JS文件本身也统一保存为UTF-8无BOM格式。C传给前端的数据统一先转成UTF-8字符串再交给CefQuery的callback-Success()。对于JSON字段在C侧直接用第三方JSON库序列化避免手工拼接转义。前端拿到的response用JSON.parse解析不要用eval否则特殊字符会出问题。如果是从本地文件读取内容再回传读取后显式做一次编码转换不要假设文件就是UTF-8。5.3 白屏与加载失败现象窗口创建成功但浏览器区域一直空白或者第一次显示时闪一下白才出内容。断点看代码发现OnContextInitialized没有被调用或者调用了但页面没渲染出来。原因白屏有三个常见来源。第一个是初始化时序问题CefInitialize之后立即调用CefBrowserHost::CreateBrowser但渲染进程还没就绪窗口只能黑屏或白屏。第二个是资源文件缺失尤其是Resources/目录里的icu文件和*.pak文件没放在exe同级目录或路径不对。第三个是URL加载失败页面请求404或证书错误页面区域显示白屏但窗口边框还在。解决第一确认CefInitialize的返回值和日志启动失败时通常有错误输出。第二开发时在命令行参数里加上--remote-debugging-port9222然后用本机浏览器打开http://localhost:9222查看渲染进程的console和network。这一步能直接看到页面是加载失败还是渲染崩溃比猜快得多。第三确认设定的URL可以单独在浏览器中打开排除页面自身问题。如果加载的是本地文件注意file://协议的路径需要绝对路径相对路径不会正确解析。5.4 证书错误页拦截现象访问内网自签证书的HTTPS接口或页面时CEF会显示“您的连接不是私密连接”的拦截页用户无法继续操作即使内网环境本就信任该证书。原因Chromium的安全策略不受CEF的直接控制内网IP或自签证书默认不受信任。这种拦截在实际内网工具中非常常见尤其是在通过IP地址访问测试环境时。解决在开发或内网部署阶段给CEF增加命令行参数--ignore-certificate-errors关闭证书校验。更推荐在CefRequestHandler的OnCertificateError回调中处理这样可以在代码里指定哪些域名忽略证书错误哪些继续保持校验不会全局放松安全策略。实现方法是在CefRequestHandler中重写OnCertificateError并调用callback-Continue(true)。注意--ignore-certificate-errors只适合内部测试给客户交付的产品不要全局加这个参数安全审计会一票否决。6. 让CEF在项目中更顺手的三件事缓存隔离、右键菜单精简与退出清理6.1 缓存目录隔离默认情况下CEF会把缓存写在用户目录下的某个自动生成文件夹里程序卸载后缓存还在磁盘上。多开应用时多个CEF实例抢同一缓存目录会造成数据紊乱。我在实际项目中强制设置独立的缓存路径settings.cache_path D:/cef_cache/ app_name;每一项主进程启动前根据当前用户和模块名拼接唯一路径确保不同模块的缓存互不影响。验证是否生效可以打开DevTools的Application面板查看Storage类型下的根目录路径。6.2 精简右键菜单CEF默认右键菜单包含“刷新”“后退”“检查元素”等选项对内网生产工具有时会引发误操作。我实现CefContextMenuHandler在OnBeforeContextMenu里把菜单模型清空只保留一个“打开调试台”项void OnBeforeContextMenu(CefRefPtrCefBrowser browser, CefRefPtrCefFrame frame, CefRefPtrCefContextMenuParams params, CefRefPtrCefMenuModel model) override { model-Clear(); model-AddItem(1, 打开调试台); }6.3 退出清理程序退出时不按顺序清理会让进程卡住数秒。我在退出流程里固定走先CefQuitMessageLoop()消息循环退出后再CefShutdown()并且只执行一次。从那以后我每次提测都强制走一遍退出流程确认任务管理器里没有任何残留进程再放给测试。这也算是一个给自己省麻烦的习惯希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑