本篇简介
从本篇开始,我们将使用CEF实现一个简单的浏览器。相比于Qt WebEngine,CEF的提供的API更为底层也更加精细繁杂,限于篇幅和时间,接下来的几篇讲解将仅仅聚焦于如何实现和前几篇讲解中类似的功能,其他未涉及到的功能和更进一步的说明,可以参考CEF的官方网站和论坛。[1]
本篇的小目标:
- 搭建CEF开发环境
- 实现嵌入CEF的Qt WebView控件QCefView
搭建CEF开发环境
承前所述,笔者开发使用的操作系统为64位Ubuntu16.04系统。其他系统下开发环境的搭建大体步骤比较类似,因此接下来有关开发环境搭建的讲解将以Ubuntu16.04系统为前提进行。
首先从官方的下载链接下载合适版本的CEF构建包,这里笔者选择的是:成文时最新的3396版本,Minimal Distribution。
注:如果需要实现对winxp系统的支持,则必须选择低于2623版的构建包。2623版之后,chromium官方已经停止了对xp系统的支持,使用此后版本的构建包可能会引发一些报错。
需要注意的是,除了构建包已经提供的各类资源外,我们还需要自己编译一个动态库libcef_dll_wrapper。进入解压好的构建包,执行下面的命令即可:
cmake . && make libcef_dll_wrapper
编译成功的话,可以在libcef_dll_wrapper文件夹下找到编译好的libcef_dll_wrapper动态库。
接下来我们规划一下开发项目的目录结构,如下图:
其中:
- libs文件夹中存放编译时所需的CEF库文件,包括libcef.so(在CEF构建包的Release目录下可以找到)和刚才编译好的libcef_dll_wrapper动态库。
- include文件夹中存放CEF构建包提供的头文件。注意:不同版本的构建包头文件的内容可能也会不同,如果要对CEF版本进行升级,最好将整个include文件夹都替换掉,然后检查所使用的API是否发生了变化。
- src文件夹中存放我们自己开发的源码。
- build文件夹中存放我们构建和部署的可执行程序包。
实现QCefView控件
首先来规划一下这个Webview控件需要实现的基本功能和实现前提:
- 秉承楔子里的应用场景,这个Webview控件仅加载单一页面,不考虑多标签的情况;
- 能够加载指定页面/刷新当前页面;
- 能够捕获所加载页面加载开始、结束、错误的事件,并通过Qt的信号机制发送给需要监听的其他控件;
依照上面的三点设计思路,可以初步拟定控件的接口如下:
signals:
void cefEmbedded();
void loadStarted(bool isMainFrame);
void loadFinished(bool ok, bool isMainFrame);
void loadError(QString errorStr);
protected slots:
virtual void onCefTimer();
public:
void load(QUrl url);
void reload();
接下来详细分析这些接口的作用和具体实现。
有关cefEmbedded信号和onCefTimer槽
由于CEF本身的消息循环和Qt的消息循环存在一定冲突,所以可能出现CEF嵌入Qt后卡住的情况。官方文档的说明是,CEF提供了单次触发消息循环的方法CefDoMessageLoopWork用以整合CEF和其他图形界面的消息循环。笔者这里采取的是较为简单粗暴的方式:QCefView控件初始化后等待很短的一段时间,确保Qt界面启动完成后再启动CEF的消息循环,并发送一个CEF嵌入完成的信号。控件初始化实现如下
QCefView::QCefView(CefRefPtr<QCefClient> cefClient, QWidget *parent) : QWidget(parent)
{
m_cefEmbedded = false;
m_cefClient = cefClient;
m_cefTimer = new QTimer(this);
connect(m_cefTimer, SIGNAL(timeout()), this, SLOT(onCefTimer()));
m_cefTimer->start(10);
QCefClient* cefClientPtr = m_cefClient.get();
// 以下省略QCefClient的信号连接
...
}
可以看到构造方法里除了启动了一个定时器外,还将一个QCefClient的引用作为参数带入了这个构造方法。这里说明两点:
- 首先,CEF里有类似Java的引用机制,实现为CefRefPtr<>,使用得当的话能很大程度上避免内存泄露。据官方文档的说明,CEF引用机制采用的是引用计数算法,这里就不详细展开分析了。
- QCefClient是对CEF原生类CefClient的一层封装。
说到CefClient,就必须简单说明一下CEF API的整体结构了。依照官方教程[2]的说明,每个CEF3的应用都拥有相同的整体结构:- 提供一个入口方法初始化CEF,并执行CEF的消息循环;
- 提供CefApp的实现,以处理进程相关的回调;
- 提供CefClient的实现,以处理浏览器实例相关的回调。
对于我们现在着手实现的应用而言,这里的进程相关(CefApp)可以简单对应到我们的整个浏览器应用,而浏览器实例相关(CefClient)则可以对应到QCefView控件。如果考虑多浏览器标签的情况,可能会出现1个CefApp对多个CefClient的情况,承前所述,这里不对这种情况进行分析。有关CefClient的具体实现会在下一节详细讲解。
言归正传,定时器超时时调用的槽方法onCefTimer实现如下:
void QCefView::onCefTimer()
{
CefDoMessageLoopWork();
if(m_cefEmbedded == false)
{
CefWindowHandle browserHandle = m_cefClient->browserWinId();
if(browserHandle != (CefWindowHandle)-1)
{
QWindow* subW = QWindow::fromWinId((WId)browserHandle);
QWidget* container = QWidget::createWindowContainer(subW, this);
QStackedLayout* cefLayout = new QStackedLayout(this);
setLayout(cefLayout);
container->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
cefLayout->addWidget(container);
m_cefEmbedded = true;
emit cefEmbedded();
}
}
}
这里在唤醒CEF的消息循环后,从CefClient中取到了当前浏览器的窗口句柄,并传递给Qt提供的子窗口控件QWindow,最后借助windowContainer将这个子窗口包装成一个控件,添加到QCefView控件的布局中。完成上述操作后,Cef就已经算是嵌入完成了,此时即可发送cefEmbedded信号。
CefClient
介绍完QCefView控件的基本框架,我们来看看作为核心的CefClient究竟是如何实现的。
查看CEF构建包提供的头文件cef_client.h可以发现,里面提供了很多返回值为NULL的虚接口GetXXXXHandler,通过继承并实现这些handler接口,即可实现对应的处理功能。因此,我们首先生命一个QCefClient类,它继承了CefClient和QWidget,使其兼具CefClient和Qt基础控件的功能:
class QCefClient: public QWidget, public CefClient {
Q_OBJECT
...
}
就我们的应用而言,最为重要的就是对浏览器实例生命周期的管理。在上一小节中,我们需要在唤醒CEF的消息循环后取到当前浏览器的窗口句柄(参见onCefTimer方法)。对此,我们可以让QCefClient继承CefLifeSpanHandler类,并实现CefClient和CefLifeSpanHandler所提供的如下接口:
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE {
return this;
}
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
m_browser = browser;
m_created = true;
}
其中第一个方法将生命周期处理器设置为本实例,而第二个方法将当前浏览器的引用传递给成员变量m_browser,并将已生成的标志位置为真。基于这些,就可以实现上一节所需的获取浏览窗口句柄的方法了:
CefRefPtr<CefBrowser> QCefClient::browser()
{
return m_browser;
}
CefWindowHandle QCefClient::browserWinId()
{
if(m_created)
{
return browser()->GetHost()->GetWindowHandle();
}
return (CefWindowHandle)-1;
}
与此类似,QCefView控件所需的加载开始/结束/错误的信号也可以通过令CefClient继承CefLoadHandler类实现。以加载开始为例,实现以下接口即可:
virtual void QCefClient::OnLoadStart(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
TransitionType transition_type) OVERRIDE {
CEF_REQUIRE_UI_THREAD();
emit loadStarted(loadingMainFrame(browser, frame));
}
这里仅对加载开始做了简单的信号发送处理,QCefView控件监听这个信号并将其转发出去,即可实现其所需的加载开始信号。其他两个信号与此类似,这里就不一一赘述了。
有关CefClient所提供的其他handler的详情,在对应的头文件中都有较为详细的说明,这里限于篇幅暂且略过,后续需要用到某个接口时再详细说明。
那么最后QCefView控件需要的就只剩下最基本的加载和刷新操作了,实现如下:
void QCefClient::load(CefString url)
{
browser()->GetMainFrame()->LoadURL(url);
m_url = url;
}
void QCefClient::reload()
{
browser()->ReloadIgnoreCache();
}
流程总结
总结而言,本小节涉及到的核心流程如下面的时序图所示:
注:时序图上标注的“返回窗口句柄”步骤是为了方便理解,实际的实现是QCefView通过调用QCefClient的getter方法获取到的窗口句柄。
有关QCefView控件的讲解就先进行到这里。下一节将讲解这个控件的具体使用方法。
参考链接
[1] Chromium Embedded Framework官网
[2] Chromium Embedded Framework官方教程