使用 UI 合成器(UI::Composition)集成 WebView2

传统的方法集成 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

这里包含所有的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

liulun

如果文章真帮到了你,谢谢您打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值