Qt嵌入浏览器(四)——实现QCefView控件

本篇简介

从本篇开始,我们将使用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动态库。

接下来我们规划一下开发项目的目录结构,如下图:


cef_folder.png

其中:

  • libs文件夹中存放编译时所需的CEF库文件,包括libcef.so(在CEF构建包的Release目录下可以找到)和刚才编译好的libcef_dll_wrapper动态库。
  • include文件夹中存放CEF构建包提供的头文件。注意:不同版本的构建包头文件的内容可能也会不同,如果要对CEF版本进行升级,最好将整个include文件夹都替换掉,然后检查所使用的API是否发生了变化。
  • src文件夹中存放我们自己开发的源码。
  • build文件夹中存放我们构建和部署的可执行程序包。

实现QCefView控件

首先来规划一下这个Webview控件需要实现的基本功能和实现前提:

  1. 秉承楔子里的应用场景,这个Webview控件仅加载单一页面,不考虑多标签的情况;
  2. 能够加载指定页面/刷新当前页面;
  3. 能够捕获所加载页面加载开始、结束、错误的事件,并通过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的引用作为参数带入了这个构造方法。这里说明两点:

  1. 首先,CEF里有类似Java的引用机制,实现为CefRefPtr<>,使用得当的话能很大程度上避免内存泄露。据官方文档的说明,CEF引用机制采用的是引用计数算法,这里就不详细展开分析了。
  2. 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时序图.png

注:时序图上标注的“返回窗口句柄”步骤是为了方便理解,实际的实现是QCefView通过调用QCefClient的getter方法获取到的窗口句柄。

有关QCefView控件的讲解就先进行到这里。下一节将讲解这个控件的具体使用方法。

>>返回系列索引

参考链接

[1] Chromium Embedded Framework官网
[2] Chromium Embedded Framework官方教程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容