编译环境准备
- 第一步:从官网下载并编译官方提供的示例代码,保证它可以正常的运行。
- 第二步:使用 Visual Studio 新建空的 Windows 桌面应用。
- 第三步:配置工程
重点是第三步,至少需要配置以下几条:
| 设置项 | 位置 | 说明 |
|---|---|---|
| 头文件包含路径 | C/C++ -> 常规 -> 附加包含目录 | 参考cefclient工程,添加CEF头文件相对路径 |
| 预处理器 | C/C++ -> 预处理器 -> 预处理器定义 | 参考cefclient工程,保持跟它一样 |
| LIB 库依赖 | 链接器 -> 输入 | 参考cefclient工程,手动去除里面的绝对路径。 主要依赖了2个绝对路径,都是cef的库文件: 1、libcef.lib 2、cef_sandbox.lib 其它都是编译器提供,不需做改动 |
| Manifest 文件 | 清单工具 -> 输入和输出 -> 附加清单文件 | 拷贝CEF文件夹中的compatibility.manifest的文件到你的工程下再进行设置。 微软获取版本号的 API 没有这个 manifest,返回值有问题 |
| 采用多线程 (/MT) | C/C++ -> 代码生成 -> 运行库 | 参考cefclient工程,保持跟它一样 |
| 堆栈保留大小 | 链接器 -> 系统 -> 堆栈保留大小 | 参考cefclient工程,保持跟它一样 |
| 启用大地址 | 链接器 -> 系统 -> 启用大地址 | 参考cefclient工程,保持跟它一样 |
CEF示例工程中,使用的是绝对路径,我们新建的工程中,必须全用相对路径,禁止出现绝对路径。
代码准备
我们需要在工程源码目录下,创建3个子文件夹:
| 文件夹名称 | 说明 |
|---|---|
| browser | 浏览器进程源码放在这里 |
| renderer | 渲染进程源码放在这里 |
| other | 其它进程源码放在这里 |
CEF的采用多进程模型,多进程共用同一个exe工程,故我们要区分开各个进程的代码,防止弄混。 实现3个APP类,对应上面说的3种进程
// 文件位置:browser/browser_app.h
class BrowserApp : public CefApp, public CefBrowserProcessHandler {...};
// 文件位置:renderer/renderer_app.h
class RendererApp : public CefApp, public CefRendererProcessHandler {...};
// 文件位置:other/other_app.h
class OtherApp : public CefApp {...};
CEF 启动过程中,会给不同的进程,传递不同的参数,我们在进程入口函数(WinMain)中,通过命令行参数 type,决定创建哪个APP
auto cmdline = CefCommandLine::CreateCommandLine();
cmdline->InitFromString(::GetCommandLine());
CefRefPtr<CefApp> app;
if (cmdline->HasSwitch("type")) {
if (cmdline->GetSwitchValue("type") == "renderer") {
app = new RendererApp; // type=renderer的是渲染进程
} else {
app = new OtherApp; // 其它进程
}
} else {
app = new BrowserApp; // 没有type参数的是浏览器进程
}
到这里,请确保你的工程已经可以正确编译通过。接下来开启正式的CEF之旅。
创建第一个窗体
窗体的代码,运行于 browser 进程中,所以代码需要保存在 browser 文件夹下。 在创建窗体前,我们需要先了解下 CEF 中关于窗体的基本知识点:
| CEF 类 | 说明 |
|---|---|
| CefWindow | 窗体 |
| CefBrowserView | 视图,它依附在窗体上 |
| CefClient | 直译成中文不太准确,应该叫“视图响应接口”,处理与视图之间的交互逻辑 |
| CefWindowDelegate | 窗体代理,处理窗体的事件回调 |
| CefBrowserViewDelegate | 视图代理,处理视图的事件回调 |
需要我们继承并实现3个类: 1、CefClient:处理视图的响应逻辑 2、CefWindowDelegate:处理窗体的事件 3、CefBrowserViewDelegate:处理视图的事件 其中 CefWindow 和 CefBrowserView 类,分别是窗体和视图的句柄(上下文),不需要我们继承,直接用就行了。
// 窗体代理:window_delegate.h
class WindowDelegate : public CefWindowDelegate {...};
// 视图代理:browser_view_delegate.h
class BrowserViewDelegate : public CefBrowserViewDelegate {...};
// 视图响应接口:client.h
class Client : public CefClient {...};
有了这3个类之后,我们就可以创建我们的第一个窗体了
// 在浏览器进程的OnContextInitialized回调函数中,创建我们的第一个窗体
class BrowserApp : public: CefApp {
public:
void OnContextInitialized() override {
// 1、先创建视图响应接口
CefRefPtr<CefClient> client = new Client;
// 2、创建视图,并关联视图响应接口
CefBrowserSettings settings;
auto browser_view = CefBrowserView::CreateBrowserView(
client,
"https://www.google.com", // 这里填写我们要加载的URL地址
settings,
nullptr,
nullptr,
new BrowserViewDelegate // 使用视图代理,处理视图事件
);
// 3、创建窗体,并关联视图
CefWindow::CreateTopLevelWindow(new WindowDelegate(browser_view)); // 使用窗体代理,处理窗体事件
}
};
以上是主体流程代码,下面看下代码的细节部分:
// 窗体代理:window_delegate.h
class WindowDelegate : public CefWindowDelegate {
public:
// 窗体创建出来后,是空白的,需要将其与指定的视图进行关联
WindowDelegate(CefRefPtr<CefBrowserView> browser_view) : m_browser_view(browser_view) {}
// CefWindowDelegate methods
virtual void OnWindowCreated(CefRefPtr<CefWindow> window) override {
// 在此为窗体关联上视图
window->AddChildView(m_browser_view);
window->Show();
m_browser_view->RequestFocus();
}
void OnWindowDestroyed(CefRefPtr<CefWindow> window) override {
m_browser_view = nullptr;
}
CefSize GetPreferredSize(CefRefPtr<CefView> view) override {
return CefSize(800, 600); // 给窗体设置大小
}
private:
CefRefPtr<CefBrowserView> m_browser_view;
};
// 视图代理:browser_view_delegate.h
class BrowserViewDelegate : public CefBrowserViewDelegate {
public:
// CefBrowserViewDelegate methods
virtual bool OnPopupBrowserViewCreated(
CefRefPtr<CefBrowserView> browser_view,
CefRefPtr<CefBrowserView> popup_browser_view,
bool is_devtools) {
// 页面中弹出的新视图,默认没有与WindowDelegate关联,使用的默认窗体。窗体尺寸为0
// 这里将新视图与我们的窗体代理关联,不然弹出的新窗体,我们无法处理其事件回调
CefWindow::CreateTopLevelWindow(new WindowDelegate(popup_browser_view));
return true;
}
};
// 视图响应接口:client.h
class Client : public CefClient, public CefLifeSpanHandler {
public:
// CefLifeSpanHandler methods:
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
m_browsers.push_back(browser);
}
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
if (m_browser.exists(browser)) {
m_browser.erase(browser);
}
if (m_browser.empty()) {
CefQuitMessageLoop(); // 当浏览器全部关闭后,通知消息循环结束。不然界面消失了,进程不会退出。
}
}
private:
std::list<CefRefPtr<CefBrowser>> m_browsers; // 记录打开的浏览器列表
};
加载第一个本地页面
前面我们已经可以加载web页面了,加载本地网页,浏览器默认用的是 file 协议,但存在几个问题
1、相对路径问题:如果页面中访问了一些相对路径,可能路径会找不到
2、同源策略问题:同源指"协议+域名+端口"三者相同。如果不同访问会有限制。显然如果用file协议去加载一些网络资源,可能会出现问题
所以我们最好使用 http 或 https 之类的常见网络协议来加载本地网页。
为了加载本地页面,我们在CEF里再搞一个HTTP服务器,就有点小题大做了。
那么CEF怎么模拟网络加载本地页面呢?
CEF提供了2种可用方案:
| CEF 资源管理方案 | 说明 |
|---|---|
| CefResourceManager | 通用资源管理 |
| CefSchemeHandlerFactory | 注册自定义协议。CEF内置了常见的如HTTP、HTTPS等协议。 如用户想自定义协议,可以使用该接口。 自定义协议示例:"client://myapp/" |
以上2种方案,均可实现对本地资源的加载,我们这里选用 CefResourceManager 方案。
第一步:打包和加密 HTML/CSS/JS 资源文件
压缩时,建议不要压缩进某个文件夹中。而是对根文件夹的文件列表直接压缩。示例如下:
res.zip
------------
+ index.html
+ index.css
+ dist
+ test.js
+ demo.js
第二步:注册加密压缩包资源管理接口
class Client
: public CefClient
, public CefRequestHandler
, public CefResourceRequestHandler {
public:
Client() {
// 在此对m_resource_manager进行初始化...
// CefResourceManager内置了一些常见资源的处理接口,我们直接使用现在的ZIP资源管理接口
m_resource_manager = new CefResourceManager();
m_resource_manager->AddArchiveProvider("http://myapp/", zip_file_path, zip_password, 0, "");
}
// CefClient methods:
virtual CefRefPtr<CefRequestHandler> GetRequestHandler() override {
return this; // 返回this指针
}
// CefRequestHandler methods:
virtual CefRefPtr<CefResourceRequestHandler> GetResourceRequestHandler(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool is_navigation,
bool is_download,
const CefString& request_initiator,
bool& disable_default_handling) override {
return this; // 返回this指针
}
// CefResourceRequestHandler methods:
virtual ReturnValue OnBeforeResourceLoad(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
CefRefPtr<CefCallback> callback) override {
// 转发调用给资源管理器
return m_resources->OnBeforeResourceLoad(browser, frame, request, callback);
}
virtual CefRefPtr<CefResourceHandler> GetResourceHandler(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request) override {
// 转发调用给资源管理器
return m_resources->GetResourceHandler(browser, frame, request);
}
private:
CefRefPtr<CefResourceManager> m_resource_manager; // CEF资源管理器
};
CefResourceManager 类中目前内置的资源处理接口有几下这些
class CefResourceManager {
public:
void AddContentProvider(...); // 添加内容资源,交由内置的模块处理
void AddDirectoryProvider(...); // 添加文件夹资源,交由内置的模块处理
void AddArchiveProvider(...); // 添加zip资源,交由内置的模块处理
void AddProvider(...); // 添加原始接口,需要由我们自定义并实现相应的接口
};
至此,我们完成了本地页面的加载。
打开浏览器开发者工具:DevTools
在 Visual Studio 里调试CEF界面时,按下F12键会造成界面崩掉。不调试直接运行就没事(原因我没去深纠)。 所以这里我监听F11键,由F11键,开启调用工具。
class Client
: public CefClient
, public CefKeyboardHandler {
public:
// CefClient methods:
virtual CefRefPtr<CefKeyboardHandler> GetKeyboardHandler() override {
return this;
}
// CefKeyboardHandler methods:
virtual bool OnPreKeyEvent(CefRefPtr<CefBrowser> browser,
const CefKeyEvent& event,
CefEventHandle os_event,
bool* is_keyboard_shortcut) override {
if (event.type == KEYEVENT_RAWKEYDOWN) {
switch (event.windows_key_code) {
case VK_F5: // 刷新页面
browser->Reload();
return true;
case VK_F11: // 打开浏览器开发者工具(在VS2022调试状态下,按F12会触发一个异常,暂没深纠,绕开用F11键)
CefWindowInfo windowInfo;
CefBrowserSettings settings;
windowInfo.SetAsPopup(NULL, "Dev Tools");
browser->GetHost()->ShowDevTools(windowInfo, this, settings, CefPoint());
return true;
}
}
return false;
}
};
至此,我们便可以调用前端代码。
JS调用C++
CEF内置了消息路由,用于打通JS对C++接口的调用。
在这里,需要进行一次知识点扫盲:
| 知识点 | 说明 |
|---|---|
| JS 代码层 | 运行于渲染进程中 |
| C++ 代码层 | 运行于浏览器进程中 |
所以 JS 调用 C++ 本质上是基于进程间通信来实现的。但好在 CEF 已经为我们实现了功能的封装,我们只需要在2个进程中,套用 CEF 提供的框架代码即可。
// 渲染进程要套用的代码
class RendererApp : public CefApp, public CefRenderProcessHandler {
public:
// CefRenderProcessHandler methods:
virtual void OnWebKitInitialized() override {
CefMessageRouterConfig config;
m_message_router = CefMessageRouterRendererSide::Create(config);
}
virtual void OnContextCreated(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) override {
m_message_router->OnContextCreated(browser, frame, context);
}
virtual void OnContextReleased(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, CefRefPtr<CefV8Context> context) override {
m_message_router->OnContextReleased(browser, frame, context);
}
virtual bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) override {
return m_message_router->OnProcessMessageReceived(browser, frame, source_process, message);
}
private:
CefRefPtr<CefMessageRouterRendererSide> m_message_router;
};
// 浏览器进程要套用的代码
class Client
: public CefClient
, public CefRequestHandler
, public CefLifeSpanHandler
public:
// CefClient methods:
virtual CefRefPtr<CefRequestHandler> GetRequestHandler() override {
return this; // 返回this指针
}
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override {
return this; // 返回this指针
}
virtual bool OnProcessMessageReceived(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefProcessId source_process,
CefRefPtr<CefProcessMessage> message) override {
// 收到进程间通信后,转发给消息路由
return m_message_router->OnProcessMessageReceived(browser, frame, source_process, message);
}
// CefRequestHandler methods:
bool OnBeforeBrowse(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool user_gesture,
bool is_redirect) override {
// 转发给消息路由
m_message_router->OnBeforeBrowse(browser, frame);
return false;
}
void OnRenderProcessTerminated(CefRefPtr<CefBrowser> browser, TerminationStatus status) override {
// 转发给消息路由
m_message_router->OnRenderProcessTerminated(browser);
}
virtual CefRefPtr<CefResourceRequestHandler> GetResourceRequestHandler(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
CefRefPtr<CefRequest> request,
bool is_navigation,
bool is_download,
const CefString& request_initiator,
bool& disable_default_handling) override {
return this; // 返回this指针
}
// CefLifeSpanHandler methods:
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) override {
// 在这里创建并初始化资源管理器
if (!m_message_router) {
CefMessageRouterConfig config;
m_message_router = CefMessageRouterBrowserSide::Create(config);
// 在这里向m_message_router注册我们自己的C++功能接口,供JS调用
}
}
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) override {
// ...
if (bNeedExit) {
// 在这里对m_message_router及其持有的资源,进行释放
// ...
m_message_router = nullptr;
}
}
}
private:
CefRefPtr<CefMessageRouterBrowserSide> m_message_router;
};
任务投递
处理JS的请求,默认在运行在浏览器进程的界面线程中的,如果遇到耗时任务,我们需要将其投递给后台线程执行。 先来一波知识点扫盲:
| 线程 | 描述 |
|---|---|
| TID_UI | 界面线程,禁止阻塞 |
| TID_IO | 处理IPC消息以及网络通信,禁止阻塞 |
| TID_RENDERER | 所有的Blink及V8交互必须在此线程中执行 |
| TID_PROCESS_LAUNCHER | 用于启动和终结浏览器进程 |
| TID_FILE_BACKGROUND | 文件线程:适用于耗时较长的后台任务 |
| TID_FILE_USER_VISIBLE | 文件线程:适用于耗时较短,如更新进度值 |
| TID_FILE_USER_BLOCKING | 文件线程:适用于耗时非常短的任务,如后台生成界面需要的数据 |
可通过 CefCurrentlyOn 函数判断当前在哪个线程。通过 CefPostTask 向指定线程投递任务。
自定义标题栏
第一步:屏蔽系统默认标题栏
只需重写 IsFrameless 接口,即可:
class WindowDelegate : public CefWindowDelegate {
public:
bool IsFrameless(CefRefPtr<CefWindow> window) override {
return true;
}
};
但这样做会带来一个问题:DevTools窗口的标题栏也没了。所以我们要做个区别:
// 视图代理类
class BrowserViewDelegate : public CefBrowserViewDelegate {
public:
virtual bool OnPopupBrowserViewCreated(
CefRefPtr<CefBrowserView> browser_view,
CefRefPtr<CefBrowserView> popup_browser_view,
bool is_devtools) {
// 将is_devtools传递给窗体代理
CefWindow::CreateTopLevelWindow(new WindowDelegate(popup_browser_view, is_devtools));
return true;
}
};
// 窗体代理类
class WindowDelegate : public CefWindowDelegate {
public:
// 构造函数中,默认不开启开发者工具窗口
WindowDelegate(CefRefPtr<CefWindow> window, bool is_devtools = false);
virtual bool IsFrameless(CefRefPtr<CefWindow> window) {
if (m_is_devtools) {
return false; // 调试窗口,显示系统默认标题栏
} else {
return true;
}
}
private:
bool m_is_devtools = false;
};
第二步:实现标题栏拖拽
1、先给前端的标题栏组件,添加上可拖拽样式属性:-webkit-app-region:drag
<div class="titlebar">
<div>
<style>
.titlebar {
-webkit-app-region: drag;
}
<style>
2、在 C++ 层响应并处理拖拽事件
class Client
: public CefClient
, public CefDragHandler {
public:
// CefClient methods:
virtual CefRefPtr<CefDragHandler> GetDragHandler() override { return this; }
// CefDragHandler methods:
virtual void OnDraggableRegionsChanged(
CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
const std::vector<CefDraggableRegion>& regions) override {
CefRefPtr<CefBrowserView> browser_view = CefBrowserView::GetForBrowser(browser);
if (browser_view) {
CefRefPtr<CefWindow> window = browser_view->GetWindow();
if (window) {
window->SetDraggableRegions(regions);
}
}
}
};
3、将前端页面中的标题,同步到窗体标题 前端界面上需要有了标题栏和内容,但在操作系统的任务栏中是看不到的,需要同步下
class Client : public CefClient, public CefDisplayHandler {
public:
// CefClient methods:
virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() override {
return this;
}
// CefDisplayHandler methods:
virtual void OnTitleChange(CefRefPtr<CefBrowser> browser, const CefString& title) override {
auto view = CefBrowserView::GetForBrowser(browser);
if (view) {
auto win = view->GetWindow();
if (win) {
win->SetTitle(title);
}
}
}
};
关闭程序前弹出警告框
如果是自定义的标题栏,没什么难度,直接响应点击事情即可。 这里讲下默认的标题栏(默认标题栏关闭按钮被点击后,前端是收不到任务消息的)
一、使用浏览器默认的关闭提示框
// 前端 JS 代码
window.onbeforeunload = function(){
return "这里的提示文案,无论你写啥,都不会显示,只会显示Chrome浏览器内置的提示"
}
// C++ 层代码
bool WindowDelegate::CanClose(CefRefPtr<CefWindow> window) {
CefRefPtr<CefBrowser> browser = browserView->GetBrowser();
if (browser) {
return browser->GetHost()->TryCloseBrowser();
}
return true;
}
二、使用自定义的关闭提示框
class Client
: public CefClient
, public CefJSDialogHandler {
public:
// CefClient methods:
CefRefPtr<CefJSDialogHandler> GetJSDialogHandler() override { return this; }
// CefJSDialogHandler methods:
bool OnBeforeUnloadDialog(
CefRefPtr<CefBrowser> browser,
const CefString& message_text,
bool is_reload,
CefRefPtr<CefJSDialogCallback> callback) override {
HWND hwnd = browser->GetHost()->GetWindowHandle();
int msgboxID = MessageBox(hwnd, L"您编辑的内容尚未保存.\n确定要关闭窗口吗?", L"系统提示", MB_OKCANCEL);
if (msgboxID == IDOK) {
callback->Continue(true, CefString());
} else {
callback->Continue(false, CefString());
}
return true;
}
};
三、总结: 无论使用哪种方法,都有个蛋疼的问题:如果用户与页面没有发生过交互,则这个框不会弹出,窗口会被直接被关闭。这是Chrome的特性。推荐使用自定义标题栏,这样就不存在这个问题了。