CEF3的HelloWorld

1,634 阅读12分钟

编译环境准备

  • 第一步:从官网下载并编译官方提供的示例代码,保证它可以正常的运行。
  • 第二步:使用 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协议去加载一些网络资源,可能会出现问题

所以我们最好使用 httphttps 之类的常见网络协议来加载本地网页。 为了加载本地页面,我们在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) overridereturn 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的特性。推荐使用自定义标题栏,这样就不存在这个问题了。