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 调整窗口。
备注:内容仅供参考。
