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.
The user can then type a URL in the address bar and navigate to that page using the Go button on the toolbar.
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
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:
class Delegate
{
public:
virtual void OnBrowserCreated(CefRefPtr<CefBrowser> browser) = 0;
virtual void OnBrowserClosing(CefRefPtr<CefBrowser> browser) = 0;
virtual void OnBrowserClosed(CefRefPtr<CefBrowser> browser) = 0;
virtual void OnSetAddress(std::string const & url) = 0;
virtual void OnSetTitle(std::string const & title) = 0;
virtual void OnSetFullscreen(bool const fullscreen) = 0;
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);
virtual CefRefPtr<CefDisplayHandler> GetDisplayHandler() override { return this; }
virtual CefRefPtr<CefLifeSpanHandler> GetLifeSpanHandler() override { return this; }
virtual CefRefPtr<CefLoadHandler> GetLoadHandler() override { return this; }
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;
virtual void OnAfterCreated(CefRefPtr<CefBrowser> browser) override;
virtual bool DoClose(CefRefPtr<CefBrowser> browser) override;
virtual void OnBeforeClose(CefRefPtr<CefBrowser> browser) override;
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;
void DetachDelegate();
private:
IMPLEMENT_REFCOUNTING(ClientHandler);
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();
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();
if(errorCode == ERR_ABORTED)
return;
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()
{
InitializeCef();
CWinApp::InitInstance();
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