使用CEF(四)— 在QT中集成CEF(1):基本集成


QT作為C++下著名的跨平台軟件開發框架,實現了一套代碼可以在所有的操作系統、平台和屏幕類型上部署。我們前幾篇文章講解了如何構建一款基於CEF的簡單的樣例,但這些樣例的GUI都是使用的原生的或者是控件功能不強大的CEF視圖框架。本文將會重新開始,使用VS2019編寫一款基於QT的並嵌入原生窗體的文章。

環境搭建

在本文中,我沒有使用QtCreator進行項目搭建的工作,而是使用VS配合QT VS Tools類來完成項目的環境。在本文,假設你已經安裝了QT,並且了解QT的相關知識。

安裝Qt VS Tools插件

在VS中,我們通過在擴展(Extension)搜索對應的QT插件,完成安裝工作,安裝完成后,需要重啟VS。

010-install-qt-extension

配置Qt環境

找到Extensions - Qt VS Tools - Options

020-qt-extension-options

找到Qt - Versions,進行QT - VS編譯的配置:

030-config-qt-options

Qt項目創建

在經過配置以后,此時使用VS進行項目創建的時候,會發現創建的向導頁面會出現Qt的相關項目模板:

040-create-qt-project

接下來創建一個名為QtCefDemo的樣例,此時會彈出Qt的創建向導:

050-popup-qt-create-guide

然后,Qt會自動幫我們配置好Debug和Release:

060-config-debug-and-release

最后,我們再調整下項目的文件:

070-final-qt-prop-config

點擊Finish,我么就得到了如下的在VS IDE下的QT項目大致結構:

080-vs-qt-proj

當我們運行該項目以后,就可以看到目前的一個簡單的QT窗體:

090-empty-window

當然,本文的目的不僅僅是創建一個Qt窗體那樣的簡單,還需要進行CEF的簡單集成。所以,接下來我們繼續配置CEF的環境。

配置CEF環境

在前一篇文章,我們已經了解如何編譯libcef_dll_wrapper這個庫,所以,本文假設你已經編譯出了libcef_dll_wrapper.lib(Debug和Release版本,並且對應版本的程序集類型分別是:MDd和MD):

100-libcef_dll_wrapper_debug_and_MDd

110-libcef_dll_wrapper_release_and_MD

接下來,我們需要在我們的解決方案下,創建對應的文件夾,用來存放CEF在編譯和運行時會使用到的頭文件、庫文件以及資源文件。

拷貝頭文件以及資源文件

首先,我們在解決方案同級目錄下創建一個名為CefFiles的文件夾,將cef文件中的Release和Include拷貝進來:

120-copy-include-and-Resouces-files

拷貝二進制庫文件

接下來,我們在CefFiles文件夾中創建一個bin目錄,用於存放libcef.lib相關文件以及ibcef_dll_wrapper.lib庫文件,但需要注意的是,我們需要按照Debug和Release進行分類:

130-copy-libcef_lib-files

對於拷貝libcef_dll_wrapper.lib文件,我們也拷貝到對應的bin/版本目錄下:

140-copy-libcef_dll_wrapper-to-bin_Debug

Release的同理:

150-copy-libcef_dll_wrapper-to-bin_Release

此時,我們的CefFiles文件結構如下:

CefFiles
├─bin
│  ├─Debug
│  │  │  ...
│  │  │  libcef.dll
│  │  │  libcef.lib
│  │  │  libcef_dll_wrapper.lib
│  │  │  ...
│  │  │
│  │  └─swiftshader
│  │          ...
│  │
│  └─Release
│      │  ...
│      │  libcef.dll
│      │  libcef.lib
│      │  libcef_dll_wrapper.lib
│      │  ...
│      │  
│      └─swiftshader
│              ...
│
├─include
│  各種.h頭文件
│  ...
└─Resources
    │  cef.pak
    │  ..
    └─locales
            ...
            zh-CN.pak
            zh-TW.pak

編寫manifest文件

在Windows上使用CEF的時候,需要配置將manifest文件打入exe可執行程序中,這個manifest文件我們直接手工創建,在項目目錄下創建一個名為app.manifest的文件:

155-create-manifest-file

內容如下:

<?xml version="1.0" encoding="utf-8"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">  
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">  
    <application> 
      <!--The ID below indicates application support for Windows 8.1 -->  
      <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>  
      <!-- 10.0 -->  
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> 
    </application> 
  </compatibility> 
</assembly>

配置VS中的頭文件以及庫文件的加載地址

首先是配置頭文件include的目錄:

160-config-Debug-and-Release-include-dir

由於頭文件不存在Debug和Release的差別,所以Release相同配置,不再贅述。

接下來是配置鏈接庫的文件路徑,由於Debug和Release下,庫文件內容存在不同,所以需要分別配置,但我們看可以使用$(Configuration)宏來完成根據環境自動配置。

170-config-Debug-link-lib

在Release下,只需要同樣的配置,但是會自動定位。

180-config-Release-link-lib

配置manifest文件

190-config-Debug-and-Release-manifest

當然,由於manifest文件不涉及Debug還是Release,所以配置一致即可。

至此,我們的使用VS作為IDE,基於QT的框架的,集成CEF的環境完全搭建完成了,在文章的末尾,我會附上在環境搭建完成下的初始狀態的項目。

集成CEF的編碼

在CEF編碼的時候,我們直接將cefsimple中的相關代碼遷移到我們的項目中,但是會進行一定的刪改。

編寫simple_handler

simple_handler.h

#pragma once

#include "include/cef_client.h"

#include <list>

class SimpleHandler : public CefClient,
                      public CefLifeSpanHandler,
                      public CefLoadHandler
{
public:
    explicit SimpleHandler();
    ~SimpleHandler();

    // Provide access to the single global instance of this object.
    static SimpleHandler* GetInstance();

    virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() OVERRIDE
    {
        return this;
    }

    virtual CefRefPtr<CefLoadHandler> GetLoadHandler() OVERRIDE { return this; }

    // CefLifeSpanHandler methods:
    virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
    virtual bool DoClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
    virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;

    // CefLoadHandler methods:
    virtual void OnLoadError(CefRefPtr<CefBrowser> browser,
                             CefRefPtr<CefFrame> frame,
                             ErrorCode errorCode,
                             const CefString& errorText,
                             const CefString& failedUrl) OVERRIDE;

    // Request that all existing browser windows close.
    void CloseAllBrowsers(bool force_close);

    bool IsClosing() const { return is_closing_; }

private:
    // List of existing browser windows. Only accessed on the CEF UI thread.
    typedef std::list<CefRefPtr<CefBrowser>> BrowserList;
    BrowserList browser_list_;

    bool is_closing_;

    // Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(SimpleHandler);
};

simple_handler.cpp

// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "simple_handler.h"

#include <sstream>
#include <string>

#include "include/base/cef_bind.h"
#include "include/cef_app.h"
#include "include/cef_parser.h"
#include "include/views/cef_browser_view.h"
#include "include/views/cef_window.h"
#include "include/wrapper/cef_closure_task.h"
#include "include/wrapper/cef_helpers.h"

namespace
{
    SimpleHandler* g_instance = nullptr;

    // Returns a data: URI with the specified contents.
    std::string GetDataURI(const std::string& data, const std::string& mime_type)
    {
        return "data:" + mime_type + ";base64," +
            CefURIEncode(CefBase64Encode(data.data(), data.size()), false)
            .ToString();
    }
} // namespace

SimpleHandler::SimpleHandler(): is_closing_(false)
{
    DCHECK(!g_instance);
    g_instance = this;
}

SimpleHandler::~SimpleHandler()
{
    g_instance = nullptr;
}

// static
SimpleHandler* SimpleHandler::GetInstance()
{
    return g_instance;
}

void SimpleHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser)
{
    CEF_REQUIRE_UI_THREAD();

    // Add to the list of existing browsers.
    browser_list_.push_back(browser);
}

bool SimpleHandler::DoClose(CefRefPtr<CefBrowser> browser)
{
    CEF_REQUIRE_UI_THREAD();

    // Closing the main window requires special handling. See the DoClose()
    // documentation in the CEF header for a detailed destription of this
    // process.
    if (browser_list_.size() == 1)
    {
        // Set a flag to indicate that the window close should be allowed.
        is_closing_ = true;
    }

    // Allow the close. For windowed browsers this will result in the OS close
    // event being sent.
    return false;
}

void SimpleHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser)
{
    CEF_REQUIRE_UI_THREAD();

    // Remove from the list of existing browsers.
    BrowserList::iterator bit = browser_list_.begin();
    for (; bit != browser_list_.end(); ++bit)
    {
        if ((*bit)->IsSame(browser))
        {
            browser_list_.erase(bit);
            break;
        }
    }

    if (browser_list_.empty())
    {
        // All browser windows have closed. Quit the application message loop.
        CefQuitMessageLoop();
    }
}

void SimpleHandler::OnLoadError(CefRefPtr<CefBrowser> browser,
    CefRefPtr<CefFrame> frame,
    ErrorCode errorCode,
    const CefString& errorText,
    const CefString& failedUrl)
{
    CEF_REQUIRE_UI_THREAD();

    // Don't display an error for downloaded files.
    if (errorCode == ERR_ABORTED)
        return;

    // Display a load error message using a data: URI.
    std::stringstream ss;
    ss << "<html><body bgcolor=\"white\">"
        "<h2>Failed to load URL "
        << std::string(failedUrl) << " with error " << std::string(errorText)
        << " (" << errorCode << ").</h2></body></html>";

    frame->LoadURL(GetDataURI(ss.str(), "text/html"));
}

void SimpleHandler::CloseAllBrowsers(bool force_close)
{
    if (!CefCurrentlyOn(TID_UI))
    {
        // Execute on the UI thread.
        CefPostTask(TID_UI, base::Bind(&SimpleHandler::CloseAllBrowsers, this,
            force_close));
        return;
    }

    if (browser_list_.empty())
        return;

    BrowserList::const_iterator it = browser_list_.begin();
    for (; it != browser_list_.end(); ++it)
        (*it)->GetHost()->CloseBrowser(force_close);
}

編寫simple_app

simple_app.h

#pragma once
// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "include/cef_app.h"

// Implement application-level callbacks for the browser process.
class SimpleApp : public CefApp, public CefBrowserProcessHandler
{
public:
    SimpleApp();

    // CefApp methods:
    virtual CefRefPtr<CefBrowserProcessHandler> GetBrowserProcessHandler()
    OVERRIDE
    {
        return this;
    }

    // CefBrowserProcessHandler methods:
    virtual void OnContextInitialized() OVERRIDE;

private:
    // Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(SimpleApp);
};

simple_app.cpp

// Copyright (c) 2013 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.

#include "simple_app.h"

#include <string>
#include "include/views/cef_window.h"
#include "include/wrapper/cef_helpers.h"
#include "simple_handler.h"

SimpleApp::SimpleApp()
{
}

void SimpleApp::OnContextInitialized()
{
    CEF_REQUIRE_UI_THREAD();
}

編寫入口代碼處理函數集成CEF

main.cpp

對於入口函數,目前只是進行QT相關代碼的編寫,我們還需要對CEF進行初始化操作,對於該文件整體如下:

#include <cef_app.h>

#include "qtcefwindow.h"
#include "stdafx.h"
#include <QtWidgets/QApplication>

#include "simple_app.h"

/**
 * 初始化QT以及CEF相關
 */
int init_qt_cef(int& argc, char** argv)
{
    const HINSTANCE h_instance = static_cast<HINSTANCE>(GetModuleHandle(nullptr));

    const CefMainArgs main_args(h_instance);
    const CefRefPtr<SimpleApp> app(new SimpleApp); //CefApp實現,用於處理進程相關的回調。

    const int exit_code = CefExecuteProcess(main_args, app.get(), nullptr);
    if (exit_code >= 0)
    {
        return exit_code;
    }

    // 設置配置
    CefSettings settings;
    settings.multi_threaded_message_loop = true; //多線程消息循環
    settings.log_severity = LOGSEVERITY_DISABLE; //日志
    settings.no_sandbox = true; //沙盒

    CefInitialize(main_args, settings, app, nullptr);

    return -1;
}


int main(int argc, char* argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); // 解決高DPI下,界面比例問題
    QApplication a(argc, argv);
    const int result = init_qt_cef(argc, argv);
    if (result >= 0)
    {
        return result;
    }

    QtCefWindow w;
    w.show();
    a.exec();

    CefShutdown(); // 關閉CEF,釋放資源

    return 0;
}

修改qtcefwindow窗體代碼

qtcefwindow.h

為窗體添加私有成員:CefRefPtr<SimpleHandler>

#pragma once

#include <QtWidgets/QMainWindow>

#include "simple_handler.h"
#include "ui_qtcefwindow.h"

class QtCefWindow : public QMainWindow
{
    Q_OBJECT

public:
    QtCefWindow(QWidget *parent = Q_NULLPTR);

private:
    Ui::QtCefWindowClass ui;
    CefRefPtr<SimpleHandler> simple_handler_; // 這里是新增的CefRefPtr<SimpleHandler>成員
};

qtcefwindow.cpp

在構造函數中,處理關聯qtcefwindow和SimpleHandler:

#include "qtcefwindow.h"

#include <cef_request_context.h>

#include "simple_handler.h"
#include "stdafx.h"

QtCefWindow::QtCefWindow(QWidget *parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);

    // 以下是將 SimpleHandler 與窗體進行關聯的代碼
    CefWindowInfo cef_wnd_info;
    QString str_url = "https://www.cnblogs.com/w4ngzhen";
    RECT win_rect;
    QRect rect = this->geometry();
    win_rect.left = rect.left();
    win_rect.right = rect.right();
    win_rect.top = rect.top();
    win_rect.bottom = rect.bottom();

    cef_wnd_info.SetAsChild((HWND)this->winId(), win_rect); //將cef界面嵌入qt界面中
    CefBrowserSettings cef_browser_settings;
    simple_handler_ = CefRefPtr<SimpleHandler>(new SimpleHandler());
    CefBrowserHost::CreateBrowser(cef_wnd_info,
        simple_handler_,
        str_url.toStdString(),
        cef_browser_settings,
        nullptr,
        CefRequestContext::GetGlobalContext());
}

運行代碼

終於,項目搭建完成以后,我們走到了最后一步,看看我們在Qt中集成CEF的效果吧。

運行程序,會發現報錯:

---------------------------
QtCefDemo.exe - 系統錯誤
---------------------------
由於找不到 libcef.dll,無法繼續執行代碼。重新安裝程序可能會解決此問題。 
---------------------------
確定   
---------------------------

對於這個問題,其實我們就是缺少運行時候的相關庫文件,這里我們暫時先手動進行拷貝工作,以Debug環境為例,我們將資源文件拷貝到輸出目錄中:

200-copy-resources-files

然后將CefFiles\bin\Debug中所有的文件拷貝到輸出目錄中:

210-copy-cef-libs

當然,我們可以通過配置自動化腳本的方式,讓IDE幫助我們拷貝這些文件,但本文不討論這個問題。在手動拷貝了文件以后,我們再次嘗試運行。

220-run-and-display

終於,我們看到了我們想要的頁面,不過似乎渲染顯示還有點問題,不過在本文我們暫且不討論。在后續,我會單獨寫一篇文章,來談一談使用CEF以及QT集成CEF的過程中會遇到的各種問題以及解決方案。

附錄:源碼以及相關文件

本文所涉及的項目源碼在:w4ngzhen/QtCefDemo (github.com)

其中,會有兩個提交:

  1. project init
  2. integrate CEF code

230-git-commit-desc

讀者可以自行創建分支,回退到指定的提交查看對應狀態的代碼。

此外,本Demo還需要我們創建的CefFiles文件夾以及其中的文件。由於Github對於大文件的處理不太方便。本人將其上傳到了網盤,讀者只需要從網盤下載CefFiles.zip文件,並將其解壓到解決方案同級目錄即可。

網盤地址:鏈接:https://pan.baidu.com/s/1BylLcETsFAJ5-TnmzpRxeA
提取碼:bydn


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM