This article has been updated for the new version of WebView2 that requires Microsoft Edge 82.0.488.0 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.
- Part 1: Introduction to Edge and WebView2
- Part 2: Creating a WebView2 component
- Part 3: Navigation and other events
The API overview
The WebView2 SDK contains the following APIs:
- Global functions, such as CreateCoreWebView2EnvironmentWithOptions() that creates a WebView2 environment with a custom version of Edge, user data directory and/or additional options, GetAvailableCoreWebView2BrowserVersionString() that retrieves the browser version (including channel name), or CompareBrowserVersion() that compares browser version to determine which version is newer, older or same.
- Interfaces, such as ICoreWebView2Environment that represents the WebView2 environment, ICoreWebView2EnvironmentOptions that defines options used to create WebView2 Environment, ICoreWebView2 that represents the actual WebView2 control, ICoreWebView2Controller that is the owner of the CoreWebView2 object, and provides support for resizing, showing and hiding, focusing, and other functionality related to windowing and composition, ICoreWebView2Settings that defines properties that enable, disable, or modify WebView features.
- Delegate interfaces, such as ICoreWebView2NavigationStartingEventHandler and ICoreWebView2NavigationCompletedEventHandler.
- Event argument interfaces, such as ICoreWebView2NavigationStartingEventArgs and ICoreWebView2NavigationCompletedEventArgs.
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
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); static CString GetInstallPath(); 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
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:
- 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.
- Register a callback function to be invoked when the web view has been created.
- 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 browser was not found. This function can be implemented as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
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; } void CWebBrowser::NavigateTo(CString url) { if (url.Find(L"://") < 0) { if (url.GetLength() > 1 && url[1] == ':') url = L"file://" + url; else url = L"http://" + url; } m_pImpl->m_webView->Navigate(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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
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 Edge 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. This is the case even though Edge 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\Edge\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 79.0.309.71.
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 Edge 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 Edge.
- Search on disk in the default installation location for a folder name of the form 79.0.309.71.
In the attached source code, you will find the following implementation for this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
CString CWebBrowser::GetInstallPath() { static CString path = [] { auto installPath = GetInstallPathFromRegistry(); if (installPath.IsEmpty()) installPath = GetInstallPathFromDisk(); return installPath; }(); return path; } CString CWebBrowser::GetInstallPathFromRegistry() { CString path; HKEY handle = nullptr; auto 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() { CString path = LR"(c:\Program Files (x86)\Microsoft\Edge\Application\)"; 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#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 (535 downloads) .
Stay tuned for the next part of the series.
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.