You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何让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);
        }
    }
}

关键部分解释

  1. WS_EX_NOACTIVATE 扩展样式
    这个Win32样式标记窗口为"不需要激活",系统会默认阻止它获得前台焦点,是实现需求的基础。

  2. 拦截 WM_MOUSEACTIVATE 消息
    当用户点击窗口时,系统会发送这个消息询问是否激活窗口。返回MA_NOACTIVATE会明确告诉系统不要激活当前窗口,彻底阻止点击导致的前台跳转。

  3. 监听 Activated 事件
    某些特殊场景下(比如键盘快捷键、其他API调用)窗口可能还是会被激活,这个事件会在窗口激活时触发,我们可以立即调用MoveInZOrderAtBottom()把窗口移回底层。

注意事项

  • 如果你的窗口包含输入控件(如TextBox),设置WS_EX_NOACTIVATE后这些控件将无法获得焦点(因为窗口本身不能被激活)。如果需要控件能接收输入但窗口不激活,需要额外处理WM_SETFOCUS等消息,但这可能不符合你"始终在底层"的核心需求。
  • 窗口销毁时建议还原原来的窗口处理函数(调用SetWindowLongPtrm_prevWndProc设回去),不过对于普通桌面应用,进程结束时会自动清理,这一步可以省略。

内容的提问来源于stack exchange,提问作者anon

火山引擎 最新活动