返回

WinUI 3设置最小窗口尺寸:巧用WM_GETMINMAXINFO消息

windows

WinUI 3:巧用窗口消息(WM_GETMINMAXINFO)设置最小窗口尺寸

搞 WinUI 3 开发的时候,想给窗口设个最小尺寸限制,防止用户拖得太小导致界面乱掉?这看起来是个挺基本的需求。要是以前搞 Win32 或者 MFC,直接在窗口过程(WndProc)里处理 WM_GETMINMAXINFO 消息就行了。但到了 WinUI 3,这事儿好像就没那么直观了,特别是对于刚接触 WinUI 和 C++/WinRT 的朋友,很容易搞不清楚代码该往哪儿放,以及怎么让它生效。

不少人(包括提问者)可能搜到一些 Win32 的代码片段,比如自定义 wWinMain、注册窗口类、写 WndProc 函数处理消息等等。代码本身可能看得懂,但问题来了:这些代码到底应该塞进 WinUI 3 项目的哪个文件里? App.xaml.cppMainWindow.xaml.cpp?还是别的什么地方?怎么让 WinUI 应用跑起来的时候,我们写的消息处理逻辑能被调用到?

甚至有人尝试修改预处理器定义(比如加 DISABLE_XAML_GENERATED_MAIN)和链接器设置(指定 wWinMain 为入口点),结果发现窗口最小尺寸限制还是没生效。感觉像是缺了点啥,或者路子走歪了。

别急,咱们来捋一捋。

一、为啥直接套用 Win32 代码模板会“失效”?

问题的关键在于,WinUI 3(特别是打包或未打包的桌面应用)虽然底层确实是跑在一个 Win32 窗口上的,但它的应用程序生命周期、窗口创建和消息循环很大程度上被框架接管了。

你如果在 App.xaml.cpp 里搞一个独立的 wWinMainWndProc,像下面这样:

// 你的 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),那我们真正的目标应该是:

  1. 获取 到 WinUI 主窗口的句柄(HWND)。
  2. 挂钩 (Subclass)这个窗口的消息处理过程,插入我们自己的逻辑来响应 WM_GETMINMAXINFO 消息。

这样,当系统向 WinUI 主窗口发送 WM_GETMINMAXINFO 消息时,我们的代码就能先一步截获并处理它,设置好最小尺寸,然后再把消息交给原来的处理程序继续处理。

下面是具体步骤:

1. 获取主窗口的 HWND

WinUI 3 提供了标准方法来从 Microsoft::UI::Xaml::Window 对象获取其底层的 HWND。你需要用到 winrt::Microsoft::UI::GetWindowIdFromWindowwinrt::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 应用,尝试拖动窗口边缘,应该就能看到设置的最小尺寸限制生效了。