阅读 743

使用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文件,并将其解压到解决方案同级目录即可。

网盘地址:链接:pan.baidu.com/s/1BylLcETs… 提取码:bydn

文章分类
前端
文章标签