Using Microsoft Edge in a native Windows desktop app – part 2

This article has been updated for the version of WebView2 that requires Microsoft Edge WebView2 Runtime 88.0.705.50 or newer.

In the second part of this series, we will see how to use the WebView2 control in a C++ Windows desktop application. We will use a single document interface MFC application that features a toolbar where you can specify an address to navigate to and buttons to navigate back and forward as well as reloading the current page or stopping navigation.

The API overview

The WebView2 SDK contains the following APIs:

The environment is a container that runs a specific version of the Edge browser, with optional custom browser arguments, and a user data folder.

In order to create a web view control you must do the following:

  • Call CreateCoreWebView2EnvironmentWithOptions() to create the web view environment.
  • When the environment is available, use the ICoreWebView2Environment interface to create the web view and its controller by calling CreateCoreWebView2Controller.
  • When the web view controller is available, use the ICoreWebView2Controller interface to retrieve a pointer to the webview, ICoreWebView2* so you can add and remove event handlers. Also, you can retrieve a pointer to the ICoreWebView2Settings interface to modify web view features.

The demo app

To see how the WebView2 control works, we will use a very simple MFC application with SDI support. The WebView2 control will be created and display within the view. The application contains a toolbar with buttons to navigate back and forward, to stop or reload a page, and a control to provide the URL. Navigation to the URL starts when you press the ENTER key. With this minimum functionality, the application mimics a browser.

The most important classes here are the following:

  • CMainFrame that represents the main window of the application, that contains the menu, toolbar, and status bar. This is where the toolbar events are handled and processed.
  • CMfcEdgeDemoView is the view in the SDI architecture. It is a window that contains and displays on top of itself the WebView2 control, implemented by the class CWebBrowser that we will see below. The class overrides OnInitialUpdate() to create the web view and DestroyWindow() to destroy it. It also handles the WM_SIZE window message to resize the web view control.

You can check the attached demo projects to look at the source code details.

Creating the WebView2 control

The WebView2 control will be managed by the CWebBrowser class. This class is derived from CWnd and has the following interface:

class CWebBrowser : public CWnd
{
public:
   enum class CallbackType 
   {
      CreationCompleted,
      NavigationCompleted
   };

   using CallbackFunc = std::function<void()>;

public:
   CWebBrowser();
   virtual ~CWebBrowser();

   virtual BOOL Create(
      LPCTSTR lpszClassName,
      LPCTSTR lpszWindowName, 
      DWORD dwStyle,
      const RECT& rect,
      CWnd* pParentWnd, 
      UINT nID,
      CCreateContext* = NULL) override;

   BOOL CreateAsync(
      DWORD dwStyle,
      const RECT& rect,
      CWnd* pParentWnd,
      UINT nID,
      CallbackFunc onCreated);

   RECT GetBounds();
   void SetBounds(LONG const width, LONG const height) { Resize(width, height); }
   void Resize(LONG const width, LONG const height);
   bool IsWebViewCreated() const;

protected:
   DECLARE_DYNCREATE(CWebBrowser)
   DECLARE_MESSAGE_MAP()

private:
   CWebBrowserImpl* m_pImpl;
   std::map<CallbackType, CallbackFunc> m_callbacks;

private:
   void RunAsync(CallbackFunc callback);
   void CloseWebView();
   void RegisterEventHandlers();
   void ResizeToClientArea();
   void NavigateTo(CString url);
   CString NormalizeUrl(CString url);

   static CString GetInstallPath();
   static CString GetInstallPathFromRegistry(bool const searchWebView = true);
   static CString GetInstallPathFromDisk(bool const searchWebView = true);
   static CString GetUserDataFolder();

   void InitializeWebView();
   HRESULT OnCreateEnvironmentCompleted(HRESULT result, ICoreWebView2Environment* environment);
   HRESULT OnCreateWebViewControllerCompleted(HRESULT result, ICoreWebView2Controller* controller);
   bool HandleWindowMessage(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result);
   BOOL CreateHostWindow(LPCTSTR lpszClassName, LPCTSTR lpszWindowName, DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID);
   static PCTSTR GetWindowClass();
   static LRESULT CALLBACK WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
};

The Create() method is an overload from CWnd. However, you can only use this if you want to initiate the creation of the web view and then forget about it. If you need to do something after the web view was created then you need to properly utilize the asynchronous API of the WebView2. The method CreateAsync() is initiating the creation of the web view and registers a callback that will be invoked when the web view creation is completed.

struct CWebBrowserImpl
{
   wil::com_ptr<ICoreWebView2Environment> m_webViewEnvironment;
   wil::com_ptr<ICoreWebView2>            m_webView;
   wil::com_ptr<ICoreWebView2Controller>  m_webController;
   wil::com_ptr<ICoreWebView2Settings>    m_webSettings;
};

CWebBrowser::CWebBrowser():m_pImpl(new CWebBrowserImpl())
{
   m_callbacks[CallbackType::CreationCompleted] = nullptr;
   m_callbacks[CallbackType::NavigationCompleted] = nullptr;
}

CWebBrowser::~CWebBrowser()
{
   SetWindowLongPtr(m_hWnd, GWLP_USERDATA, 0);
   CloseWebView();
   delete m_pImpl;
}

BOOL CWebBrowser::CreateHostWindow(
   LPCTSTR lpszClassName,
   LPCTSTR lpszWindowName,
   DWORD dwStyle,
   const RECT& rect,
   CWnd* pParentWnd,
   UINT nID)
{
   if (lpszClassName == nullptr)
      lpszClassName = GetWindowClass();

   if (!CWnd::Create(lpszClassName, lpszWindowName, dwStyle, rect, pParentWnd, nID))
      return FALSE;

   ::SetWindowLongPtr(m_hWnd, GWLP_USERDATA, (LONG_PTR)this);

   return TRUE;
}

BOOL CWebBrowser::CreateAsync(
   DWORD dwStyle,
   const RECT& rect,
   CWnd* pParentWnd, 
   UINT nID,
   CallbackFunc onCreated)
{
   if (!CreateHostWindow(nullptr, nullptr, dwStyle, rect, pParentWnd, nID))
      return FALSE;

   m_callbacks[CallbackType::CreationCompleted] = onCreated;

   InitializeWebView();

   return TRUE;
}

There are three steps here:

  1. Create a parent (host) window. The purpose of this window is have a message queue that we will use to process callbacks. When an event occurs, we post a message to the queue. The window procedure will process the message and invoke the appropriate callback. In this example, we have defined the CallbackType enumeration that provides two types of callbacks: one for completing the navigation, and one for completing the creation of the view.
  2. Register a callback function to be invoked when the web view has been created.
  3. Initialize the web view.

To initialize the web view we must call the CreateCoreWebView2EnvironmentWithOptions() method with the following arguments:

  • The path to the installation folder of Edge. If this is null, the component should automatically locate the installation of Edge and use that. In practice, providing null does not work well, and the component is not able to detect the browser.
  • The patch to the user data folder. If this is null, a subfolder in the current folder will be created. Beware that if your application is installed in Program Files, it will not be able to create it. Invoking this method will result in an access denied error (0x80070005 which is a HRESULT value for ERROR_ACCESS_DENIED). Therefore, make sure you provide a user folder to a writable location.
  • Optional environment options (as ICoreWebView2EnvironmentOptions*) to change the behavior of the web view.
  • A handler for the result of the asynchronous operation, that will be invoked if the environment was successfully created.

If this function fails, it returns an error code. For instance, 0x80070002 (HRESULT for ERROR_FILE_NOT_FOUND) means that the WebView2 component was not found.

It is important to note that the WebView2 environment as well as all the other WebView2 objects are single threaded and have dependencies on Windows components that require COM to be initialized for a single-threaded apartment. Therefore, the application is required to call CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED) before calling the CreateCoreWebView2EnvironmentWithOptions() function. Failure to do so, will result in an CO_E_NOTINITIALIZED error. In the attached demo project, this call is performed in the CMfcEdgeDemoApp::InitInstance() method.

The initialization function of the CWebBrowser class can be implemented as follows:

void CWebBrowser::CloseWebView()
{
   if (m_pImpl->m_webView)
   {
      m_pImpl->m_webController->Close();

      m_pImpl->m_webController = nullptr;
      m_pImpl->m_webView = nullptr;
      m_pImpl->m_webSettings = nullptr;
   }

   m_pImpl->m_webViewEnvironment = nullptr;
}

void CWebBrowser::InitializeWebView()
{
   CloseWebView();

   CString subFolder = GetInstallPath();
   CString appData = GetUserDataFolder();
   ICoreWebView2EnvironmentOptions* options = nullptr;

   HRESULT hr = CreateCoreWebView2EnvironmentWithOptions(
      subFolder, 
      appData,
      options,
      Callback<ICoreWebView2CreateCoreWebView2EnvironmentCompletedHandler>(
         this, 
         &CWebBrowser::OnCreateEnvironmentCompleted).Get());

   if (!SUCCEEDED(hr))
   {
      CString text;
      if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
      {
         text = L"Cannot found the Edge browser.";
      }
      else
      {
         text = L"Cannot create the webview environment.";
      }

      ShowFailure(hr, text);
   }
}

When the creation of the environment completes successfully, the provided callback is invoked. The first argument of the handler is a HRESULT, and the second is a pointer to the ICoreWebView2Environment interface that defines the environment. This pointer can be used to create the web view by calling CreateCoreWebView2Controller(). This method has two parameters: the handle of the parent window and a callback that will be invoked when the web view creation completes. The implementation of this function is as follows:

HRESULT CWebBrowser::OnCreateEnvironmentCompleted(
   HRESULT result, 
   ICoreWebView2Environment* environment)
{
   CHECK_FAILURE(result);
   CHECK_FAILURE(environment->QueryInterface(IID_PPV_ARGS(&m_pImpl->m_webViewEnvironment)));
   CHECK_FAILURE(m_pImpl->m_webViewEnvironment->CreateCoreWebView2Controller(
      m_hWnd, 
      Callback<ICoreWebView2CreateCoreWebView2ControllerCompletedHandler>(
         this, 
         &CWebBrowser::OnCreateWebViewControllerCompleted).Get()));

   return S_OK;
}

The callback OnCreateWebViewControllerCompleted is invoked with two arguments: a HRESULT value that indicates the success of the operation and a pointer to the ICoreWebView2Controller interfaces that defines the controller for the web view. This pointer can be used to get a pointer to the ICoreWebView2 interface. This, in turn, can be used to add and remove event handlers and invoke various methods such as navigation. The implementation is as follows:

HRESULT CWebBrowser::OnCreateWebViewControllerCompleted(
   HRESULT result, 
   ICoreWebView2Controller* controller)
{
   if (result == S_OK)
   {
      if (controller != nullptr)
      {
         m_pImpl->m_webController = controller;
         CHECK_FAILURE(controller->get_CoreWebView2(&m_pImpl->m_webView));

         CHECK_FAILURE(m_pImpl->m_webView->get_Settings(&m_pImpl->m_webSettings));

         RegisterEventHandlers();

         ResizeToClientArea();
      }

      auto callback = m_callbacks[CallbackType::CreationCompleted];
      if (callback != nullptr)
         RunAsync(callback);
   }
   else
   {
      ShowFailure(result, L"Cannot create webview environment.");
   }

   return S_OK;
}

We will look at handling events in the next installment. What you can see here is that when the creation completes we invoke the callback the user passed when initiating the asynchronous creation of the web view. However, the invocation is not done directly. Instead, a message is posted to the web view’s parent window message queue. When this message is processed, the callback is actually invoked.

void CWebBrowser::RunAsync(CallbackFunc callback)
{
   auto* task = new CallbackFunc(callback);
   PostMessage(MSG_RUN_ASYNC_CALLBACK, reinterpret_cast<WPARAM>(task), 0);
}

LRESULT CALLBACK CWebBrowser::WndProcStatic(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
   if (auto app = (CWebBrowser*)::GetWindowLongPtr(hWnd, GWLP_USERDATA))
   {
      LRESULT result = 0;
      if (app->HandleWindowMessage(hWnd, message, wParam, lParam, &result))
      {
         return result;
      }
   }

   return ::DefWindowProc(hWnd, message, wParam, lParam);
}

bool CWebBrowser::HandleWindowMessage(
   HWND, UINT message, WPARAM wParam, LPARAM lParam, LRESULT* result)
{
   *result = 0;
   
   switch (message)
   {
   case WM_SIZE:
   {
      if (lParam != 0)
      {
         ResizeToClientArea();
         return true;
      }
   }
   break;
   case MSG_RUN_ASYNC_CALLBACK:
   {
      auto* task = reinterpret_cast<CallbackFunc*>(wParam);
      (*task)();
      delete task;
      return true;
   }
   break;
   }

   return false;
}

Having a pointer to the ICoreWebView2 and ICoreWebView2Controller interfaces, we can also implement the other methods from the public interface of the CWebBrowser class.

RECT CWebBrowser::GetBounds()
{
   RECT rc{0,0,0,0};
   if (m_pImpl->m_webController)
   {
      m_pImpl->m_webController->get_Bounds(&rc);
   }

   return rc;
}

void CWebBrowser::Resize(LONG const width, LONG const height)
{
   SetWindowPos(nullptr, 0, 0, width, height, SWP_NOMOVE| SWP_NOREPOSITION);
}

CString CWebBrowser::GetLocationURL()
{
   CString url;
   if (m_pImpl->m_webView)
   {
      wil::unique_cotaskmem_string uri;
      m_pImpl->m_webView->get_Source(&uri);

      if (wcscmp(uri.get(), L"about:blank") == 0)
      {
         uri = wil::make_cotaskmem_string(L"");
      }

      url = uri.get();
   }

   return url;
}

CString CWebBrowser::NormalizeUrl(CString url)
{
   if (url.Find(_T("://")) < 0)
   {
      if (url.GetLength() > 1 && url[1] == ':')
         url = _T("file://") + url;
      else
         url = _T("http://") + url;
   }

   return url;
}

void CWebBrowser::NavigateTo(CString url)
{
   m_pImpl->m_webView->Navigate(NormalizeUrl(url));
}

void CWebBrowser::Navigate(CString const & url, CallbackFunc onComplete)
{
   if (m_pImpl->m_webView)
   {      
      m_callbacks[CallbackType::NavigationCompleted] = onComplete;
      NavigateTo(url);
   }
}

void CWebBrowser::Stop()
{
   if (m_pImpl->m_webView)
   {
      m_pImpl->m_webView->Stop();
   }
}

void CWebBrowser::Reload()
{
   if (m_pImpl->m_webView)
   {
      m_pImpl->m_webView->Reload();
   }
}

void CWebBrowser::GoBack()
{
   if (m_pImpl->m_webView)
   {
      BOOL possible = FALSE;
      m_pImpl->m_webView->get_CanGoBack(&possible);
      if(possible)
         m_pImpl->m_webView->GoBack();
   }
}

void CWebBrowser::GoForward()
{
   if (m_pImpl->m_webView)
   {
      BOOL possible = FALSE;
      m_pImpl->m_webView->get_CanGoForward(&possible);
      if (possible)
         m_pImpl->m_webView->GoForward();
   }
}

We will discuss the details about events and navigation in the next post.

What is left to show here is how the CWebBrowser is used from the SDI's view, which you can see below:

void CMfcEdgeDemoView::OnInitialUpdate()
{
   CView::OnInitialUpdate();

   this->ModifyStyleEx(WS_EX_CLIENTEDGE | WS_EX_WINDOWEDGE, 0, 0);
   this->ModifyStyle(WS_CAPTION | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_THICKFRAME | WS_BORDER, 0, 0);

   m_pWebBrowser = std::make_unique<CWebBrowser>();

   if (m_pWebBrowser != nullptr)
   {
      CRect rectClient;
      GetClientRect(rectClient);

      m_pWebBrowser->CreateAsync(
         WS_VISIBLE | WS_CHILD,
         rectClient,
         this,
         1,
         [this]() {
            m_pWebBrowser->Navigate(L"https://bing.com", nullptr);
         });
   }
}

BOOL CMfcEdgeDemoView::DestroyWindow()
{
   m_pWebBrowser.reset();

   return CView::DestroyWindow();
}

void CMfcEdgeDemoView::OnSize(UINT nType, int cx, int cy)
{
   CView::OnSize(nType, cx, cy);

   CRect rectClient;
   GetClientRect(rectClient);

   if (m_pWebBrowser != nullptr)
      m_pWebBrowser->Resize(cx, cy);
}

Notice that when calling CreateAsync(), we pass a lambda that, when invoked, triggers navigation to the https://bing.com web page.

Finding the WebView2 location

In my experience with the CreateCoreWebView2EnvironmentWithOptions(), passing null for the browser location did not work well and it was unable to find the browser installation, regardless of the version I was using (whether it was the Beta or the RTM version).

The Edge browser is installed under C:\Program Files (x86)\Microsoft\Edge\Application. The WebView2 runtime is installed under C:\Program Files (x86)\Microsoft\EdgeWebView\Application. This is the case even though Edge (and the runtime) is a 64-bit application. The reason its installation path is under Program Files (x86) and not under Program Files (as it is expected for 64-bit application) is probably historical. Chrome does the same because it was easier for scenarios when users migrated from 32-bit to the 64-bit version of the browser.

However, the folder you are expected to provide to CreateCoreWebView2EnvironmentWithOptions() is not C:\Program Files (x86)\Microsoft\EdgeWebView\Application but a subfolder that has the same name as the version of the browser. In the picture above, the version (and folder name) is 88.0.705.50.

The current version of this implementation works only with Edge Beta, which has a different installation path, c:\Program Files (x86)\Microsoft\Edge Beta\.

To programmatically detect the path of the runtime installation you can do the following:

  • Search in Windows Registry. Installation location and version information is available under SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView. (For the Edge browser it is under SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge.)
  • Search on disk in the default installation location for a folder name of the form 88.0.705.50.

In the attached source code, you will find the following implementation for this:

CString CWebBrowser::GetInstallPath()
{
   static CString path = []
   {  
      auto installPath = GetInstallPathFromRegistry();   // check registry for WebView2
      if (installPath.IsEmpty())
         installPath = GetInstallPathFromDisk();         // check disk for WebView2
      
      return installPath;
   }(); 

   return path;
}

CString CWebBrowser::GetInstallPathFromRegistry(bool const searchWebView)
{
   CString path;

   HKEY handle = nullptr;

   LSTATUS result = ERROR_FILE_NOT_FOUND;

   if (searchWebView)
   {
      result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
         LR"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView)",
         0,
         KEY_READ,
         &handle);

      if (result != ERROR_SUCCESS)
         result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
            LR"(SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft EdgeWebView)",
            0,
            KEY_READ,
            &handle);
   }
   else // this is for demo purposes only; don't use this on production environments
   {
      result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
         LR"(SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge)",
         0,
         KEY_READ,
         &handle);

      if (result != ERROR_SUCCESS)
         result = RegOpenKeyEx(HKEY_LOCAL_MACHINE,
            LR"(SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Microsoft Edge)",
            0,
            KEY_READ,
            &handle);
   }

   if (result == ERROR_SUCCESS)
   {
      TCHAR buffer[MAX_PATH + 1]{ 0 };
      DWORD type = REG_SZ;
      DWORD size = MAX_PATH;
      result = RegQueryValueEx(handle, L"InstallLocation", 0, &type, reinterpret_cast<LPBYTE>(buffer), &size);
      if (result == ERROR_SUCCESS) 
         path += CString{ buffer };

      TCHAR version[100]{ 0 };
      size = 100;
      result = RegQueryValueEx(handle, L"Version", 0, &type, reinterpret_cast<LPBYTE>(version), &size);
      if (result == ERROR_SUCCESS)
      {
         if (path.GetAt(path.GetLength() - 1) != L'\\')
            path += L"\\";
         path += CString{ version };
      }
      else
         path.Empty();

      RegCloseKey(handle);
   }

   return path;
}

CString CWebBrowser::GetInstallPathFromDisk(bool const searchWebView)
{
   CString path =
      searchWebView ?
      LR"(c:\Program Files (x86)\Microsoft\EdgeWebView\Application\)" :
      LR"(c:\Program Files (x86)\Microsoft\Edge\Application\)"; // this is for demo purposes only; don't use this on production environments
   CString pattern = path + L"*";

   WIN32_FIND_DATA ffd{ 0 };
   HANDLE hFind = FindFirstFile(pattern, &ffd);
   if (hFind == INVALID_HANDLE_VALUE)
   {
      [[maybe_unused]] DWORD error = ::GetLastError();
      return {};
   }

   do
   {
      if (ffd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
      {
         CString name{ ffd.cFileName };
         int a, b, c, d;
         if (4 == swscanf_s(ffd.cFileName, L"%d.%d.%d.%d", &a, &b, &c, &d))
         {
            FindClose(hFind);
            return path + name;
         }
      }
   } while (FindNextFile(hFind, &ffd) != 0);

   FindClose(hFind);

   return {};
}

A few more words...

In the code above, there were references to a CHECK_FAILURE macro, as well as the function ShowFailure(). This function displays a message to the user containing information about an error. There is also a function CheckFailure(), called from the CHECK_FAILURE macro that logs an error message and then terminates the process. These functions have been adapted from the sample code provided with the WebView2 SDK.

#define CHECK_FAILURE_STRINGIFY(arg)         #arg
#define CHECK_FAILURE_FILE_LINE(file, line)  ([](HRESULT hr){ CheckFailure(hr, L"Failure at " CHECK_FAILURE_STRINGIFY(file) L"(" CHECK_FAILURE_STRINGIFY(line) L")"); })
#define CHECK_FAILURE                        CHECK_FAILURE_FILE_LINE(__FILE__, __LINE__)
#define CHECK_FAILURE_BOOL(value)            CHECK_FAILURE((value) ? S_OK : E_UNEXPECTED)

void ShowFailure(HRESULT hr, CString const & message)
{
   CString text;
   text.Format(L"%s (0x%08X)", (LPCTSTR)message, hr);

   ::MessageBox(nullptr, static_cast<LPCTSTR>(text), L"Failure", MB_OK);
}

void CheckFailure(HRESULT hr, CString const & message)
{
   if (FAILED(hr))
   {    
      CString text;
      text.Format(L"%s : 0x%08X", (LPCTSTR)message, hr);

      // TODO: log text
     
      std::exit(hr);
   }
}

Try the app

You can download, build, and try the sample app for this series from here: MfcEdgeDemo.zip (755 downloads) .

Stay tuned for the next part of the series.

1 Reply to “Using Microsoft Edge in a native Windows desktop app – part 2”

  1. Thanks, Great Article! In your sample app I had to add CoInitialize before the CreateCoreWebView2EnvironmentWithOptions call, and then I added CoUninitialize at the end of CloseWebView.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.