PAYATHON 2026

Microsoft WebView2 中如何处理 window.resizeTo

排障赵师傅

结论

WebView2 目前没有公开的 window.resizeTo 回调,也不会将这类请求转换为宿主可以处理的 Win32 消息。

调用 NewWindowRequested 和 put_NewWindow 后,WebView2 只负责将新页面交给指定的 ICoreWebView2。弹出窗口的顶层 HWND、窗口边框和尺寸仍由宿主管理。页面调用 window.resizeTo 时,WebView2 不会自动对宿主窗口执行 SetWindowPos。

这里的原因也不是新窗口被识别成了 tab。put_NewWindow 只会建立 WebView2 内容之间的 opener/new-window 关系,宿主创建的 HWND 不会因此成为由 Edge 管理的浏览器弹窗。

两类尺寸请求需要分别处理:

  • 对于 window.open 指定的初始尺寸,读取 ICoreWebView2WindowFeatures。
  • 对于页面运行期间发起的 window.resizeTo,通过 Web Message 通知 Win32 宿主,由宿主调整顶层窗口。

为什么收不到特殊的窗口消息

如果没有处理 NewWindowRequested,Edge Runtime 会自行创建并管理弹出窗口。它可以在浏览器内部接收 JavaScript 的尺寸调整请求,再调用窗口系统修改窗口大小。因此,Spy++ 最终能观察到 WM_WINDOWPOSCHANGING、WM_SIZE 等消息。

调用 put_NewWindow 后,顶层窗口由应用创建。WebView2 不拥有该窗口,也无法确定页面尺寸应该如何对应窗口边框、标题栏、DPI 和显示器工作区,因此不会代替宿主调用 SetWindowPos。

WM_WINDOWPOSCHANGING 是窗口尺寸发生变化后产生的消息,并非 WebView2 用来传递 window.resizeTo 请求的接口。

在 Edge 自行创建的弹窗中,window.chrome.webview 为 undefined。这是因为该窗口并非应用托管的 WebView2 实例,没有向宿主发送 Web Message 的桥接对象。

处理 window.open 的初始尺寸

假设页面通过以下方式打开窗口:

window.open("/popup", "popup", "width=800,height=600,left=100,top=100");

可以在 NewWindowRequested 事件中获取 ICoreWebView2WindowFeatures。其中,Width、Height、Left、Top 以及对应的 HasSize、HasPosition 表示创建窗口时请求的初始位置和尺寸。

示意代码如下:

wil::com_ptr<ICoreWebView2WindowFeatures> features;
args->get_WindowFeatures(&features);

BOOL hasSize = FALSE;
BOOL hasPosition = FALSE;

features->get_HasSize(&hasSize);
features->get_HasPosition(&hasPosition);

UINT32 width = 0;
UINT32 height = 0;
UINT32 left = 0;
UINT32 top = 0;

if (hasSize)
{
    features->get_Width(&width);
    features->get_Height(&height);
}

if (hasPosition)
{
    features->get_Left(&left);
    features->get_Top(&top);
}

这些属性只记录 window.open 传入的窗口特征,无法监听页面随后执行的 window.resizeTo。

如果新 WebView2 采用异步方式创建,应先从事件参数中获取 deferral。等新 controller 和 ICoreWebView2 创建完成后,再调用 put_NewWindow、put_Handled(TRUE) 和 Complete()。新旧 WebView2 还需满足 put_NewWindow 对环境和初始化状态的要求,例如通常需要使用同一个 WebView2 Environment。

处理运行期间的尺寸请求

如果页面由应用控制,可以定义一个明确的宿主接口:

function requestHostResize(width, height) {
    if (!window.chrome?.webview) {
        return;
    }

    window.chrome.webview.postMessage({
        type: "resizeTo",
        width,
        height
    });
}

Win32 端监听 WebMessageReceived,验证消息后调整承载该 WebView2 的顶层窗口:

webView->add_WebMessageReceived(
    Callback<ICoreWebView2WebMessageReceivedEventHandler>(
        [popupHwnd](
            ICoreWebView2* sender,
            ICoreWebView2WebMessageReceivedEventArgs* args) -> HRESULT
        {
            wil::unique_cotaskmem_string source;
            RETURN_IF_FAILED(args->get_Source(&source));

            wil::unique_cotaskmem_string json;
            RETURN_IF_FAILED(args->get_WebMessageAsJson(&json));

            // ParseResizeRequest 是应用自己的 JSON 解析和校验函数。
            int cssWidth = 0;
            int cssHeight = 0;
            if (!ParseResizeRequest(json.get(), cssWidth, cssHeight))
                return S_OK;

            // 设置合理上限和下限,避免页面请求异常尺寸。
            cssWidth = std::clamp(cssWidth, 320, 3840);
            cssHeight = std::clamp(cssHeight, 240, 2160);

            const UINT dpi = GetDpiForWindow(popupHwnd);
            const int pixelWidth = MulDiv(cssWidth, dpi, 96);
            const int pixelHeight = MulDiv(cssHeight, dpi, 96);

            SetWindowPos(
                popupHwnd,
                nullptr,
                0,
                0,
                pixelWidth,
                pixelHeight,
                SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);

            return S_OK;
        }).Get(),
    &webMessageToken);

示例中的 ParseResizeRequest 需要检查以下内容:

  • type 是否等于 resizeTo。
  • width 和 height 是否为有限的正数。
  • 数值是否过大、过小或格式异常。
  • args->get_Source() 返回的消息来源是否可信,只有受信任的页面才能调整窗口。

如果传入的尺寸指客户区大小,需要根据窗口样式调用 AdjustWindowRectExForDpi,将其换算为整个顶层窗口的大小:

RECT rect{ 0, 0, clientPixelWidth, clientPixelHeight };

AdjustWindowRectExForDpi(
    &rect,
    static_cast<DWORD>(GetWindowLongPtr(popupHwnd, GWL_STYLE)),
    FALSE,
    static_cast<DWORD>(GetWindowLongPtr(popupHwnd, GWL_EXSTYLE)),
    GetDpiForWindow(popupHwnd));

SetWindowPos(
    popupHwnd,
    nullptr,
    0,
    0,
    rect.right - rect.left,
    rect.bottom - rect.top,
    SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);

window.resizeTo 的参数表示窗口外部尺寸。为了兼容现有网页的语义,应直接调整顶层窗口大小。只有应用自定义接口明确约定参数表示客户区尺寸时,才需要调用 AdjustWindowRectExForDpi。

兼容已有的 window.resizeTo 调用

如果无法逐处将页面中的调用改为 requestHostResize,可以在文档创建前注入兼容脚本,把 window.resizeTo 转发给宿主:

(() => {
    const originalResizeTo = window.resizeTo.bind(window);

    window.resizeTo = (width, height) => {
        if (window.chrome?.webview) {
            window.chrome.webview.postMessage({
                type: "resizeTo",
                width,
                height
            });
            return;
        }

        originalResizeTo(width, height);
    };
})();

宿主可以通过 AddScriptToExecuteOnDocumentCreated 注入该脚本。通常,脚本会在页面自身的 JavaScript 运行前安装。

这种方式可以作为兼容层,但有以下限制:

  • 页面可能提前保存原始函数的引用,也可能在之后重新定义 window.resizeTo。
  • 第三方 frame 未必能访问顶层页面的 WebView2 桥接对象。
  • 页面可能按照浏览器行为检查 outerWidth、outerHeight,这些值要等宿主完成调整后才能反映实际尺寸。
  • 不应允许不受信任的内容随意移动宿主窗口或调整其大小。

页面由应用维护时,显式调用 requestHostResize 更容易控制,也便于处理失败、尺寸限制和多显示器 DPI。

关于 popup 和 tab 的判断

put_NewWindow 没有提供将新 WebView2 标记为 popup 或 tab 的设置。网页是否符合浏览器对 resizeTo 的限制,属于浏览器内部策略,WebView2 没有公开相应的分类查询或强制切换接口。

新页面即使在 JavaScript 层保留了 window.opener,WebView2 也无法据此调整外部 Win32 窗口。限制主要来自宿主对窗口的所有权,不能只归因于 popup/tab 分类。

无需通过 Spy++ 寻找未公开的通知消息,也不建议模拟 Edge 的内部窗口行为。创建窗口时,从 ICoreWebView2WindowFeatures 获取初始尺寸;页面运行期间,则通过 Web Message 将尺寸请求交给宿主,再调用 SetWindowPos 调整窗口。

备注:内容仅供参考。