65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (50投票s)

2016 年 6 月 10 日

CPOL

10分钟阅读

viewsIcon

147978

downloadIcon

4972

使用 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)
  • 两个示例应用程序 (cefsimplecefclient)
  • 使用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文件夹中的DebugRelease文件夹)。
  • 更改VC++目录,并在包含目录中添加..\,在Library目录中添加$(SolutionDir)$(Configuration)\
  • libcef.liblibcef_dll_wrapper.lib添加到链接器的“附加依赖项”中。
  • 在运行应用程序之前,将Resources文件夹的内容复制到DebugRelease中,因为这包含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);  
}

设置应用程序

唯一剩下的重要事情是初始化和取消初始化应用程序。这必须在CefMfcdDemoAppCWinApp派生类)的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日:初始版本
© . All rights reserved.