Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Embedding a Chromium Browser in an MFC Application

0.00/5 (No votes)
27 Nov 2019 15  
Chromium Embedded Framework (CEF) v3 to embed a Chromium browser in an MFC application

Introduction

Chromium Embedded Framework (or CEF) is an open source project that enables developers to include a Chromium browser in 3rd party applications. The base CEF provides APIs for C and C++ applications, but external projects (not implemented by CEF) support other languages such as C#, Java, Delphi or Python. This article shows how to embed a Chromium browser in an MFC single document interface application.

Prerequisites

CEF is available both as a binary distribution and as source code. Building from sources can be done locally or with an automated build system but requires building Chromium too which is a bit more complicated.

The binary distributions of CEF 3 are available at https://cefbuilds.com/ and include the following:

  • Debug and Release builds of CEF shared library (libcef) and its dependencies
  • C++ wrapper static library (libcef_dll_wrapper) necessary for C++ applications using CEF
  • Two sample applications (cefsimple and cefclient)
  • Resources for applications using CEF
  • Debug and Release symbols for CEF (as separate downloads)

For the purpose of this article, we will be using the binary distribution of CEF.

The prerequisites for this article therefore are:

  • Latest CEF 64-bit build from the development branch (trunk)
  • Visual Studio 2013 (because CEF is compiled with this version)

For information on how to build CEF 64-bit, see my article Building the 64-bit version of Chromium Embedded Framework on Windows.

For simplicity, the MFC application we will be creating in this article should be located in the main CEF folder with the same output location as the CEF sample applications.

Anatomy of the Application

An MFC SDI application has several components:

  • A CWinApp derived class representing the instance of the application; this offers an entry point for initializing the application and one for clean up
  • A CFrameWnd derived class representing the frame of a view; in a single document interface application, there is a single view and therefore a single frame and this frame is called “main frame”
  • A CDocument derived class representing the document
  • A CView derived class representing the view that displays the data from the document

A CEF-base application, on the other hand, has the following components:

  • An entry point to initialize CEF and run the CEF message loop.
  • A CefApp derived class to handle process-specific callbacks.
  • A CefClient derived class to handle browser-instance-specific callbacks (this can include callbacks for browser lifespan, context menus, dialogs, display notifications, drag events, focus events, keyboard events, etc.). A single instance of CefClient can be shared among any number of browsers.
  • One or more CefBrowser instances created with CefBrowserHost::CreateBrowser().

To embed a Chromium browser in an MFC application, we need to:

  • Initialize CEF when the application starts; this should be done in CWinApp::InitInstance() overridden method
  • Uninitialize CEF when the application exists; this should be done in CWinApp::ExitInstance() overridden method
  • Implement CefClient to handle browser-instance-specific callbacks; an instance of the CefClient is stored in the CView derived class. When the view is created, we must also create a CefBrowser as a child of the view and display it on top of the view and always keep it sized at the same size as the view.

Static versus DLL Run-time Library

The libcef_dll_wrapper and the MFC application needs to be built with the same options for the runtime library, that is either multi-threaded static version (/MT or /MTd) or multi-threaded DLL (/MD or /MDd).

By default, the libcef_dll_wrapper is set to be built with the static version. That means the MFC application needs the same setting as well as Use MFC in a Static Library in the General configuration settings. If you can't or don't want to build your MFC application with those options, then you must change the runtime library settings for libcef_dll_wrapper from /MT to /MD.

The sample code that is available with this article is built with /MD.

The Sample Application

The demo application provided with this article is a simple browser. It has an address bar where you can type a URL (it also updates the current URL as you browse through pages) and a few buttons, one to navigate to the selected URL, one to navigate backwards and one to navigate forward. And of course, the browser window where a web page is rendered.

At startup, the browser will display the content of the HTML file from disk, a sort of intro page.

Image 1

The user can then type a URL in the address bar and navigate to that page using the Go button on the toolbar.

Image 2

Throughout the rest of the article, we will see step by step how to implement this application. Some details will be left out though to the reader to find in the source code.

Creating the MFC Application

To get started with it, we must create the MFC application first. This should be a single document interface (aka SDI) application. I called my application cefmfcdemo. The wizard generates the classes mentioned above and for simplicity, I changed the names of the classes to the following:

  • CefView for the view class
  • CefDoc for the document class
  • CefMfcDemoApp for the application class

Image 3 Image 4

As mentioned before, the application should be created in the same folder with the CEF library. The only reason for that is of course simplicity of the demo because we can easily setup the project with similar dependencies and output like the other sample applications that come with CEF.

The settings that you have to do:

  • Create a 64-bit project target copying settings from the 32-bit one.
  • Make sure $(SolutionDir)$(Configuration)\ is the output for all platforms and configurations (that means the Debug and Release folders in the main CEF folder).
  • Change VC++ directories and add ..\ to the include directories and $(SolutionDir)$(Configuration)\ to the Library directories.
  • Add libcef.lib and libcef_dll_wrapper.lib to the Additional Dependencies of the Linker.
  • Copy the content of the Resources folder to Debug and Release before you run the application since this contains resources needed by the CEF framework.

The ClientHandler Class

This class provides handler implementations for browser specific callbacks such as lifespan, context menu, dialogs, drag events, keyboard events, and others. The implementation for this demo application is a simplified version of what is available in the cefsimple and especially cefclient sample applications from CEF. Given the architecture of a SDI application, there will be a single browser instance which simplifies the implementation of the client handler. In practice, a CefClient instance can be shared between many browsers.

The ClientHandler header looks like this:

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

The ClientHandler is derived from several classes:

  • CefClient: the interface for handler implementations
  • CefDisplayHandler: the interface for handling events related to browser display state; methods on this class are called on the UI thread
  • CefLifeSpanHandler: the interface for handling events related to browser lifespan; methods on this class are called on the UI thread unless otherwise specified
  • CefLoadHandler: the interface for handling events related to browser load status; the methods on this class are called on the browser process UI thread or render process main thread

Since the ClientHandler class implements the three mentioned handler interfaces, it overrides the following virtual methods (with a trivial implementation).

virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }

The inner Delegate class represents an interface to receive notifications of various events, such as browser creation and destruction, change of URL, etc. This will be implemented by the view where the browser is created.

To create the browser, the ClientHandler provides a method called CreateBrowser(). This actually simply calls CefBrowserHost::CreateBrowser with appropriate parameters.

void ClientHandler::CreateBrowser
    (CefWindowInfo const & info, CefBrowserSettings const & settings, CefString const & url)
{
   CefBrowserHost::CreateBrowser(info, this, url, settings, nullptr);
}

The implementation of the handler interface methods are in turn calling methods from the Delegate interface, giving a chance to the view (in this case) to do something as a result to the occurrence of a particular event.

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

The instance of a delegate is set in the constructor and reset in DetachDelegate().

ClientHandler::ClientHandler(Delegate* delegate)
   : m_delegate(delegate)
{
}

void ClientHandler::DetachDelegate()
{
   m_delegate = nullptr;
}

The CefView Class

This is the implementation of CView, but also of ClientHandler::Delegate interface.

The general idea behind the implementation is: when the view is created, also create a browser and display it on top of the window; whenever the size of the view window changes, also change the size of the browser window to cover the entire client area.

To complicate the thing a bit, the browser will first display a start-up page, loaded from disk, that contains some general information. The user can then navigate to any URL by typing it in the application's address bar.

In the OnInitialUpdate() method, we do the following:

  • Create the URL of the start-up page that will be loaded from disk.
  • Create an instance of CefWindowInfo that represents the window information for the browser that will be created and set the view as the parent for the browser and the client area of the view as the window rectangle for the browser.
  • Create an instance of CefBrowserSettings that represents settings for the browser initialization and set the web_security to STATE_DISABLED (this means disabling the web security restrictions, i.e., same-origin policy).
  • Create an instance of ClientHandler specifying the view instance as the Delegate and create the browser specifying the window information, browser settings and initial 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;
}

The implementation of the ClientHandler::Delegate interface is relatively simple and should not require much explanation. Note that in OnSetAddress, we verify whether the new URL matches the start-up URL and in that case, we set no text in the application's address bar.

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)
{
}

An important thing that we have to do in the view is resizing the browser window every time the view changes size. Since the browser window is completely overlapping the view's client area, their size must be the same all the time. So in the view, we must handle the WM_SIZE message and resize the browser.

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

Another feature that the application supports is refreshing the current page when pressing F5. Since the browser overlaps the client area of the view handling WM_KEYDOWN message to do something when F5 is pressed, we have to actively look at the mesages received by the parent before they are dispatched further. This is done by overriding PreTranslateMessage() in the view.

BOOL CefView::PreTranslateMessage(MSG* pMsg)
{
   if(pMsg->message == WM_KEYDOWN)
   {
      if(pMsg->wParam == VK_F5)
      {
         m_browser->Reload();
      }
   }

   return CView::PreTranslateMessage(pMsg);  
}

Setting Up the Application

The only important thing left is initializing and uninitializing the application. This has to be done in InitInstance() and ExitInstance() of CefMfcdDemoApp (the CWinApp derived class). In InitInstance(), we will call InitializeCef() and in ExitInstance(), we will call UninitializeCef().

What these do:

  • The initialization function is calling CefInitialize to initialize the CEF browser process, passing several parameters: application arguments, application settings, and a CefApp object. In the application settings, we set multi_threaded_message_loop to false which means we must call CefDoMessageLoopWork() from our application message look.
  • The uninitialize function is simply calling CefShutdown() to shut down the CEF browser process before the application exits.
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();
}

Since we specified that the browser process should not run a message loop in a separate thread (by setting multi_threaded_message_loop to false), we need to call CefDoMessageLoopWork() from the main thread's message loop. We do that by overriding CWinApp::PumpMessage() as shown below:

BOOL CefMfcdDemoApp::PumpMessage()
{
   auto result = CWinApp::PumpMessage();

   CefDoMessageLoopWork();
   
   return result;
}

Putting It All Together

Having all this in place, the application can be built and run. The result should look like in the screenshot presented earlier. Notice that the sample application contains additional functionality than what has been described so far. However, those details are not related to the embedding of a Chromium browser an in MFC application. You can check the attached source for those details.

This article presented the minimum necessary steps for embedding a Chromium browser in an MFC application. For details about the framework including documentation on using it in native applications, check the project's web page.

History

  • 10th June, 2016: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here