使用CEF(二)— 基于VS2019编写一个简单CEF样例

使用CEF(二)— 基于VS2019编写一个简单CEF样例

在这一节中,本人将会在Windows下使用VS2019创建一个空白的C++Windows Desktop Application项目,逐步进行修改配置和代码编写,并在这个过程中介绍vs使用过程中和C++项目的结合。源码见文章末尾Github链接。

前提

你已经阅读过《使用CEF(1)— 起步》,你可以在这些地方读到:知乎链接cnblogs。或,你知道如何获得libcef的库以及libcef_dll_wrapper静态库。

文件准备

接下来,本人将以Debug的模式下完成代码的开发工作。在Release下是同样的步骤,但是需要注意的是你所选择的目标是Debug或是Release都需要和libcef库以及libcef_dll_wrapper完全一致。

  • 现在,你需要libcef库文件相关文件,它来自于:

  • 你需要使用libcef_dll_wrapper静态库文件,它来自于你编译出来的静态库:

  • 你需要libcef与wrapper的include文件,它来自于:

接下来我们创建一个名为cef的文件夹,并且把上述提到的文件夹和文件放到该目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
cef
│ libcef_dll_wrapper.lib
│ libcef_dll_wrapper.pdb

├─Debug
│ │ ......
│ │ libcef.dll
│ │ libcef.lib
│ │ libEGL.dll
│ │ libGLESv2.dll
│ │ ......
│ │
│ └─swiftshader
│ libEGL.dll
│ libGLESv2.dll

└─include
│ cef_accessibility_handler.h
│ cef_api_hash.h
│ cef_app.h
│ cef_audio_handler.h
| .....

基础文件创建完成后,我们开始编写一个简单的基于CEF的程序吧!

项目创建

创建一个Windows桌面应用程序

创建一个名为simple-cef的项目

创建完成后,我们删除所有模板生成的代码,得到一个完全空白的应用程序项目:

依赖添加

头文件添加

众所周知,C/C++头文件作为声明定义,对于编译过程有着举足轻重的位置。当我们引入CEF编译我们的项目时候,首先需要include正确位置的头文件,才能实现编译(狭义的编译,不包括链接)。我们首先把上述做好的cef文件夹放到项目所在目录下,也就是说我们把cef的inlucde头文件以及静态库文件全都加到了项目中:

然后,在VS中,我们通过如下的方式为我们的项目引入CEF的头文件:

右键项目propertiesC/C++GeneralAdditional Include Directories

PS:如果你发现没有C/C++分类,是因为你没有创建任何的源代码文件,官方FAQ。所以我们在Source Files目录下先创建一个main.cpp,然后继续上述的配置。

PS:这里本人使用了$(ProjectDir),它是一个VS宏变量,返回项目所在目录(即,vcxproj所在目录),且目录末尾带反斜杠\。从上面的Evaluated value里面展示的经过实际计算得到的值,可以验证我们配置是否正确。这里正确的返回了我们放在项目目录下的cef文件夹

这里只需要添加到cef文件夹这一层级,是因为cef/include里面的头文件在include的时候,采用了对应的"include/xxx.h",即需要从引入目录中找到include文件夹,里面查找xxx.h头文件。当我们指定到了cef层级后,就能够使得编译器正确处理cef头文件中include的位置。

这里以**$(ProjectDir)cef/include/cef_broweser.h**这个头文件举例:

当编译器发现里面的#include预编译命令后,会从头文件目录中去查找,即希望从上述配置的$(ProjectDir)cef/以及默认目录下查找,默认的项目目录应该是找不到了,但是可以在$(ProjectDir)cef/目录下找到include/cef_base.h等文件,因为$(ProjectDir)cef/include/cef_base.h确实是正确的文件路径。因此,上述额外的include文件夹只需要指定到cef层级即可。

库文件添加

完成头文件的添加后,我们还需要添加链接目标,即cef的静态库。添加方式为:

propertiesLinkerInputAdditional Dependencies

同样使用宏变量来指定对应的lib静态库:libcef_dll_wrapper.lib、libcef.lib、cef_sandbox.lib。

通过上述的库文件添加,我们就完成了**编译(狭义,头文件查找)——链接(库文件链接)**这两个步骤的配置了,接下来就是进一步,开始我们的代码编写之路。

代码编写与说明

CEF的整体架构以及CefApp以及CefClient的概念可以参考该仓库里面的文档,或者是阅读官方文档。接下来将使用cefsimple代码进行解释说明,并适当增加一些小的细节。

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
#ifndef SIMPLE_APP_H
#define SIMPLE_APP_H
#pragma once

#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);
};

#endif

这里引入的时候,如果发现VS提示,#include "include/cef_app.h"无效,首先检查上述的对项目的配置是否正确!上述项目Properties中配置的平台是x64,VS中也请选择一致的平台。而且在本Demo是无法使用32位的,因为我们下载的静态库是x64位的。

simple_app.cpp

在simple_app的实现中,主要需要提供3个部分的代码实现:

  • CefWindowDelegate
  • CefBrowserViewDelegate
  • SimpleApp
CefWindowDelegate与CefBrowserViewDelegate

Cef窗体代理以及Cef浏览器视图代理,他们是CEF提供的一套图形视图框架。这一套图形接口目前在Windows和Linux上支持了,所以在Windows和Linux我们完全可以不用选择原生的窗体框架(例如在Windows上的WinForm和Linux上的QT之类的),而是直接使用CEF提供的图形视图框架。而CEF的图形视图框架的内部实现原理我们暂时不需要知道,可以把它们想象成一些窗体和控件对象,它们需要在SimpleApp中的实现用到,所以也写在了simple_app.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
// SimpleBrowserViewDelegate
// 继承CefBrowserViewDelegate,即CEF浏览器视图代理。
// 该代理由CEF屏蔽细节,只暴露出视图控件指定的接口回调供我们实现即可
class SimpleBrowserViewDelegate : public CefBrowserViewDelegate
{
public:
SimpleBrowserViewDelegate()
{
}

bool OnPopupBrowserViewCreated(CefRefPtr<CefBrowserView> browser_view,
CefRefPtr<CefBrowserView> popup_browser_view,
bool is_devtools) OVERRIDE
{
// Create a new top-level Window for the popup. It will show itself after
// creation.
CefWindow::CreateTopLevelWindow(
new SimpleWindowDelegate(popup_browser_view));

// We created the Window.
return true;
}

private:
IMPLEMENT_REFCOUNTING(SimpleBrowserViewDelegate);
DISALLOW_COPY_AND_ASSIGN(SimpleBrowserViewDelegate);
};
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
// SimpleWindowDelegate
// 继承CefWindowDelegate,即CEF窗口代理。
// 该代理由CEF屏蔽细节,只暴露窗口一些接口回调供我们实现即可。
class SimpleWindowDelegate : public CefWindowDelegate
{
public:
explicit SimpleWindowDelegate(CefRefPtr<CefBrowserView> browser_view)
: browser_view_(browser_view)
{
}
// 窗体创建时
void OnWindowCreated(CefRefPtr<CefWindow> window) OVERRIDE
{
// Add the browser view and show the window.
window->AddChildView(browser_view_);
window->Show();

// Give keyboard focus to the browser view.
browser_view_->RequestFocus();
}
// 窗体销毁时
void OnWindowDestroyed(CefRefPtr<CefWindow> window) OVERRIDE
{
browser_view_ = nullptr;
}
// 窗体是否可以关闭
bool CanClose(CefRefPtr<CefWindow> window) OVERRIDE
{
// Allow the window to close if the browser says it's OK.
CefRefPtr<CefBrowser> browser = browser_view_->GetBrowser();
if (browser)
return browser->GetHost()->TryCloseBrowser();
return true;
}
// 获取窗体展示的最佳尺寸
CefSize GetPreferredSize(CefRefPtr<CefView> view) OVERRIDE
{
return CefSize(800, 600);
}

private:
CefRefPtr<CefBrowserView> browser_view_;

IMPLEMENT_REFCOUNTING(SimpleWindowDelegate);
DISALLOW_COPY_AND_ASSIGN(SimpleWindowDelegate);
};
SimpleApp
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
SimpleApp::SimpleApp()
{
}

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

CefRefPtr<CefCommandLine> command_line =
CefCommandLine::GetGlobalCommandLine();

const bool enable_chrome_runtime =
command_line->HasSwitch("enable-chrome-runtime");

#if defined(OS_WIN) || defined(OS_LINUX)
// Create the browser using the Views framework if "--use-views" is specified
// via the command-line. Otherwise, create the browser using the native
// platform framework. The Views framework is currently only supported on
// Windows and Linux.
const bool use_views = command_line->HasSwitch("use-views");
#else
const bool use_views = false;
#endif

// SimpleHandler implements browser-level callbacks.
CefRefPtr<SimpleClient> handler(new SimpleClient(use_views));

// Specify CEF browser settings here.
CefBrowserSettings browser_settings;

std::string url;

// Check if a "--url=" value was provided via the command-line. If so, use
// that instead of the default URL.
url = command_line->GetSwitchValue("url");
if (url.empty())
url = "https://www.cnblogs.com/w4ngzhen/";

if (use_views && !enable_chrome_runtime)
{
// Create the BrowserView.
CefRefPtr<CefBrowserView> browser_view = CefBrowserView::CreateBrowserView(
handler, url, browser_settings, nullptr, nullptr,
new SimpleBrowserViewDelegate());

// Create the Window. It will show itself after creation.
CefWindow::CreateTopLevelWindow(new SimpleWindowDelegate(browser_view));
}
else
{
// Information used when creating the native window.
CefWindowInfo window_info;

#if defined(OS_WIN)
// On Windows we need to specify certain flags that will be passed to
// CreateWindowEx().
window_info.SetAsPopup(NULL, "simple-cef by w4ngzhen");
#endif

// Create the first browser window.
CefBrowserHost::CreateBrowser(window_info, handler, url, browser_settings,
nullptr, nullptr);
}
}

simple_client

simple_client.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
52
53
54
55
56
57
58
59
60
61
#ifndef SIMPLE_CLIENT_H
#define SIMPLE_CLIENT_H

#include "include/cef_client.h"

#include <list>

class SimpleClient : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
public CefLoadHandler
{
public:
explicit SimpleClient(bool use_views);
~SimpleClient();

static SimpleClient* GetInstance();

virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() OVERRIDE
{ return this; }

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

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

// CefDisplayHandler的实现声明:
virtual void OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) OVERRIDE;
// CefLifeSpanHandler的实现声明:
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) OVERRIDE;
virtual bool DoClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) OVERRIDE;
// CefLoadHandler的实现声明:
virtual void OnLoadError(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
ErrorCode errorCode,
const CefString& errorText,
const CefString& failedUrl) OVERRIDE;

void CloseAllBrowsers(bool force_close); // 请求将所有的已经存在的浏览器窗体进行关闭
bool IsClosing() const { return is_closing_; }

private:
// 平台特定的标题修改
// 当我们没有CEF的GUI视图框架的时候,就需要特定平台的标题修改实现
// 例如,Windows中需要我们获取窗体句柄,调用Windows的API完成对该窗体的标题修改
void PlatformTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title);
const bool use_views_; // 是否使用了CEF的GUI视图框架
// 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(SimpleClient);
};

#endif
simple_client.cpp以及simple_client_os_win.cpp

这里我们提供了两份源代码,第一份是所有平台的通用实现,而第二份源码从名称可以看出跟特定的操作系统平台有关,这里就是Windows,为什么会有两份源码我们下文会逐步了解。

首先看simple_client.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
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include "simple_client.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
{
SimpleClient* 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

SimpleClient::SimpleClient(bool use_views)
: use_views_(use_views), is_closing_(false)
{
DCHECK(!g_instance);
g_instance = this;
}

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

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

void SimpleClient::OnTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title)
{
CEF_REQUIRE_UI_THREAD();

if (use_views_)
{
// 如果使用CEF的GUI视图框架,那么修改窗体的标题通过调用该视图框架的API完成
CefRefPtr<CefBrowserView> browser_view =
CefBrowserView::GetForBrowser(browser);
if (browser_view)
{
CefRefPtr<CefWindow> window = browser_view->GetWindow();
if (window)
window->SetTitle(title);
}
}
else
{
// 否则使用特定平台窗体标题修改API
// 详情见simple_client_os_win.cpp
PlatformTitleChange(browser, title);
}
}

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

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

bool SimpleClient::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 SimpleClient::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 SimpleClient::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 SimpleClient::CloseAllBrowsers(bool force_close)
{
if (!CefCurrentlyOn(TID_UI))
{
// Execute on the UI thread.
CefPostTask(TID_UI, base::Bind(&SimpleClient::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);
}

上述代码有重要部分为函数SimpleClient::OnTitleChange的实现。在该实现代码中,通过判断变量use_views_来决定是否使用CEF提供的视图框架,也就有了下面两种情况:

  • 使用了CEF提供的视图框架:在这种情况下,窗体的标题改变直接使用CEF视图框架提供的API完成修改;
  • 使用CEF提供的视图框架:在这种情况下,我们一定用了原生的窗体框架或者是第三方的(QT或者GTK+),那么就需要调用相关原生窗体的API或者第三方的API来完成窗体标题的修改。

由于存在上面的情况2,才有了下面的simple_client_os_win.cpp的代码。(PS:上面的代码并没有实现头文件里面的PlatformTitleChange声明哟,只是调用了而已)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// simple_client_os_win.cpp代码
#include "simple_client.h"

#include <windows.h>
#include <string>

#include "include/cef_browser.h"

void SimpleClient::PlatformTitleChange(CefRefPtr<CefBrowser> browser,
const CefString& title) {
// 通过GetHost()来获取CEF浏览器对象的宿主对象(这里就是Windows原生窗体)
// 再获取对应的窗体句柄
// 通过#include <windows.h>得到的WindowsAPI完成标题修改
CefWindowHandle hwnd = browser->GetHost()->GetWindowHandle();
if (hwnd)
SetWindowText(hwnd, std::wstring(title).c_str());
}

这段代码实际上跟特定的平台有关,这里就是Windows平台。

  1. 通过GetHost()来获取CEF浏览器对象的宿主对象(这里就是Windows原生窗体);
  2. 再获取对应的窗体句柄;
  3. 通过#include <windows.h>得到的WindowsAPI完成标题修改。

入口代码main.cpp

编写完成上述的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
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
// 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 <windows.h>

#include "include/cef_command_line.h"
#include "include/cef_sandbox_win.h"
#include "simple_app.h"

// When generating projects with CMake the CEF_USE_SANDBOX value will be defined
// automatically if using the required compiler version. Pass -DUSE_SANDBOX=OFF
// to the CMake command-line to disable use of the sandbox.
// Uncomment this line to manually enable sandbox support.
// #define CEF_USE_SANDBOX 1

#if defined(CEF_USE_SANDBOX)
// The cef_sandbox.lib static library may not link successfully with all VS
// versions.
#pragma comment(lib, "cef_sandbox.lib")
#endif

// Entry point function for all processes.
int APIENTRY wWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR lpCmdLine,
int nCmdShow) {
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);

// Enable High-DPI support on Windows 7 or newer.
CefEnableHighDPISupport();

void* sandbox_info = nullptr;

#if defined(CEF_USE_SANDBOX)
// Manage the life span of the sandbox information object. This is necessary
// for sandbox support on Windows. See cef_sandbox_win.h for complete details.
CefScopedSandboxInfo scoped_sandbox;
sandbox_info = scoped_sandbox.sandbox_info();
#endif

// Provide CEF with command-line arguments.
CefMainArgs main_args(hInstance);

// CEF applications have multiple sub-processes (render, plugin, GPU, etc)
// that share the same executable. This function checks the command-line and,
// if this is a sub-process, executes the appropriate logic.
int exit_code = CefExecuteProcess(main_args, nullptr, sandbox_info);
if (exit_code >= 0) {
// The sub-process has completed so return here.
return exit_code;
}

// Parse command-line arguments for use in this method.
CefRefPtr<CefCommandLine> command_line = CefCommandLine::CreateCommandLine();
command_line->InitFromString(::GetCommandLineW());

// Specify CEF global settings here.
CefSettings settings;

if (command_line->HasSwitch("enable-chrome-runtime")) {
// Enable experimental Chrome runtime. See issue #2969 for details.
settings.chrome_runtime = true;
}

#if !defined(CEF_USE_SANDBOX)
settings.no_sandbox = true;
#endif

// SimpleApp implements application-level callbacks for the browser process.
// It will create the first browser instance in OnContextInitialized() after
// CEF has initialized.
CefRefPtr<SimpleApp> app(new SimpleApp);

// Initialize CEF.
CefInitialize(main_args, settings, app.get(), sandbox_info);

// Run the CEF message loop. This will block until CefQuitMessageLoop() is
// called.
CefRunMessageLoop();

// Shut down CEF.
CefShutdown();

return 0;
}

编译与运行

上述代码完成后,我们的代码结构如下:

我们右键项目使用build指令进行尝试编译,如果不出意外会看到这些内容:

1
error LNK2038: mismatch detected for 'RuntimeLibrary': value 'MTd_StaticDebug' doesn't match value 'MDd_DynamicDebug'

译为中文大意为:未检测到运行时库:MTd_StaticDebug无法匹配MDd_DynamicDebug,MTd是什么?MDd又是什么?关键字:MD、MDd、MT以及MTd。读者可以参考这篇文章深入了解:VS运行时 /MD、/MDd 和 /MT、/MTd之间的区别。简单一点讲,我们编译出来的libcef_dll_wrapper.lib库的某个标志与我们当前编译的程序的某个标志不一致:一个是MTd一个是MDd。那么这个标志在哪儿设置呢?我们可以右键项目工程——properties——C/C++——Code Generation(代码生成)——Runtime Library中看到。

在我们的simple项目中,VS在创建项目的时候默认使用了MDd,那么libcef_dll_wrapper.lib又是使用的什么呢?在《使用CEF(1)— 起步》文章中编译libcef_dll_wrapper.lib的项目目录下使用的是MTd。下图是再回看当时的项目使用的运行库类型:

当然,具体情况也要具体判断。例如Debug与Release的不同,又或者是当时确实是使用MD(d)进行编译的,总之需要一一对应起来。这里我们修改我们的simple项目的RuntimeLibrary为对应的MTd,再次进行编译。不出意外,你会看到如下的编译成功的输出:

1
2
3
4
5
6
7
8
9
Rebuild started...
1>------ Rebuild All started: Project: simple-cef, Configuration: Debug x64 ------
1>main.cpp
1>simple_app.cpp
1>simple_client.cpp
1>simple_client_os_win.cpp
1>Generating Code...
1>simple-cef.vcxproj -> D:\Projects\cef-projects\simple-cef\x64\Debug\simple-cef.exe
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

于是,我们运行生成出来的exe,不出意外会有弹框报错。

1
2
3
4
5
6
7
---------------------------
simple-cef.exe - 系统错误
---------------------------
由于找不到 libcef.dll,无法继续执行代码。重新安装程序可能会解决此问题。
---------------------------
确定
---------------------------

检查目录下发现,确实只有个孤单的可执行程序,并没有那些依赖库。此时我们需要将所有的依赖文件全部复制到运行目录下,主要有以下几个部分需要拷贝:

  • Resources

Resources文件夹里面的所有文件和子文件夹复制到运行目录下。

  • CEF依赖库文件

将上图中除了两个lib库文件之外的组件拷贝到运行目录下。

此时,我们的编译出来的运行目录如下:

我们再次尝试运行该simple-cef,终于能够成功打开,然而再次不出意外的话,会看到一个白屏的浏览器窗口。首先会看到标题,然后转为对应的空白

运行问题:Check failed: fallback_available == base::win::GetVersion() > base::win::Version::WIN8 (1 vs. 0)

上述白屏后,还会在运行目录下会看到一个名为debug.log的文件,打开检查内容。

1
2
3
4
// debug.log
[0124/113454.346:INFO:content_main_runner_impl.cc(976)] Chrome is running in full browser mode.
[0124/113454.488:FATAL:dwrite_font_proxy_init_impl_win.cc(91)] Check failed: fallback_available == base::win::GetVersion() > base::win::Version::WIN8 (1 vs. 0)
[0124/113454.545:FATAL:dwrite_font_proxy_init_impl_win.cc(91)] Check failed: fallback_available == base::win::GetVersion() > base::win::Version::WIN8 (1 vs. 0)

该错误的关键字:CEF base::win::GetVersion() > base::win::Version::WIN8。这里能够得到一个CEF官方论坛的解答:CEF Forum Check failed: fallback_available (magpcss.org)。简单来说,浏览器程序无法加载manifest文件从而无法处理操作系统的版本问题。

解决方案

  1. 创建manifest文件放在项目根目录下

项目根目录下创建一个manifest文件:simple-cef.manifest

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>
  1. 为项目添加上述manifest

打开项目的属性,找到Manifest Tool —— Input and Output —— Additional Manifest Files,选择项目根目录下的simple-cef.manifest

保存后,我们再次构建项目并运行我们的simple-cef.exe,终于看到了期待已久的页面:

写在结尾

在不断的踩坑下,我们终于得到了一个网络页面,不过这并不意味着我们的使用CEF之旅就结束了,恰恰相反,通过这个Demo,我们接触到了更多的东西,有CefApp、CefClient类,有CefBrowserProcessHandler等等,这些类是干什么的?CefWindowDelegate、CefBrowserViewDelegate这里些CEF框架提供的窗体GUI代理又是怎样的概念?CEF跨平台的实现策略又是怎样的呢?问题只增不减,本人也会就着这些问题继续探索并给出总结。

源代码

w4ngzhen/simple-cef (github.com)

PS:在改源码中,没有将上述的cef相关库以及include文件放在源码库中,因为静态库超过了大小。请读者自行编译并按照指定的方式添加。