Qt嵌入瀏覽器(四)——實現(xiàn)QCefView控件

本篇簡介

從本篇開始,我們將使用CEF實現(xiàn)一個簡單的瀏覽器。相比于Qt WebEngine,CEF的提供的API更為底層也更加精細繁雜,限于篇幅和時間,接下來的幾篇講解將僅僅聚焦于如何實現(xiàn)和前幾篇講解中類似的功能,其他未涉及到的功能和更進一步的說明,可以參考CEF的官方網站和論壇。[1]

本篇的小目標:

  • 搭建CEF開發(fā)環(huán)境
  • 實現(xiàn)嵌入CEF的Qt WebView控件QCefView

搭建CEF開發(fā)環(huán)境

承前所述,筆者開發(fā)使用的操作系統(tǒng)為64位Ubuntu16.04系統(tǒng)。其他系統(tǒng)下開發(fā)環(huán)境的搭建大體步驟比較類似,因此接下來有關開發(fā)環(huán)境搭建的講解將以Ubuntu16.04系統(tǒng)為前提進行。

首先從官方的下載鏈接下載合適版本的CEF構建包,這里筆者選擇的是:成文時最新的3396版本,Minimal Distribution。

注:如果需要實現(xiàn)對winxp系統(tǒng)的支持,則必須選擇低于2623版的構建包。2623版之后,chromium官方已經停止了對xp系統(tǒng)的支持,使用此后版本的構建包可能會引發(fā)一些報錯。

需要注意的是,除了構建包已經提供的各類資源外,我們還需要自己編譯一個動態(tài)庫libcef_dll_wrapper。進入解壓好的構建包,執(zhí)行下面的命令即可:

cmake . && make libcef_dll_wrapper 

編譯成功的話,可以在libcef_dll_wrapper文件夾下找到編譯好的libcef_dll_wrapper動態(tài)庫。

接下來我們規(guī)劃一下開發(fā)項目的目錄結構,如下圖:


cef_folder.png

其中:

  • libs文件夾中存放編譯時所需的CEF庫文件,包括libcef.so(在CEF構建包的Release目錄下可以找到)和剛才編譯好的libcef_dll_wrapper動態(tài)庫。
  • include文件夾中存放CEF構建包提供的頭文件。注意:不同版本的構建包頭文件的內容可能也會不同,如果要對CEF版本進行升級,最好將整個include文件夾都替換掉,然后檢查所使用的API是否發(fā)生了變化。
  • src文件夾中存放我們自己開發(fā)的源碼。
  • build文件夾中存放我們構建和部署的可執(zhí)行程序包。

實現(xiàn)QCefView控件

首先來規(guī)劃一下這個Webview控件需要實現(xiàn)的基本功能和實現(xiàn)前提:

  1. 秉承楔子里的應用場景,這個Webview控件僅加載單一頁面,不考慮多標簽的情況;
  2. 能夠加載指定頁面/刷新當前頁面;
  3. 能夠捕獲所加載頁面加載開始、結束、錯誤的事件,并通過Qt的信號機制發(fā)送給需要監(jiān)聽的其他控件;

依照上面的三點設計思路,可以初步擬定控件的接口如下:

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();

接下來詳細分析這些接口的作用和具體實現(xiàn)。

有關cefEmbedded信號和onCefTimer槽

由于CEF本身的消息循環(huán)和Qt的消息循環(huán)存在一定沖突,所以可能出現(xiàn)CEF嵌入Qt后卡住的情況。官方文檔的說明是,CEF提供了單次觸發(fā)消息循環(huán)的方法CefDoMessageLoopWork用以整合CEF和其他圖形界面的消息循環(huán)。筆者這里采取的是較為簡單粗暴的方式:QCefView控件初始化后等待很短的一段時間,確保Qt界面啟動完成后再啟動CEF的消息循環(huán),并發(fā)送一個CEF嵌入完成的信號??丶跏蓟瘜崿F(xiàn)如下

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的引用作為參數(shù)帶入了這個構造方法。這里說明兩點:

  1. 首先,CEF里有類似Java的引用機制,實現(xiàn)為CefRefPtr<>,使用得當?shù)脑捘芎艽蟪潭壬媳苊鈨却嫘孤?。?jù)官方文檔的說明,CEF引用機制采用的是引用計數(shù)算法,這里就不詳細展開分析了。
  2. QCefClient是對CEF原生類CefClient的一層封裝。
    說到CefClient,就必須簡單說明一下CEF API的整體結構了。依照官方教程[2]的說明,每個CEF3的應用都擁有相同的整體結構:
    • 提供一個入口方法初始化CEF,并執(zhí)行CEF的消息循環(huán);
    • 提供CefApp的實現(xiàn),以處理進程相關的回調;
    • 提供CefClient的實現(xiàn),以處理瀏覽器實例相關的回調。

對于我們現(xiàn)在著手實現(xiàn)的應用而言,這里的進程相關(CefApp)可以簡單對應到我們的整個瀏覽器應用,而瀏覽器實例相關(CefClient)則可以對應到QCefView控件。如果考慮多瀏覽器標簽的情況,可能會出現(xiàn)1個CefApp對多個CefClient的情況,承前所述,這里不對這種情況進行分析。有關CefClient的具體實現(xiàn)會在下一節(jié)詳細講解。

言歸正傳,定時器超時時調用的槽方法onCefTimer實現(xiàn)如下:

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的消息循環(huán)后,從CefClient中取到了當前瀏覽器的窗口句柄,并傳遞給Qt提供的子窗口控件QWindow,最后借助windowContainer將這個子窗口包裝成一個控件,添加到QCefView控件的布局中。完成上述操作后,Cef就已經算是嵌入完成了,此時即可發(fā)送cefEmbedded信號。

CefClient

介紹完QCefView控件的基本框架,我們來看看作為核心的CefClient究竟是如何實現(xiàn)的。
查看CEF構建包提供的頭文件cef_client.h可以發(fā)現(xiàn),里面提供了很多返回值為NULL的虛接口GetXXXXHandler,通過繼承并實現(xiàn)這些handler接口,即可實現(xiàn)對應的處理功能。因此,我們首先生命一個QCefClient類,它繼承了CefClient和QWidget,使其兼具CefClient和Qt基礎控件的功能:

class QCefClient: public QWidget, public CefClient {
    Q_OBJECT
    ...
}

就我們的應用而言,最為重要的就是對瀏覽器實例生命周期的管理。在上一小節(jié)中,我們需要在喚醒CEF的消息循環(huán)后取到當前瀏覽器的窗口句柄(參見onCefTimer方法)。對此,我們可以讓QCefClient繼承CefLifeSpanHandler類,并實現(xiàn)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,并將已生成的標志位置為真?;谶@些,就可以實現(xiàn)上一節(jié)所需的獲取瀏覽窗口句柄的方法了:

CefRefPtr<CefBrowser> QCefClient::browser()
{
    return m_browser;
}

CefWindowHandle QCefClient::browserWinId()
{
    if(m_created)
    {
        return browser()->GetHost()->GetWindowHandle();
    }
    return (CefWindowHandle)-1;
}

與此類似,QCefView控件所需的加載開始/結束/錯誤的信號也可以通過令CefClient繼承CefLoadHandler類實現(xiàn)。以加載開始為例,實現(xiàn)以下接口即可:

virtual void QCefClient::OnLoadStart(CefRefPtr<CefBrowser> browser,
                         CefRefPtr<CefFrame> frame,
                         TransitionType transition_type) OVERRIDE {
    CEF_REQUIRE_UI_THREAD();
    emit loadStarted(loadingMainFrame(browser, frame));
}

這里僅對加載開始做了簡單的信號發(fā)送處理,QCefView控件監(jiān)聽這個信號并將其轉發(fā)出去,即可實現(xiàn)其所需的加載開始信號。其他兩個信號與此類似,這里就不一一贅述了。

有關CefClient所提供的其他handler的詳情,在對應的頭文件中都有較為詳細的說明,這里限于篇幅暫且略過,后續(xù)需要用到某個接口時再詳細說明。

那么最后QCefView控件需要的就只剩下最基本的加載和刷新操作了,實現(xiàn)如下:

void QCefClient::load(CefString url)
{
    browser()->GetMainFrame()->LoadURL(url);
    m_url = url;
}

void QCefClient::reload()
{
    browser()->ReloadIgnoreCache();
}

流程總結

總結而言,本小節(jié)涉及到的核心流程如下面的時序圖所示:


QCefView時序圖.png

注:時序圖上標注的“返回窗口句柄”步驟是為了方便理解,實際的實現(xiàn)是QCefView通過調用QCefClient的getter方法獲取到的窗口句柄。

有關QCefView控件的講解就先進行到這里。下一節(jié)將講解這個控件的具體使用方法。

>>返回系列索引

參考鏈接

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容