如何让WinUI 3(Windows App SDK)窗口始终保持在底层且点击后不回到前台
解决方案:让WinUI 3窗口始终保持在底层且点击不激活
要实现你的需求,核心是阻止窗口被激活,同时维持它在Z序的最底层。因为WinUI 3 Desktop窗口本质是基于Win32 HWND的,我们可以通过Win32 API和消息拦截来实现这个效果。
具体实现步骤
下面是修改后的完整代码,我会逐一解释关键部分:
using Microsoft.UI; using Microsoft.UI.Windowing; using Microsoft.UI.Xaml; using System; using System.Runtime.InteropServices; using WinRT.Interop; namespace Widgets { public sealed partial class MainWindow : Window { private AppWindow m_AppWindow; private IntPtr m_hWnd; // Win32 API 声明 [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex); [DllImport("user32.dll")] private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam); // 常量定义 private const int GWL_EXSTYLE = -20; private const uint WS_EX_NOACTIVATE = 0x08000000; private const int WM_MOUSEACTIVATE = 0x0021; private const int MA_NOACTIVATE = 3; private IntPtr m_prevWndProc; public MainWindow() { this.InitializeComponent(); m_hWnd = WindowNative.GetWindowHandle(this); m_AppWindow = GetAppWindowForCurrentWindow(); // 1. 设置窗口扩展样式,阻止默认激活 SetWindowExNoActivate(); // 2. 拦截窗口消息,处理鼠标点击激活事件 HookWndProc(); // 3. 初始将窗口移至Z序最底层 m_AppWindow.MoveInZOrderAtBottom(); // 4. 监听激活事件,防止意外激活后窗口跑到前台 this.Activated += MainWindow_Activated; } private void MainWindow_Activated(object sender, WindowActivatedEventArgs args) { if (args.WindowActivationState != WindowActivationState.Deactivated) { // 一旦窗口被激活,立即将它移回最底层 m_AppWindow.MoveInZOrderAtBottom(); } } private void SetWindowExNoActivate() { // 获取当前窗口的扩展样式 IntPtr exStyle = GetWindowLongPtr(m_hWnd, GWL_EXSTYLE); // 添加WS_EX_NOACTIVATE样式,告诉系统这个窗口不需要被激活 exStyle = (IntPtr)((uint)exStyle | WS_EX_NOACTIVATE); SetWindowLongPtr(m_hWnd, GWL_EXSTYLE, exStyle); } private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg == WM_MOUSEACTIVATE) { // 当用户点击窗口时,返回MA_NOACTIVATE明确阻止激活 return (IntPtr)MA_NOACTIVATE; } // 其他消息交给原来的窗口处理函数 return CallWindowProc(m_prevWndProc, hWnd, msg, wParam, lParam); } private void HookWndProc() { // 替换窗口的消息处理函数,拦截我们需要处理的消息 m_prevWndProc = SetWindowLongPtr(m_hWnd, -4, Marshal.GetFunctionPointerForDelegate(new WndProcDelegate(WndProc))); } private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); private AppWindow GetAppWindowForCurrentWindow() { WindowId wndId = Win32Interop.GetWindowIdFromWindow(m_hWnd); return AppWindow.GetFromWindowId(wndId); } } }
关键部分解释
WS_EX_NOACTIVATE扩展样式:
这个Win32样式标记窗口为"不需要激活",系统会默认阻止它获得前台焦点,是实现需求的基础。拦截
WM_MOUSEACTIVATE消息:
当用户点击窗口时,系统会发送这个消息询问是否激活窗口。返回MA_NOACTIVATE会明确告诉系统不要激活当前窗口,彻底阻止点击导致的前台跳转。监听
Activated事件:
某些特殊场景下(比如键盘快捷键、其他API调用)窗口可能还是会被激活,这个事件会在窗口激活时触发,我们可以立即调用MoveInZOrderAtBottom()把窗口移回底层。
注意事项
- 如果你的窗口包含输入控件(如TextBox),设置
WS_EX_NOACTIVATE后这些控件将无法获得焦点(因为窗口本身不能被激活)。如果需要控件能接收输入但窗口不激活,需要额外处理WM_SETFOCUS等消息,但这可能不符合你"始终在底层"的核心需求。 - 窗口销毁时建议还原原来的窗口处理函数(调用
SetWindowLongPtr把m_prevWndProc设回去),不过对于普通桌面应用,进程结束时会自动清理,这一步可以省略。
内容的提问来源于stack exchange,提问作者anon




