使用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文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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

内容如下:

1
2
3
4
5
6
7
8
9
10
11
<?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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
// 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 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进行初始化操作,对于该文件整体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#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>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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的效果吧。

运行程序,会发现报错:

1
2
3
4
5
6
7
---------------------------
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