声明:文章来自http://www.codeproject.com/Articles/1105945/Embedding-a-Chromium-browser-in-an-MFC-application
先通过机翻然后人工大体修改,有不通的地方,但不影响理解,程序员看懂代码就好,英语慢慢来
在MFC应用程序中嵌入一个谷歌嵌入式框架(CEF)V3的浏览器。
CEF是一个开源项目,允许开发人员包括第三方应用的浏览器。基于CEF提供的API,用于C和C++应用程序。但外部项目(由CEF不执行)支持其他语言如C或Python #,java,delphi。本文介绍了如何将浏览器嵌入在MFC单文档界面的应用。
先做准备:
CEF是既可以作为一个二进制分布和源代码。从来源的建设可以在本地或与一个自动化的建立系统,但也需要建设铬,这是一个有点复杂。
CEF 3二进制分布在https://cefbuilds.com/可包括以下内容:
- 调试版本和发布版本的共享库(CEF libcef)及其依赖项
- C++封装静态库(libcef_dll_wrapper)必要的C++应用程序使用CEF
- 两个示例应用程序(cefsimple和cefclient)
- 应用程序使用CEF资源
- 调试和发布的符号为CEF(单独下载)
- 对于本文的目的,我们将使用CEF二进制分布。
- 新的CEF 64位建立从开发分支(干)
- Visual Studio 2013(因为CEF与这个版本编译)
关于如何建立CEF 64位看了我的文章在Windows建立铬嵌入式框架的64位版本。
简单的MFC应用程序,我们将在本文中创建应位于同一输出位置为CEF的示例应用程序的主要CEF文件夹。
应用解剖
一个MFC SDI应用程序有几个组成部分:
- 一个CWinApp派生类表示应用程序实例;这为初始化和清理应用程序入口点。
- 一个CFrameWnd派生类代表一个视图的框架;在单文档界面的应用程序有一个单一的视图,因此一个框架,这个框架被称为“main frame”。
- 一个CDocument一个派生类表示文档。
- CView派生类表示显示文档数据的视图。
另一方面,CEF基础中的应用主要有以下几部分组成:
- 初始化CEF和运行CEF消息循环的切入点。
- 一个cefapp派生类来处理特定的回调过程。
- 一个cefclient派生类来处理浏览器实例具体回调(这可以包括回调浏览器的使用寿命,上下文菜单,对话框,通知显示,拖动事件、焦点事件、键盘事件,等)。cefclient的单个实例可以是任何数量的浏览器之间共享。
- 一个或多个cefbrowser实例创建cefbrowserhost::createbrowser()。
MFC应用程序中嵌入浏览器我们需要:
- 当应用程序启动时初始化CEF;应该这样做:CWinApp::initinstance() 重载的方法
- 释放销毁CEF在应用;应该这样做:CWinApp::exitinstance()重载的方法
- 实施cefclient处理浏览器实例的具体回调;cefclient实例存储在CView派生类的。当视图创建我们还必须创造一个cefbrowser作为子视图并显示在视图上面且保持它的大小尺寸为视图相同。
静态与动态链接库的运行时库
- 的libcef_dll_wrapper和MFC应用程序需要的运行时库相同的选项了,要么是多线程静态版(/MT或/MTd)或多线程DLL(/ MD或/MDd)。
- 默认情况下,libcef_dll_wrapper在静态版本上创建。这意味着MFC应用程序需要相同的设置以及在常规配置设置在静态库中使用MFC。如果你不能或不想建立MFC应用程序的选项,那么你必须把libcef_dll_wrapper运行时库设置由/MT改成/Md
- 这是本文提供的示例代码是基于/Md 创建。
示例应用程序
提供这篇文章的演示应用程序是一个简单的浏览器。它有一个地址栏,你可以键入一个网址(当你浏览网页时它也可更新当前的网址)和几个按钮,一个导航到选定的网址,一个导航和一个向前导航。当然,呈现一个网页的浏览器窗口。
在启动时,浏览器将显示从磁盘的HTML文件的内容,这是一个介绍页面。
在启动时,浏览器将显示从磁盘的HTML文件的内容,这是一个介绍页面。
然后,用户可以在地址栏中键入一个网址,并使用工具栏上的“转到”按钮导航到该页(谷歌被墙,换其他网址进行试验)。
在整个文章的后面,我们将一步一步地演示如何实现这个应用程序。一些细节将忽略掉,读者可以在源代码中找到。
创建MFC应用程序
开始我们必须首先创建MFC应用程序。这是一个单文档界面(又名SDI)中的应用。我这应用命名cefmfcdemo。向导生成上面所述的类,为简单起见,我将类的名称更改为以下:
- CefView 视图类
- CefDoc 文档类
- CefMfcDemoApp APP类
如前所述的应用应与CEF库相同的文件夹中创建。唯一的原因是因为我们的演示过程简单,可以方便地设置项目依赖项和像其他类似的示例应用程序来与CEF输出。
你必须做的设置:
你必须做的设置:
从32位创建一个64位项目目标复制设置
确保$(SolutionDir)$(Configuration)\是所有平台和配置的输出(即调试及主要CEF文件夹释放文件夹)
改变VC++目录add ..\头文件到目录,包括$(SolutionDir)$(Configuration)\库目录
libcef.lib和libcef_dll_wrapper.lib添加到链接器的附加依赖项
由于这包含由CEF框架所需要的资源,复制的资源文件夹的调试和发布你运行这个应用程序
确保$(SolutionDir)$(Configuration)\是所有平台和配置的输出(即调试及主要CEF文件夹释放文件夹)
改变VC++目录add ..\头文件到目录,包括$(SolutionDir)$(Configuration)\库目录
libcef.lib和libcef_dll_wrapper.lib添加到链接器的附加依赖项
由于这包含由CEF框架所需要的资源,复制的资源文件夹的调试和发布你运行这个应用程序
的clienthandler类
这个类提供了处理程序实现特定浏览器的回调如命,上下文菜单,对话框,拖动事件,键盘事件,和其他。这个演示程序的实施是一个简化版本,什么是可利用的在cefsimple尤其是cefclient示例应用程序从CEF。给定一个SDI应用程序的体系结构,将有一个单一的浏览器实例,简化了客户端程序的实现。在实践中cefclient实例之间可以共享许多浏览器。
的clienthandler头文件如下:
这个类提供了处理程序实现特定浏览器的回调如命,上下文菜单,对话框,拖动事件,键盘事件,和其他。这个演示程序的实施是一个简化版本,什么是可利用的在cefsimple尤其是cefclient示例应用程序从CEF。给定一个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 CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
内部委托类代表一个接口接收各种事件的通知,如浏览器创建和销毁、修改的网址等。这将由创建浏览器的视图来实现。
创建浏览器提供了一种方法调用createbrowser()。这实际上只是调用适当的参数 cefbrowserhost:createbrowser 。
void ClientHandler::CreateBrowser(CefWindowInfo const & info, CefBrowserSettings const & settings, CefString const & url)
{
CefBrowserHost::CreateBrowser(info, this, url, settings, nullptr);
}
处理程序接口方法的实现在委托接口中调用方法,给视图一个消息,以处理一个特定事件响应。
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);
}
委托的实例是在detachdelegate()构造函数初始化。
ClientHandler::ClientHandler(Delegate* delegate)
: m_delegate(delegate)
{
}
void ClientHandler::DetachDelegate()
{
m_delegate = nullptr;
}
在cefview类
这是CView的实施,而且clienthandler::代表接口。
实现的背后的总体思路是:当视图创建时也创建一个浏览器,并显示在窗口的顶部;每当视图窗口的大小的变化也改变了浏览器窗口的大小,以覆盖整个客户区。
为了使事情复杂化一点,浏览器将首先显示一个启动页,从磁盘加载,包含一些一般性的信息。然后用户可以通过在应用程序的地址栏中键入它来导航到任何的网址。
在oninitialupdate()方法我们做以下:
创建将从磁盘加载的启动页的网址
创建cefwindowinfo表示浏览器将被创建并设置视图作为浏览器的视图客户区对于浏览器窗口矩形的父窗口信息的实例。
创建cefbrowsersettings表示浏览器初始化设置和设置web_security到state_disabled实例(这意味着禁用Web安全限制,即同源策略)。
创建clienthandler指定视图实例为代表和创建浏览器指定窗口的信息的一个实例,浏览器设置和初始的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);
}
建立应用
唯一重要的事情是去初始化应用程序的初始化和反初始化。这是initinstance()和exitinstance()做的cefmfcddemoapp(派生类CWinApp)。在initinstance()我们称initializecef()和exitinstance()我们称uninitializecef()。
这些做什么:
初始化函数调用cefinitialize初始化CEF浏览器进程,通过几个参数:应用参数,应用程序设置,和cefapp对象。在应用程序设置,我们设置multi_threaded_message_loop假这意味着我们必须从我们的应用程序消息看cefdomessageloopwork()。
uninitialize函数的简单调用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 为假)我们需要从主线程的消息循环调用cefdomessageloopwork()。我们通过重写CWinApp:::pumpmessage()如下。
BOOL CefMfcdDemoApp::PumpMessage()
{
auto result = CWinApp::PumpMessage();
CefDoMessageLoopWork();
return result;
}
最后把它全部组合起来OK