在 MFC 应用程序中嵌入 Chromium 浏览器






4.98/5 (50投票s)
使用 Chromium Embedded Framework (CEF) v3 在 MFC 应用程序中嵌入 Chromium 浏览器
引言
Chromium Embedded Framework(简称CEF)是一个开源项目,它允许开发者在第三方应用程序中包含一个Chromium浏览器。CEF基础版为C和C++应用程序提供了API,但外部项目(非CEF实现)支持其他语言,如C#、Java、Delphi或Python。本文展示了如何在MFC单文档界面应用程序中嵌入Chromium浏览器。
必备组件
CEF既有二进制分发版也有源代码。从源代码构建可以在本地完成,也可以使用自动化构建系统,但这需要同时构建Chromium,这要复杂一些。
CEF 3的二进制分发版可在https://cefbuilds.com/获取,其中包括以下内容:
- CEF共享库 (libcef) 及其依赖项的Debug和Release版本
- C++应用程序使用CEF所需的C++包装器静态库 (libcef_dll_wrapper)
- 两个示例应用程序 (
cefsimple
和cefclient
) - 使用CEF的应用程序所需资源
- CEF的Debug和Release符号(作为单独的下载)
本文将使用CEF的二进制分发版。
因此,本文的先决条件是:
- 开发分支(trunk)的最新CEF 64位构建版
- Visual Studio 2013(因为CEF是用这个版本编译的)
有关如何构建CEF 64位的信息,请参阅我的文章在Windows上构建64位版Chromium Embedded Framework。
为简单起见,本文中我们将创建的MFC应用程序应位于主CEF文件夹中,并与CEF示例应用程序具有相同的输出位置。
应用程序的结构
一个MFC SDI应用程序有几个组件:
- 一个
CWinApp
派生类,代表应用程序实例;它提供了初始化应用程序的入口点和清理的入口点 - 一个
CFrameWnd
派生类,代表视图的框架;在单文档界面应用程序中,只有一个视图,因此只有一个框架,这个框架被称为“主框架” - 一个
CDocument
派生类,代表文档 - 一个
CView
派生类,代表显示文档数据的视图
另一方面,一个基于CEF的应用程序有以下组件:
- 一个用于初始化CEF并运行CEF消息循环的入口点。
- 一个
CefApp
派生类,用于处理特定于进程的回调。 - 一个
CefClient
派生类,用于处理特定于浏览器实例的回调(这可以包括浏览器生命周期、上下文菜单、对话框、显示通知、拖放事件、焦点事件、键盘事件等的回调)。一个CefClient
实例可以在任意数量的浏览器之间共享。 - 一个或多个使用
CefBrowserHost::CreateBrowser()
创建的CefBrowser
实例。
要在MFC应用程序中嵌入Chromium浏览器,我们需要:
- 在应用程序启动时初始化CEF;这应该在
CWinApp::InitInstance()
重写方法中完成 - 在应用程序退出时取消初始化CEF;这应该在
CWinApp::ExitInstance()
重写方法中完成 - 实现
CefClient
来处理特定于浏览器实例的回调;CefClient
的一个实例存储在CView
派生类中。创建视图时,我们还必须创建一个CefBrowser
作为视图的子窗口,并将其显示在视图顶部,并始终保持其大小与视图相同。
静态与DLL运行时库
libcef_dll_wrapper
和MFC应用程序需要使用相同的运行时库选项构建,即多线程静态版本(/MT或/MTd)或多线程DLL(/MD或/MDd)。
默认情况下,libcef_dll_wrapper
设置为使用静态版本构建。这意味着MFC应用程序也需要相同的设置,以及在“通用配置设置”中的“在静态库中使用MFC”。如果您不能或不想使用这些选项构建MFC应用程序,那么您必须将libcef_dll_wrapper
的运行时库设置从/MT更改为/MD。
本文提供的示例代码是用/MD构建的。
示例应用程序
本文提供的演示应用程序是一个简单的浏览器。它有一个地址栏,您可以在其中输入URL(在浏览页面时也会更新当前URL),还有几个按钮,一个用于导航到选定的URL,一个用于后退,一个用于前进。当然,还有一个渲染网页的浏览器窗口。
启动时,浏览器将显示磁盘上的HTML文件内容,这是一种介绍页面。
用户随后可以在地址栏中输入URL,并使用工具栏上的“Go”按钮导航到该页面。
在文章的其余部分,我们将逐步介绍如何实现此应用程序。不过,一些细节将留给读者在源代码中查找。
创建MFC应用程序
首先,我们必须创建MFC应用程序。这应该是一个单文档界面(即SDI)应用程序。我将我的应用程序命名为cefmfcdemo
。向导生成了上面提到的类,为了简单起见,我将类的名称更改为以下内容:
- 视图类为
CefView
- 文档类为
CefDoc
- 应用程序类为
CefMfcDemoApp
如前所述,应用程序应与CEF库位于同一文件夹中。这样做的唯一原因当然是演示的简单性,因为我们可以轻松地设置项目,使其具有与CEF附带的其他示例应用程序类似的依赖项和输出。
您必须进行的设置:
- 创建一个64位项目目标,复制32位项目的设置。
- 确保$(SolutionDir)$(Configuration)\是所有平台和配置的输出(这意味着主CEF文件夹中的Debug和Release文件夹)。
- 更改VC++目录,并在包含目录中添加..\,在Library目录中添加$(SolutionDir)$(Configuration)\。
- 将libcef.lib和libcef_dll_wrapper.lib添加到链接器的“附加依赖项”中。
- 在运行应用程序之前,将Resources文件夹的内容复制到Debug和Release中,因为这包含CEF框架所需的资源。
ClientHandler类
该类提供了浏览器特定回调的处理程序实现,例如生命周期、上下文菜单、对话框、拖放事件、键盘事件等。此演示应用程序的实现是CEF中cefsimple
,尤其是cefclient
示例应用程序中可用内容的简化版本。考虑到SDI应用程序的架构,将只有一个浏览器实例,这简化了客户端处理程序的实现。实际上,一个CefClient
实例可以在许多浏览器之间共享。
ClientHandler
头文件如下所示:
#include "include/base/cef_lock.h"
#include "include/cef_client.h"
class ClientHandler : public CefClient,
public CefDisplayHandler,
public CefLifeSpanHandler,
public CefLoadHandler
{
public:
// Implement this interface to receive notification of ClientHandler
// events. The methods of this class will be called on the main thread.
class Delegate
{
public:
// Called when the browser is created.
virtual void OnBrowserCreated(CefRefPtr<CefBrowser> browser) = 0;
// Called when the browser is closing.
virtual void OnBrowserClosing(CefRefPtr<CefBrowser> browser) = 0;
// Called when the browser has been closed.
virtual void OnBrowserClosed(CefRefPtr<CefBrowser> browser) = 0;
// Set the window URL address.
virtual void OnSetAddress(std::string const & url) = 0;
// Set the window title.
virtual void OnSetTitle(std::string const & title) = 0;
// Set fullscreen mode.
virtual void OnSetFullscreen(bool const fullscreen) = 0;
// Set the loading state.
virtual void OnSetLoadingState(bool const isLoading,
bool const canGoBack,
bool const canGoForward) = 0;
protected:
virtual ~Delegate() {}
};
public:
ClientHandler(Delegate* delegate);
~ClientHandler();
void CreateBrowser(CefWindowInfo const & info, CefBrowserSettings const & settings,
CefString const & url);
// CefClient methods:
virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
// CefDisplayHandler methods:
virtual void OnAddressChange(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame, const CefString& url) override;
virtual void OnTitleChange(CefRefPtr<CefBrowser> browser, const CefString& title) override;
virtual void OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen) override;
// 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 OnLoadingStateChange(CefRefPtr<CefBrowser> browser,
bool isLoading,
bool canGoBack,
bool canGoForward) override;
virtual void OnLoadError(CefRefPtr<CefBrowser> browser,
CefRefPtr<CefFrame> frame,
ErrorCode errorCode,
const CefString& errorText,
const CefString& failedUrl) override;
// This object may outlive the Delegate object so it's necessary for the
// Delegate to detach itself before destruction.
void DetachDelegate();
private:
// Include the default reference counting implementation.
IMPLEMENT_REFCOUNTING(ClientHandler);
// Include the default locking implementation.
IMPLEMENT_LOCKING(ClientHandler);
private:
Delegate* m_delegate;
};
ClientHandler
派生自几个类:
CefClient
:处理程序实现的接口CefDisplayHandler
:用于处理与浏览器显示状态相关的事件的接口;此类上的方法在UI线程上调用CefLifeSpanHandler
:用于处理与浏览器生命周期相关的事件的接口;除非另有说明,此类上的方法在UI线程上调用CefLoadHandler
:用于处理与浏览器加载状态相关的事件的接口;此类上的方法在浏览器进程UI线程或渲染进程主线程上调用
由于ClientHandler
类实现了上述三个处理程序接口,它重写了以下virtual
方法(具有一个简单的实现)。
virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
内部的Delegate
类代表一个接口,用于接收各种事件的通知,例如浏览器创建和销毁、URL更改等。这将在创建浏览器的视图中实现。
为了创建浏览器,ClientHandler
提供了一个名为CreateBrowser()
的方法。这实际上只是简单地调用CefBrowserHost::CreateBrowser
并传入适当的参数。
void ClientHandler::CreateBrowser
(CefWindowInfo const & info, CefBrowserSettings const & settings, CefString const & url)
{
CefBrowserHost::CreateBrowser(info, this, url, settings, nullptr);
}
处理程序接口方法的实现反过来会调用Delegate
接口中的方法,从而使视图(在此例中)有机会在特定事件发生时执行某些操作。
void ClientHandler::OnAddressChange
(CefRefPtr<CefBrowser> browser, CefRefPtr<CefFrame> frame, const CefString& url)
{
CEF_REQUIRE_UI_THREAD();
// Only update the address for the main (top-level) frame.
if(frame->IsMain())
{
if(m_delegate != nullptr)
m_delegate->OnSetAddress(url);
}
}
void ClientHandler::OnTitleChange(CefRefPtr<CefBrowser> browser, const CefString& title)
{
CEF_REQUIRE_UI_THREAD();
if(m_delegate != nullptr)
m_delegate->OnSetTitle(title);
}
void ClientHandler::OnFullscreenModeChange(CefRefPtr<CefBrowser> browser, bool fullscreen)
{
CEF_REQUIRE_UI_THREAD();
if(m_delegate != nullptr)
m_delegate->OnSetFullscreen(fullscreen);
}
void ClientHandler::OnAfterCreated(CefRefPtr<CefBrowser> browser)
{
CEF_REQUIRE_UI_THREAD();
if(m_delegate != nullptr)
m_delegate->OnBrowserCreated(browser);
}
bool ClientHandler::DoClose(CefRefPtr<CefBrowser> browser)
{
CEF_REQUIRE_UI_THREAD();
if(m_delegate != nullptr)
m_delegate->OnBrowserClosing(browser);
return false;
}
void ClientHandler::OnBeforeClose(CefRefPtr<CefBrowser> browser)
{
CEF_REQUIRE_UI_THREAD();
if(m_delegate != nullptr)
m_delegate->OnBrowserClosed(browser);
}
void ClientHandler::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.
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->LoadString(ss.str(), failedUrl);
}
void ClientHandler::OnLoadingStateChange
(CefRefPtr<CefBrowser> browser, bool isLoading, bool canGoBack, bool canGoForward)
{
CEF_REQUIRE_UI_THREAD();
if(m_delegate != nullptr)
m_delegate->OnSetLoadingState(isLoading, canGoBack, canGoForward);
}
delegate
实例在构造函数中设置,并在DetachDelegate()
中重置。
ClientHandler::ClientHandler(Delegate* delegate)
: m_delegate(delegate)
{
}
void ClientHandler::DetachDelegate()
{
m_delegate = nullptr;
}
CefView类
这是CView
的实现,也是ClientHandler::Delegate
接口的实现。
实现的一般思路是:当视图被创建时,也创建一个浏览器并将其显示在窗口顶部;每当视图窗口的大小改变时,也改变浏览器窗口的大小以覆盖整个客户区。
为了稍微复杂一点,浏览器将首先显示一个从磁盘加载的启动页面,其中包含一些一般信息。然后用户可以通过在应用程序的地址栏中输入URL来导航到任何URL。
在OnInitialUpdate()
方法中,我们执行以下操作:
- 创建将从磁盘加载的启动页面的URL。
- 创建
CefWindowInfo
实例,表示将要创建的浏览器的窗口信息,并将视图设置为浏览器的父级,视图的客户区设置为浏览器的窗口矩形。 - 创建
CefBrowserSettings
实例,表示浏览器初始化设置,并将web_security
设置为STATE_DISABLED
(这意味着禁用Web安全限制,即同源策略)。 - 创建
ClientHandler
实例,将视图实例指定为Delegate
,并创建浏览器,指定窗口信息、浏览器设置和初始URL。
void CefView::OnInitialUpdate()
{
CView::OnInitialUpdate();
InitStartUrl();
auto rect = RECT{0};
GetClientRect(&rect);
CefWindowInfo info;
info.SetAsChild(GetSafeHwnd(), rect);
CefBrowserSettings browserSettings;
browserSettings.web_security = STATE_DISABLED;
m_clientHandler = new ClientHandler(this);
m_clientHandler->CreateBrowser(info, browserSettings, CefString(m_startUrl));
}
void CefView::InitStartUrl()
{
TCHAR path_buffer[_MAX_PATH] = {0};
TCHAR drive[_MAX_DRIVE] = {0};
TCHAR dir[_MAX_DIR] = {0};
TCHAR fname[_MAX_FNAME] = {0};
TCHAR ext[_MAX_EXT] = {0};
::GetModuleFileName(NULL, path_buffer, sizeof(path_buffer));
auto err = _tsplitpath_s
(path_buffer, drive, _MAX_DRIVE, dir, _MAX_DIR, fname, _MAX_FNAME, ext, _MAX_EXT);
if(err != 0) {}
auto s = CString{dir};
s += _T("html");
err = _tmakepath_s(path_buffer, _MAX_PATH, drive, (LPCTSTR)s, _T("index"), _T("html"));
if(err != 0) {}
m_startUrl = CString {path_buffer};
m_startUrl.Replace(_T('\\'),_T('/'));
m_startUrl = CString {_T("file:///")} + m_startUrl;
}
ClientHandler::Delegate
接口的实现相对简单,无需过多解释。请注意,在OnSetAddress
中,我们验证新URL是否与启动URL匹配,在这种情况下,我们不在应用程序的地址栏中设置任何文本。
void CefView::OnBrowserCreated(CefRefPtr<CefBrowser> browser)
{
m_browser = browser;
}
void CefView::OnBrowserClosing(CefRefPtr<CefBrowser> browser)
{
}
void CefView::OnBrowserClosed(CefRefPtr<CefBrowser> browser)
{
if(m_browser != nullptr &&
m_browser->GetIdentifier() == browser->GetIdentifier())
{
m_browser = nullptr;
m_clientHandler->DetachDelegate();
}
}
void CefView::OnSetAddress(std::string const & url)
{
auto main = static_cast<CMainFrame*>(m_wndMain);
if(main != nullptr)
{
auto newurl = CString {url.c_str()};
if(newurl.Find(m_startUrl) >= 0)
newurl = "";
main->SetUrl(newurl);
}
}
void CefView::OnSetTitle(std::string const & title)
{
::SetWindowText(m_hWnd, CefString(title).ToWString().c_str());
}
void CefView::OnSetFullscreen(bool const fullscreen)
{
if(m_browser != nullptr)
{
if(fullscreen)
{
CefWindowsHelpers::Maximize(m_browser);
}
else
{
CefWindowsHelpers::Restore(m_browser);
}
}
}
void CefView::OnSetLoadingState(bool const isLoading,
bool const canGoBack,
bool const canGoForward)
{
}
我们在视图中必须做的一件重要事情是,每当视图大小改变时,都要调整浏览器窗口的大小。由于浏览器窗口完全覆盖了视图的客户区,它们的尺寸必须始终相同。因此,在视图中,我们必须处理WM_SIZE
消息并调整浏览器大小。
void CefView::OnSize(UINT nType, int cx, int cy)
{
CView::OnSize(nType, cx, cy);
if(m_clientHandler != nullptr)
{
if(m_browser != nullptr)
{
auto hwnd = m_browser->GetHost()->GetWindowHandle();
auto rect = RECT {0};
GetClientRect(&rect);
::SetWindowPos(hwnd, HWND_TOP, rect.left,
rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER);
}
}
}
应用程序支持的另一个功能是按F5时刷新当前页面。由于浏览器覆盖了视图的客户端区域,因此处理WM_KEYDOWN
消息以在按下F5时执行操作,我们必须主动查看父级收到的消息,然后才能进一步分派它们。这通过在视图中重写PreTranslateMessage()
来完成。
BOOL CefView::PreTranslateMessage(MSG* pMsg)
{
if(pMsg->message == WM_KEYDOWN)
{
if(pMsg->wParam == VK_F5)
{
m_browser->Reload();
}
}
return CView::PreTranslateMessage(pMsg);
}
设置应用程序
唯一剩下的重要事情是初始化和取消初始化应用程序。这必须在CefMfcdDemoApp
(CWinApp
派生类)的InitInstance()
和ExitInstance()
中完成。在InitInstance()
中,我们将调用InitializeCef()
,在ExitInstance()
中,我们将调用UninitializeCef()
。
它们的作用是:
- 初始化函数调用
CefInitialize
来初始化CEF浏览器进程,传入几个参数:应用程序参数、应用程序设置和一个CefApp
对象。在应用程序设置中,我们将multi_threaded_message_loop
设置为false
,这意味着我们必须从应用程序消息循环中调用CefDoMessageLoopWork()
。 - 取消初始化函数只是简单地调用
CefShutdown()
来在应用程序退出之前关闭CEF浏览器进程。
void CefMfcdDemoApp::InitializeCef()
{
CefMainArgs mainargs(m_hInstance);
CefSettings settings;
settings.multi_threaded_message_loop = false;
CefInitialize(mainargs, settings, m_app, nullptr);
}
void CefMfcdDemoApp::UninitializeCef()
{
CefShutdown();
}
BOOL CefMfcdDemoApp::InitInstance()
{
// various initialization
InitializeCef();
CWinApp::InitInstance();
// more initialization
return TRUE;
}
int CefMfcdDemoApp::ExitInstance()
{
AfxOleTerm(FALSE);
UninitializeCef();
return CWinApp::ExitInstance();
}
由于我们指定浏览器进程不应在单独的线程中运行消息循环(通过将multi_threaded_message_loop
设置为false
),我们需要从主线程的消息循环中调用CefDoMessageLoopWork()
。我们通过重写CWinApp::PumpMessage()
来实现,如下所示:
BOOL CefMfcdDemoApp::PumpMessage()
{
auto result = CWinApp::PumpMessage();
CefDoMessageLoopWork();
return result;
}
整合
完成所有这些设置后,应用程序就可以构建和运行了。结果应该与前面显示的屏幕截图类似。请注意,示例应用程序包含比到目前为止所描述的更多功能。然而,这些细节与在MFC应用程序中嵌入Chromium浏览器无关。您可以查看附加的源代码以了解这些细节。
本文介绍了在MFC应用程序中嵌入Chromium浏览器所需的最少步骤。有关该框架的详细信息,包括在原生应用程序中使用它的文档,请查看项目的网页。
历史
- 2016年6月10日:初始版本