深入Windows消息循环:手把手教你用Unity拦截WM_SIZING实现自定义窗口控制
深入Windows消息循环手把手教你用Unity拦截WM_SIZING实现自定义窗口控制在Unity开发中窗口管理通常被视为引擎自动处理的黑箱部分。但当我们需要实现特殊窗口效果——比如强制保持16:9比例、自定义标题栏或无边框窗口拖动时就必须深入Windows消息机制的核心层。本文将揭示如何通过拦截WM_SIZING消息在Unity中实现操作系统级的窗口控制。1. Windows消息机制基础1.1 消息循环原理每个Windows应用程序都运行在一个持续处理消息的循环中。当用户调整窗口大小时系统会生成WM_SIZING消息消息代码0x214其中包含窗口新尺寸的矩形区域信息。通过拦截这个消息我们可以修改其参数来实现自定义控制。关键消息类型对照表消息代码含义典型应用场景0x214WM_SIZING窗口大小调整中0x216WM_MOVING窗口移动中0x0112WM_SYSCOMMAND系统菜单命令如最大化1.2 Unity窗口的特殊性Unity生成的Windows窗口默认使用UnityWndClass作为窗口类名。要操作这个窗口我们需要通过EnumThreadWindows枚举当前线程的所有窗口用GetClassName识别Unity窗口获取其窗口句柄HWND[DllImport(user32.dll)] private static extern bool EnumThreadWindows( uint dwThreadId, EnumWindowsProc lpEnumFunc, IntPtr lParam); private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);2. 实现自定义窗口比例控制2.1 窗口过程WindowProc重定向核心步骤是替换默认的窗口处理函数// 定义委托类型 private delegate IntPtr WndProcDelegate( IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); // 替换窗口过程 IntPtr newWndProcPtr Marshal.GetFunctionPointerForDelegate(wndProcDelegate); IntPtr oldWndProcPtr SetWindowLong( unityHWnd, GWLP_WNDPROC, newWndProcPtr);注意32位和64位系统需要使用不同的API函数SetWindowLong32/SetWindowLongPtr642.2 处理WM_SIZING消息在自定义窗口过程中我们需要解析lParam指向的RECT结构根据拖拽方向wParam计算新尺寸应用宽高比约束更新RECT并写回case WM_SIZING: RECT rc (RECT)Marshal.PtrToStructure(lParam, typeof(RECT)); // 计算边框尺寸 RECT windowRect new RECT(); GetWindowRect(hWnd, ref windowRect); RECT clientRect new RECT(); GetClientRect(hWnd, ref clientRect); int borderWidth windowRect.Right - windowRect.Left - (clientRect.Right - clientRect.Left); int borderHeight windowRect.Bottom - windowRect.Top - (clientRect.Bottom - clientRect.Top); // 应用宽高比计算 rc.Right rc.Left Mathf.RoundToInt( (rc.Bottom - rc.Top - borderHeight) * aspect) borderWidth; Marshal.StructureToPtr(rc, lParam, true); break;3. 高级窗口控制技巧3.1 全屏模式处理在全屏切换时需要特殊处理void Update() { if (Screen.fullScreen !wasFullscreenLastFrame) { // 计算带黑边的全屏分辨率 bool needHorizontalBars aspect (float)Screen.currentResolution.width / Screen.currentResolution.height; int width needHorizontalBars ? Mathf.RoundToInt(Screen.currentResolution.height * aspect) : Screen.currentResolution.width; int height needHorizontalBars ? Screen.currentResolution.height : Mathf.RoundToInt(Screen.currentResolution.width / aspect); Screen.SetResolution(width, height, true); } wasFullscreenLastFrame Screen.fullScreen; }3.2 最小/最大尺寸限制在Inspector中暴露参数[SerializeField] private int minWidthPixel 640; [SerializeField] private int maxWidthPixel 1920;在消息处理中应用限制int newWidth Mathf.Clamp( rc.Right - rc.Left, minWidthPixel, maxWidthPixel); int newHeight Mathf.Clamp( rc.Bottom - rc.Top, minHeightPixel, maxHeightPixel);4. 实战无边框窗口拖动通过扩展消息处理可以实现无边框窗口的拖动功能4.1 拦截非客户区消息private const int WM_NCHITTEST 0x0084; private const int HTCLIENT 1; private const int HTCAPTION 2; IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg WM_NCHITTEST) { // 将客户端区域点击视为标题栏 IntPtr result CallWindowProc( oldWndProcPtr, hWnd, msg, wParam, lParam); if (result (IntPtr)HTCLIENT) return (IntPtr)HTCAPTION; return result; } // ...其他消息处理 }4.2 自定义标题栏实现结合UI元素可以实现现代化标题栏创建Canvas覆盖窗口顶部添加关闭/最小化按钮通过WM_SYSCOMMAND处理按钮点击private const int SC_CLOSE 0xF060; private const int SC_MINIMIZE 0xF020; if (msg WM_SYSCOMMAND) { switch (wParam.ToInt32() 0xFFF0) { case SC_CLOSE: Application.Quit(); break; case SC_MINIMIZE: ShowWindow(hWnd, 2); // SW_MINIMIZE break; } }5. 性能优化与错误处理5.1 安全的回调清理在退出时必须恢复原始窗口过程private bool ApplicationWantsToQuit() { if (!started) return false; StartCoroutine(DelayedQuit()); return false; } IEnumerator DelayedQuit() { SetWindowLong(unityHWnd, GWLP_WNDPROC, oldWndProcPtr); yield return new WaitForEndOfFrame(); Application.Quit(); }5.2 编辑器兼容性处理在Unity编辑器中需要特殊处理#if !UNITY_EDITOR // 仅在实际构建中挂钩窗口过程 oldWndProcPtr SetWindowLong(unityHWnd, GWLP_WNDPROC, newWndProcPtr); #endif实现这种底层控制时最棘手的部分是处理不同Windows版本间的API差异。在实际项目中建议添加详细的日志记录特别是在窗口过程回调中这能帮助快速定位消息处理问题。