WinUI 3设置最小窗口尺寸:巧用WM_GETMINMAXINFO消息
2025-04-08 20:11:22
WinUI 3:巧用窗口消息(WM_GETMINMAXINFO)设置最小窗口尺寸
搞 WinUI 3 开发的时候,想给窗口设个最小尺寸限制,防止用户拖得太小导致界面乱掉?这看起来是个挺基本的需求。要是以前搞 Win32 或者 MFC,直接在窗口过程(WndProc)里处理 WM_GETMINMAXINFO
消息就行了。但到了 WinUI 3,这事儿好像就没那么直观了,特别是对于刚接触 WinUI 和 C++/WinRT 的朋友,很容易搞不清楚代码该往哪儿放,以及怎么让它生效。
不少人(包括提问者)可能搜到一些 Win32 的代码片段,比如自定义 wWinMain
、注册窗口类、写 WndProc
函数处理消息等等。代码本身可能看得懂,但问题来了:这些代码到底应该塞进 WinUI 3 项目的哪个文件里? App.xaml.cpp
?MainWindow.xaml.cpp
?还是别的什么地方?怎么让 WinUI 应用跑起来的时候,我们写的消息处理逻辑能被调用到?
甚至有人尝试修改预处理器定义(比如加 DISABLE_XAML_GENERATED_MAIN
)和链接器设置(指定 wWinMain
为入口点),结果发现窗口最小尺寸限制还是没生效。感觉像是缺了点啥,或者路子走歪了。
别急,咱们来捋一捋。
一、为啥直接套用 Win32 代码模板会“失效”?
问题的关键在于,WinUI 3(特别是打包或未打包的桌面应用)虽然底层确实是跑在一个 Win32 窗口上的,但它的应用程序生命周期、窗口创建和消息循环很大程度上被框架接管了。
你如果在 App.xaml.cpp
里搞一个独立的 wWinMain
和 WndProc
,像下面这样:
// 你的 App.xaml.cpp 里尝试的代码(简化版)
int WINAPI wWinMain(HINSTANCE hInstance, /*...*/)
{
// ... WinRT 初始化 ...
// ... 启动 XAML Application ...
Application::Start(...); // <-- WinUI 框架创建主窗口并运行自己的消息循环
// --- 你添加的 Win32 代码 ---
WNDCLASSEX wc = { /*...*/ };
wc.lpfnWndProc = MySeparateWndProc; // 一个独立的窗口过程
RegisterClassEx(&wc);
// 创建一个全新的、独立的 Win32 窗口
HWND hWndManual = CreateWindowEx( /*...*/ );
ShowWindow(hWndManual, nCmdShow);
// 运行一个独立的 Win32 消息循环
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0)) { /*...*/ }
// --- 结束 ---
return 0;
}
LRESULT CALLBACK MySeparateWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// 这个 WndProc 只处理上面 CreateWindowEx 创建的那个 hWndManual 窗口的消息
switch (uMsg)
{
case WM_GETMINMAXINFO:
// 这里设置的最小尺寸只对 hWndManual 有效
MINMAXINFO* mmi = (MINMAXINFO*)lParam;
mmi->ptMinTrackSize.x = 800; // 比如设个 800x600
mmi->ptMinTrackSize.y = 600;
return 0;
// ... 其他消息处理 ...
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
看到问题了吗?
你写的 wWinMain
虽然启动了 WinUI 应用 (Application::Start
),但随后又用 CreateWindowEx
创建了一个 完全不同 的 Win32 窗口 (hWndManual
),并且给它配了个独立的窗口过程 (MySeparateWndProc
) 和消息循环。
你在 MySeparateWndProc
里处理 WM_GETMINMAXINFO
,只会影响到那个用 CreateWindowEx
手动创建出来的、光秃秃的 hWndManual
窗口 ,跟 WinUI 框架创建和管理的那个承载着你所有 XAML 控件的主窗口(MainWindow
)一点关系都没有 ! WinUI 主窗口的消息路由根本没走你写的 MySeparateWndProc
。
这就是为什么你感觉代码被“忽略”了——它确实在运行,但作用在了错误的对象上。
结论: 不要试图用自定义 wWinMain
去“替换”WinUI 的启动流程,也不要自己创建独立的 Win32 窗口和消息循环来处理主窗口逻辑。那样只会把事情搞复杂,还达不到目的。同时,之前尝试修改的 DISABLE_XAML_GENERATED_MAIN
预处理器定义和 wWinMain
链接器入口点设置,对于解决这个问题也是不需要 的,可以改回默认设置。
二、正解:挂钩(Subclassing)WinUI 主窗口
既然 WinUI 主窗口本质上也是个 Win32 窗口(HWND),那我们真正的目标应该是:
- 获取 到 WinUI 主窗口的句柄(HWND)。
- 挂钩 (Subclass)这个窗口的消息处理过程,插入我们自己的逻辑来响应
WM_GETMINMAXINFO
消息。
这样,当系统向 WinUI 主窗口发送 WM_GETMINMAXINFO
消息时,我们的代码就能先一步截获并处理它,设置好最小尺寸,然后再把消息交给原来的处理程序继续处理。
下面是具体步骤:
1. 获取主窗口的 HWND
WinUI 3 提供了标准方法来从 Microsoft::UI::Xaml::Window
对象获取其底层的 HWND。你需要用到 winrt::Microsoft::UI::GetWindowIdFromWindow
和 winrt::Microsoft::UI::GetWindowFromWindowId
这两个函数。
// 通常在 MainWindow 的某个时刻(比如构造函数或 Loaded 事件后)执行
// assuming 'window' is your winrt::Microsoft::UI::Xaml::Window object (e.g., your MainWindow instance)
// Get the WindowId
auto windowId = winrt::Microsoft::UI::GetWindowIdFromWindow(window);
// Get the HWND using the WindowId
HWND hWnd = winrt::Microsoft::UI::GetWindowFromWindowId(windowId);
if (hWnd == NULL)
{
// 错误处理:获取 HWND 失败
// 可能窗口还没完全初始化好?
return;
}
// 拿到了 hWnd,可以进行下一步了!
代码放哪儿? 获取 HWND 的操作,以及接下来的挂钩操作,最合适的地方是主窗口 (MainWindow
) 自己的代码文件 (MainWindow.xaml.cpp
) 中。通常可以在 MainWindow
的构造函数 或者窗口加载完成 (比如 Loaded
事件)之后进行,确保此时窗口句柄已经有效。放在 App.xaml.cpp
里不太合适,因为 App
类通常不直接持有主窗口的 HWND,且生命周期也可能不匹配。
2. 使用 SetWindowSubclass 进行挂钩
获取到 HWND
后,推荐使用 SetWindowSubclass
函数(需要 CommCtrl.h
头文件,并链接 Comctl32.lib
)来挂钩窗口过程。这是比老旧的 SetWindowLongPtr(hWnd, GWLP_WNDPROC, ...)
更现代、更安全的方式,因为它更容易管理多个挂钩,并且能方便地调用下一个挂钩或原始窗口过程。
你需要:
- 定义一个回调函数 (Subclass Procedure),它的签名类似
WndProc
,用来处理你关心的消息。 - 调用
SetWindowSubclass
把你的回调函数关联到目标窗口 (hWnd
)。
// 在 MainWindow.xaml.h 或 .cpp 里
#include <CommCtrl.h> // 需要包含这个头文件
#pragma comment(lib, "Comctl32.lib") // 链接库
// 声明/定义你的 Subclass 回调函数
LRESULT CALLBACK MainWindowSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
// 在 MainWindow 的构造函数或 Loaded 事件处理函数中
// ... 获取 hWnd ...
if (hWnd)
{
// 设置挂钩
// uIdSubclass: 子类 ID,随便给个唯一的就行
// dwRefData: 可以传个自定义数据,比如 MainWindow 的指针,方便在回调里访问成员
BOOL result = SetWindowSubclass(
hWnd, // 目标窗口句柄
MainWindowSubclassProc, // 你的回调函数指针
1, // 子类 ID (任意唯一 UINT_PTR 值)
reinterpret_cast<DWORD_PTR>(this) // 把 this 指针传过去 (可选)
);
if (!result)
{
// 挂钩失败处理
DWORD error = GetLastError();
// log or handle error...
}
}
3. 实现 Subclass 回调函数
现在,来实现 MainWindowSubclassProc
。它会接收到发往主窗口的所有消息。我们只关心 WM_GETMINMAXINFO
。
// MainWindow.xaml.cpp
// 引入必要的头文件
#include <windowsx.h> // 为了 MINMAXINFO 结构
// Subclass 回调函数的实现
LRESULT CALLBACK MainWindowSubclassProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
UINT_PTR uIdSubclass, // 之前 SetWindowSubclass 传的 ID
DWORD_PTR dwRefData) // 之前 SetWindowSubclass 传的 this 指针 (如果传了)
{
// 可以通过 dwRefData 获取 MainWindow 实例指针 (如果需要访问成员)
// auto pThis = reinterpret_cast<winrt::MyProject::implementation::MainWindow*>(dwRefData);
switch (uMsg)
{
case WM_GETMINMAXINFO:
{
// 这就是我们要处理的消息!
MINMAXINFO* pMinMaxInfo = reinterpret_cast<MINMAXINFO*>(lParam);
// 设置最小跟踪尺寸 (用户能拖到的最小尺寸)
pMinMaxInfo->ptMinTrackSize.x = 800; // 你想要的最小宽度
pMinMaxInfo->ptMinTrackSize.y = 600; // 你想要的最小高度
// 注意:这里设置的是 ptMinTrackSize
// 你也可以根据需要设置 ptMaxTrackSize (最大尺寸)
// pMinMaxInfo->ptMaxTrackSize.x = ...;
// pMinMaxInfo->ptMaxTrackSize.y = ...;
// 甚至可以设置窗口最大化时的尺寸和位置 (ptMaxSize, ptMaxPosition)
// 返回 0 表示我们处理了此消息
return 0;
}
case WM_NCDESTROY: // 窗口即将销毁
{
// **重要:** 在窗口销毁前,必须移除我们的挂钩!
// 否则之后可能会导致程序崩溃
RemoveWindowSubclass(hWnd, MainWindowSubclassProc, uIdSubclass);
// 然后把消息交给下一个挂钩或原始窗口过程处理
// DefSubclassProc 会做这件事
break; // 注意这里是 break,让 DefSubclassProc 来处理后续
}
}
// 对于所有我们不处理的消息,或者处理后还需要默认行为的消息 (如 WM_NCDESTROY)
// **必须** 调用 DefSubclassProc 将消息传递给下一个挂钩或原始窗口过程
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
关键点解释:
- 处理
WM_GETMINMAXINFO
: 当这个消息来的时候,lParam
是一个指向MINMAXINFO
结构的指针。我们修改这个结构里的ptMinTrackSize
成员,就能设定窗口拖动时的最小宽度和高度。修改完后直接return 0;
,表示我们已经处理完毕,不需要系统再进行默认处理了。 - 移除挂钩 (
RemoveWindowSubclass
): 这是非常重要的一步!窗口关闭销毁时(比如收到WM_NCDESTROY
消息时),一定要调用RemoveWindowSubclass
把我们的挂钩移除掉。否则,窗口对象销毁后,如果系统再试图调用我们的回调函数,就会访问无效内存,导致程序崩溃。 - 调用
DefSubclassProc
: 对于我们不关心的消息,或者像WM_NCDESTROY
这样处理完还需要默认行为的消息,必须调用DefSubclassProc
。它负责把消息传递给链条中的下一个挂钩,或者最终传给原始的窗口过程,保证窗口的其他功能正常运作。
4. 整合到 MainWindow 类
现在把这些代码片段整合到 MainWindow
类中。
MainWindow.xaml.h:
#pragma once
#include "MainWindow.g.h"
#include <Windows.h> // For HWND
#include <CommCtrl.h> // For Subclassing APIs
namespace winrt::MyProject::implementation // 替换成你自己的项目命名空间
{
// 前向声明 Subclass 回调函数
LRESULT CALLBACK MainWindowSubclassProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam, UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
struct MainWindow : MainWindowT<MainWindow>
{
MainWindow();
// 你可能还有其他的成员函数和事件处理程序...
// 可以在这里添加一个成员变量来保存 HWND (如果多处需要)
// HWND m_hWnd{ nullptr };
private:
// 可以在窗口关闭时触发的事件处理函数中移除挂钩
void Window_Closed(winrt::Windows::Foundation::IInspectable const& sender, winrt::Microsoft::UI::Xaml::WindowEventArgs const& args);
winrt::event_token m_closedToken; // 保存事件 token 用于注销
// 如果需要从 SubclassProc 访问 MainWindow 成员,可以用这个
static const UINT_PTR SUBCLASS_ID = 1;
};
}
namespace winrt::MyProject::factory_implementation // 替换成你自己的项目命名空间
{
struct MainWindow : MainWindowT<MainWindow, implementation::MainWindow>
{
};
}
MainWindow.xaml.cpp:
#include "pch.h"
#include "MainWindow.xaml.h"
#if __has_include("MainWindow.g.cpp")
#include "MainWindow.g.cpp"
#endif
#include <winrt/Microsoft.UI.Interop.h> // For GetWindowIdFromWindow, GetWindowFromWindowId
#include <windowsx.h> // For MINMAXINFO
#pragma comment(lib, "Comctl32.lib") // 链接 Comctl32
using namespace winrt;
using namespace Microsoft::UI::Xaml;
// Note: Gdiplus Sample requires Desktop Bridge or unpackaged WinUI apps
// Ensure you have the correct project type or configuration for HWND interop
namespace winrt::MyProject::implementation // 替换成你自己的项目命名空间
{
MainWindow::MainWindow()
{
InitializeComponent();
// 获取 HWND
auto windowNative{ this->try_as<::IWindowNative>() };
winrt::check_bool(windowNative); // 确保接口查询成功
HWND hWnd{ nullptr };
windowNative->get_WindowHandle(&hWnd);
if (hWnd)
{
// m_hWnd = hWnd; // 如果有成员变量,在这里赋值
// 设置窗口挂钩
// 注意:将 this 指针作为 dwRefData 传递
BOOL result = SetWindowSubclass(
hWnd,
MainWindowSubclassProc,
SUBCLASS_ID, // 使用在 .h 中定义的 ID
reinterpret_cast<DWORD_PTR>(this) // 传递 this 指针
);
if (!result)
{
// 挂钩失败,可以记录日志或抛出异常
DWORD error = GetLastError();
// Example: winrt::throw_hresult(HRESULT_FROM_WIN32(error));
}
}
else
{
// 获取 HWND 失败
// 可能是在构造函数里太早了?可以考虑移到 Loaded 事件里
}
// 注册 Closed 事件处理程序,用于移除挂钩
m_closedToken = this->Closed({ this, &MainWindow::Window_Closed });
}
// Subclass 回调函数实现
LRESULT CALLBACK MainWindowSubclassProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam,
UINT_PTR uIdSubclass,
DWORD_PTR dwRefData)
{
// 从 dwRefData 恢复 MainWindow 指针 (如果需要访问成员)
MainWindow* pThis = reinterpret_cast<MainWindow*>(dwRefData);
switch (uMsg)
{
case WM_GETMINMAXINFO:
{
MINMAXINFO* pMinMaxInfo = reinterpret_cast<MINMAXINFO*>(lParam);
pMinMaxInfo->ptMinTrackSize.x = 800; // 设置最小宽度
pMinMaxInfo->ptMinTrackSize.y = 600; // 设置最小高度
return 0; // 已处理
}
case WM_NCDESTROY:
{
// 移除挂钩
// 注意: 这里用之前传递的 pThis 调用 RemoveWindowSubclass 是安全的
// 因为 WM_NCDESTROY 是在对象销毁流程中的一部分
RemoveWindowSubclass(hWnd, MainWindowSubclassProc, uIdSubclass);
// 不要在这里 `return 0;`,让 DefSubclassProc 处理后续
break;
}
}
// 其他消息交给默认处理
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
// 窗口关闭事件处理函数
void MainWindow::Window_Closed(IInspectable const&, WindowEventArgs const&)
{
// 理论上,在 WM_NCDESTROY 里移除挂钩是最好的时机。
// 但作为备用或补充,也可以在这里尝试移除,尽管可能窗口句柄已失效。
// auto windowNative{ this->try_as<::IWindowNative>() };
// HWND hWnd{ nullptr };
// if (windowNative) windowNative->get_WindowHandle(&hWnd);
// if (hWnd) {
// RemoveWindowSubclass(hWnd, MainWindowSubclassProc, SUBCLASS_ID);
// }
// 注销 Closed 事件
this->Closed(m_closedToken);
}
// ... 其他 MainWindow 的方法 ...
}
注意: 获取 HWND 的方式 (try_as<::IWindowNative>()
) 是 WinUI 3 中推荐的标准方式。确保你的项目包含了必要的 Microsoft.WindowsAppSDK
引用。
5. 小结一下关键点
- 忘记
wWinMain
: 不要用自定义wWinMain
来处理 WinUI 主窗口的消息。让 WinUI 框架自己管理启动和主消息循环。 - 找对地方: 获取 HWND 和设置/移除 Subclass 的代码应该放在主窗口类 (
MainWindow.xaml.cpp
和.h
) 中,通常在构造函数或Loaded
事件里设置,在WM_NCDESTROY
消息(通过 SubclassProc 捕获)或Closed
事件里移除。 - 用 Subclassing:
SetWindowSubclass
是挂钩现有窗口消息处理的安全方式。 - 处理
WM_GETMINMAXINFO
: 在 Subclass 回调函数里拦截此消息,修改MINMAXINFO
结构设置最小尺寸,然后返回 0。 - 别忘
DefSubclassProc
: 对于不处理的消息,或者处理完需要默认行为的消息,一定要调用DefSubclassProc
。 - 记得
RemoveWindowSubclass
: 在窗口销毁前(WM_NCDESTROY
时)必须移除挂钩,防止崩溃。 - 检查头文件和库: 确保包含了
CommCtrl.h
,windowsx.h
,winrt/Microsoft.UI.Interop.h
,并链接了Comctl32.lib
。 - 项目配置: 不需要
DISABLE_XAML_GENERATED_MAIN
和自定义wWinMain
入口点。使用标准的 WinUI 3 项目模板设置即可。
现在,重新编译运行你的 WinUI 3 应用,尝试拖动窗口边缘,应该就能看到设置的最小尺寸限制生效了。