传统的方法集成 WebView2 是把 WebView2 当作一个子窗口,嵌入到你创建的窗口中。
这种方式有一些局限性:
第一:虽然你可以在一个窗口中集成多个 WebView2 子窗口,但一旦涉及到 WebView2 子窗口覆盖,那么你就很难控制它们之间的层级。
这个问题应该是有解决方案的,但想来解决方案一定很绕,后来我就没做更多尝试。
第二:如果你让 WebView2 子窗口覆盖父窗口的整个工作区,那么父窗口就接不到鼠标事件了,就算你想用 WM_NCHITTEST 来处理窗口拖拽改变大小和位置都不行
这个问题也有解决方案,就是你在 WebView2 的页面中接收用户的点击和拖拽事件,在把消息发送到 Host 进程,在由 Host 进程管控父窗口的大小和位置即可,总之也很绕。
使用 UI 合成器(UI::Composition)的方案集成 WebView2 则没有上述这些问题。
UI 合成器(UI::Composition)会把 WebView2 渲染的内容合成到你的窗口上,你的窗口不需要再管理子窗口句柄(HWND)。
这样你就可以对 WebView2 渲染的内容施加各种操作,比如:旋转、缩放、透明度控制、模糊等,甚至可以与其他 Composition 对象无缝融合。
这是微软的官方示例,中间区域是 WebView 合成到父窗口的内容。左下角和右下角两个矩形覆盖了 WebView 的界面。
官方示例源码地址:https://github.com/MicrosoftEdge/WebView2Samples
说实话,示例代码作者想尽可能的向读者展示各种可能性,所以把代码搞得挺复杂得,可读性比较差,我这里给大家写个简单得版本。
首先创建合成器:
#include <WebView2.h>
#include <WebView2EnvironmentOptions.h>
#include <wrl.h>
#include <wil/com.h>
#include <DispatcherQueue.h>
#include <winrt/base.h>
#include <winrt/Windows.Foundation.h>
#include <windows.ui.composition.interop.h>
#include <winrt/Windows.UI.Composition.Desktop.h>
using namespace Microsoft;
using namespace winrt::Windows;
UI::Composition::Compositor compositor{nullptr};
DispatcherQueueOptions options{
sizeof(DispatcherQueueOptions), /* dwSize */
DQTYPE_THREAD_CURRENT, /* threadType */
DQTAT_COM_ASTA /* apartmentType */
};
static System::DispatcherQueueController dispatchCtrl{nullptr};
CreateDispatcherQueueController(options, reinterpret_cast<ABI::Windows::System::IDispatcherQueueController**>(winrt::put_abi(dispatchCtrl)));
compositor = winrt::Windows::UI::Composition::Compositor();
这里 dispatchCtrl 可以是全局单例,但 compositor 必须是线程内单例。
接着,在窗口内创建具备 UI 合成能力的 WebView2 控件:
auto ctrlReadyCB = WRL::Callback<ICoreWebView2CreateCoreWebView2CompositionControllerCompletedHandler>(this, &BrowserWindow::ctrlReady);
auto env3 = App::get()->env.try_query<ICoreWebView2Environment3>();
auto result = env3->CreateCoreWebView2CompositionController(hwnd,ctrlReadyCB.Get());
控件创建成功后,把控件内容合成到窗口上:
wil::com_ptr<ICoreWebView2Controller> ctrl;
wil::com_ptr<ICoreWebView2CompositionController> ctrlComp;
UI::Composition::Desktop::DesktopWindowTarget m_target{ nullptr };
UI::Composition::ContainerVisual m_rootVisual{ nullptr };
UI::Composition::ContainerVisual m_webViewVisual{ nullptr };
HRESULT BrowserWindow::ctrlReady(HRESULT result, ICoreWebView2CompositionController* ctrlComp)
{
this->ctrlComp = ctrlComp;
this->ctrl = this->ctrlComp.query<ICoreWebView2Controller>();
ctrl->put_IsVisible(true);
auto app = App::get();
//这是前面创建的compositor
compositor.as<ABI::Windows::UI::Composition::Desktop::ICompositorDesktopInterop>();
interop->CreateDesktopWindowTarget(hwnd, false, reinterpret_cast<ABI::Windows::UI::Composition::Desktop::IDesktopWindowTarget**>(winrt::put_abi(m_target)));
m_rootVisual = app->compositor.CreateContainerVisual();
m_rootVisual.RelativeSizeAdjustment({ 1.0f, 1.0f });
m_rootVisual.Offset({ 0, 0, 0 });
m_target.Root(m_rootVisual);
m_webViewVisual = app->compositor.CreateContainerVisual();
m_rootVisual.Children().InsertAtTop(m_webViewVisual);
this->ctrlComp->put_RootVisualTarget(m_webViewVisual.as<IUnknown>().get());
RECT bounds;
GetClientRect(hwnd, &bounds);
ctrl->put_Bounds(bounds);
loadPage(); //这个方法负责让webview2加载页面,具体逻辑就不多说了
return S_OK;
}
至此,你的窗口就成功以 UI 合成器(UI::Composition)方式集成了 WebView2。
然而此时你的页面接收不到任何鼠标事件,我们需要主动把窗口的鼠标事件转发给它。
使用子窗口的方式集成 WebView2 就不需要开发者操心这些,因为子窗口内的 WebView2 会自动接收并处理用户的鼠标事件。
先来看如何把窗口的鼠标事件转发到 WebView2 中:
else if (msg >= WM_MOUSEFIRST && msg <= WM_MOUSELAST)
{
routeMsgToPage(msg, wParam, lParam);
return true;
}
WM_MOUSEFIRST 和 WM_MOUSELAST 两个消息之间,包含所有的鼠标事件类型(这是Windows API 定义的)
void BrowserWindow::routeMsgToPage(UINT msg, WPARAM wParam, LPARAM lParam)
{
if (!ctrlComp) return;
DWORD mouseData = 0;
POINT point{ .x{GET_X_LPARAM(lParam)},.y{GET_Y_LPARAM(lParam)} };
//todo 鼠标按下 释放时 SetCapture ReleaseCapture
if (msg == WM_MOUSEMOVE)
{
if (!isMouseTracking) {
TRACKMOUSEEVENT tme = { sizeof(TRACKMOUSEEVENT) };
tme.dwFlags = TME_LEAVE;
tme.hwndTrack = hwnd;
TrackMouseEvent(&tme);
isMouseTracking = true;
}
}
else if (msg == WM_MOUSEWHEEL || msg == WM_MOUSEHWHEEL)
{
mouseData = GET_WHEEL_DELTA_WPARAM(wParam);
ScreenToClient(hwnd, &point);
}
else if (msg == WM_XBUTTONDBLCLK || msg == WM_XBUTTONDOWN || msg == WM_XBUTTONUP) { //前进后退之类的按钮
mouseData = GET_XBUTTON_WPARAM(wParam);
}
auto eventKind = static_cast<COREWEBVIEW2_MOUSE_EVENT_KIND>(msg);
auto eventKey = static_cast<COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS>(GET_KEYSTATE_WPARAM(wParam));
ctrlComp->SendMouseInput(eventKind, eventKey, mouseData, point);
}
在上面的方法中,我处理了鼠标移动、点击、滚轮滚动消息。最后通过 ctrlComp 的SendMouseInput 方法转发给了 WebView2 控件。
很显然,这是有损耗的,比子窗口集成 WebView2 要略慢一些,但用户感觉不出来。
完成上述工作后,你还得接收 WebView2 鼠标光标变化事件,让窗口变化鼠标光标:
// 你可以在加载页面前注册此事件
EventRegistrationToken token;
auto cursorChangeCB = WRL::Callback<ICoreWebView2CursorChangedEventHandler>(this, &BrowserWindow::cursorChange);
this->ctrlComp->add_CursorChanged(cursorChangeCB.Get(), &token);
事件的回调函数:
HRESULT BrowserWindow::cursorChange(ICoreWebView2CompositionController*, IUnknown*)
{
HCURSOR cursor = nullptr;
HRESULT hr = this->ctrlComp->get_Cursor(&cursor);
if (SUCCEEDED(hr) && cursor)
{
SetCursor(cursor);
}
return S_OK;
}
上述代码中 SetCursor 方法会触发 WM_SETCURSOR 消息,处理 WM_SETCURSOR 消息的代码如下所示
else if (msg == WM_SETCURSOR) {
if (LOWORD(lParam) != HTCLIENT) return 0;
if (!ctrlComp) return 0;
HCURSOR cursor = nullptr;
auto hr = ctrlComp->get_Cursor(&cursor);
if (FAILED(hr)) return false;
if (!cursor) return 0;
SetCursor(cursor);
return true;
}
以上这些就是关键代码,如果你想深入了解,那么可以关注我这个项目:
https://github.com/xland/HorseJs
这里包含所有的代码。