You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让WPF窗口始终置于全屏应用(如游戏)之上?

如何让WPF覆盖层始终显示在独占全屏应用上方?

直接能用的解决办法

要搞定全屏游戏下的覆盖层置顶问题,核心是监听窗口切换事件,每次切换到全屏应用时强制重置覆盖层的层级,同时调整窗口样式避免WPF透明特性带来的冲突。

修改后的C#代码

public partial class MainView : Window
{
    public const int WS_EX_TRANSPARENT = 0x00000020;
    public const int WS_EX_LAYERED = 0x00080000;
    public const int GWL_EXSTYLE = (-20);
    public const int SWP_NOMOVE = 0x0002;
    public const int SWP_NOSIZE = 0x0001;
    public const int HWND_TOPMOST = -1;

    [DllImport("user32.dll")]
    public static extern int GetWindowLong(IntPtr hwnd, int index);

    [DllImport("user32.dll")]
    public static extern int SetWindowLong(IntPtr hwnd, int index, int newStyle);

    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

    [DllImport("user32.dll")]
    private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventDelegate lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);

    [DllImport("user32.dll")]
    private static extern bool UnhookWinEvent(IntPtr hWinEventHook);

    private delegate void WinEventDelegate(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime);

    private IntPtr _eventHook;

    public MainView(IServiceProvider provider)
    {
        InitializeComponent();
        Loaded += (sender, args) =>
        {
            IntPtr hwnd = new WindowInteropHelper(this).Handle;
            
            // 用分层窗口+透明样式替代WPF的AllowsTransparency,保留原生层级支持
            int extendedStyle = GetWindowLong(hwnd, GWL_EXSTYLE);
            SetWindowLong(hwnd, GWL_EXSTYLE, extendedStyle | WS_EX_TRANSPARENT | WS_EX_LAYERED);
            
            // 初始强制把窗口钉在最顶层
            SetWindowPos(hwnd, (IntPtr)HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
            
            // 监听前台窗口切换事件,一旦切到全屏应用就重新置顶
            _eventHook = SetWinEventHook(0x0003, 0x0003, IntPtr.Zero, new WinEventDelegate(OnForegroundWindowChanged), 0, 0, 0x0001);
        };
        Closing += (sender, args) =>
        {
            // 程序关闭时取消钩子,避免内存泄漏
            UnhookWinEvent(_eventHook);
        };
        DataContext = provider.GetRequiredService<MainViewModel>();
    }

    private void OnForegroundWindowChanged(IntPtr hWinEventHook, uint eventType, IntPtr hwnd, int idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
    {
        // 只处理窗口级别的切换事件
        if (idObject != 0 || idChild != 0)
            return;
            
        // 检查当前前台窗口是否为全屏应用
        if (IsFullScreenWindow(hwnd))
        {
            IntPtr overlayHwnd = new WindowInteropHelper(this).Handle;
            // 强制把覆盖层重新钉到最顶层
            SetWindowPos(overlayHwnd, (IntPtr)HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
            // 某些场景下可尝试先取消置顶再重新设置:
            // SetWindowPos(overlayHwnd, (IntPtr)(-2), 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
            // SetWindowPos(overlayHwnd, (IntPtr)HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
        }
    }

    private bool IsFullScreenWindow(IntPtr hwnd)
    {
        if (hwnd == IntPtr.Zero)
            return false;
            
        GetWindowRect(hwnd, out RECT rect);
        Screen screen = Screen.FromHandle(hwnd);
        // 判断窗口是否占满整个屏幕
        return rect.Left == screen.Bounds.Left && rect.Top == screen.Bounds.Top &&
               rect.Right == screen.Bounds.Right && rect.Bottom == screen.Bounds.Bottom;
    }

    [DllImport("user32.dll")]
    private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    private struct RECT
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }
}

修改后的XAML代码

<Window
        ...
        WindowStyle="None" ResizeMode="NoResize"
        Left="1670" Top="0" d:DataContext="{d:DesignInstance viewModels:MainViewModel}"
        mc:Ignorable="d" Topmost="True"
        Title="MainView" Height="450" Width="250">
    <!-- 移除AllowsTransparency,用Grid的半透明背景实现效果 -->
    <Grid Background="#40DarkGray">
        <ContentControl Content="{Binding CurrentViewModel}"/>
    </Grid>
</Window>

为啥原来的方案失效?

  1. 独占全屏应用的层级优先级:全屏游戏会接管整个显示输出,系统会将其窗口层级设为最高,WPF自带的Topmost=True无法突破这个限制。
  2. AllowsTransparency的坑:开启该属性后,WPF会采用软件渲染模式,脱离系统原生窗口层级管理,导致置顶设置失效。

Steam/Discord等Overlay的实现逻辑

这类专业Overlay并非使用普通窗口,而是采用注入游戏进程+Hook DirectX/OpenGL渲染管线的方式:

  • 直接在游戏的渲染流程中绘制Overlay内容,相当于和游戏画面处于同一渲染层级。
  • 这种方式完全不受窗口层级限制,能完美适配全屏独占模式,但需要处理不同游戏引擎的兼容性,实现复杂度远高于普通窗口方案。

内容的提问来源于stack exchange,提问作者Artur S.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 02:48:19