本文来告诉大家在 WPF 中,设置窗口全屏化的一个稳定的设置方法。在设置窗口全屏的时候,经常遇到的问题就是应用程序虽然设置最大化加无边框,但是此方式经常会有任务栏冒出来,或者说窗口没有贴屏幕的边。本文的方法是基于 Win32 的,由 lsj 提供的方法,当前已在 1000 多万台设备上稳定运行超过三年时间,只有很少的电脑才偶尔出现任务栏不消失的情况
简单的 WPF 全屏窗口只需设置 WindowStyle 和 WindowState 属性即可,如以下 XAML 代码
或如下的后台 cs 代码
尽管以上的方法足够简单且大部分情况下行之有效,然而在很多用户的设备上都会常遇到任务栏冒出来,或者说窗口没有贴屏幕的边等问题
本文提供了基于 win32 的稳定方法,经过了大量设备的运行测试,基本可以确认本文的方法是非常稳定的全屏窗口的方法,只有很少的电脑才偶尔出现任务栏不消失的情况。本文的所使用的方法由 lsj 提供,我只是一个记录此技术的工具人
本文的方法核心方式是通过 Hook 的方式获取当前窗口的 Win32 消息,在消息里面获取显示器信息,根据获取显示器信息来设置窗口的尺寸和左上角的值。可以支持在全屏,多屏的设备上稳定设置全屏。支持在全屏之后,窗口可通过 API 方式(也可以用 Win + Shift + Left/Right)移动,调整大小,但会根据目标矩形寻找显示器重新调整到全屏状态
设置全屏在 Windows 的要求就是覆盖屏幕的每个像素,也就是要求窗口盖住整个屏幕、窗口没有WS_THICKFRAME样式、窗口不能有标题栏且最大化
使用本文提供的 FullScreenHelper 类的 StartFullScreen 方法即可进入全屏。进入全屏的窗口必须具备的要求如上文所述,不能有标题栏。如以下的演示例子,设置窗口样式 WindowStyle="None"
如下面代码
窗口样式不是强行要求,可以根据自己的业务决定。但如果有窗口样式,那将根据窗口的样式决定全屏的行为。我推荐默认设置为 WindowStyle="None"
用于解决默认的窗口没有贴边的问题
为了演示如何调用全屏方法,我在窗口添加一个按钮,在点击按钮时,在后台代码进入或退出全屏
以下是点击按钮的逻辑
本文其实是将原本团队内部的逻辑抄了一次,虽然我能保证团队内的版本是稳定的,但是我不能保证在抄的过程中,我写了一些逗比逻辑,让这个全屏代码不稳定
以下是具体的实现方法,如不想了解细节,那请到本文最后拷贝代码即可。本文的方法已经合入到 https://github.com/HandyOrg/HandyControl 仓库,不想抄代码的伙伴可以直接使用 https://www.nuget.org/packages/HandyControl 库
先来聊聊 StartFullScreen 方法的实现。此方法需要实现让没有全屏的窗口进入全屏,已进入全屏的窗口啥都不做。在窗口退出全屏时,还原进入全屏之前的窗口的状态。为此,设置两个附加属性,用来分别记录窗口全屏前位置和样式的附加属性,在进入全屏窗口的方法尝试获取窗口信息设置到附加属性
以上代码用到的 Win32 方法和类型定义,都可以在本文最后获取到,在这里就不详细写出
在进入全屏模式时,需要完成的步骤如下
-
需要将窗口恢复到还原模式,在有标题栏的情况下最大化模式下无法全屏。去掉
WS_MAXIMIZE
样式,使窗口变成还原状。不能使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE)
方法,避免看到窗口变成还原状态这一过程,也避免影响窗口的Visible
状态 -
需要去掉
WS_THICKFRAME
样式,在有该样式的情况下不能全屏 -
去掉
WS_MAXIMIZEBOX
样式,禁用最大化,如果最大化会退出全屏
以上写法是 Win32 函数调用的特有方式,习惯就好。在 Win32 的函数设计中,因为当初每个字节都是十分宝贵的,所以恨不得一个字节当成两个来用,这也就是参数为什么通过枚举的二进制方式,看起来很复杂的逻辑设置的原因
全屏的过程,如果有 DWM 动画,将会看到窗口闪烁。因此如果设备上有开启 DWM 那么进行关闭动画。对应的,需要在退出全屏的时候,重新打开 DWM 过渡动画
接着就是本文的核心逻辑部分,通过 Hook 的方式修改窗口全屏,使用如下代码添加 Hook 用来拿到窗口消息
为了触发 KeepFullScreenHook 方法进行实际的设置窗口全屏,可以通过设置一下窗口的尺寸的方法,如下面代码
这就是 StartFullScreen 的所有代码
在 KeepFullScreenHook 方法就是核心的逻辑,通过收到 Win 消息,判断是 WM_WINDOWPOSCHANGING
消息,获取当前屏幕范围,设置给窗口
此方法会用到一些 Win32 的内存访问,虽然以上代码在实际测试中和在实际的用户设备上运行没有发现问题,但是当时在写的时候,为了防止访问内存过程中因为一些致命异常导致程序崩溃,就加上了 HandleProcessCorruptedStateExceptions
特性。在 dotnet core 下,此 HandleProcessCorruptedStateExceptionsAttribute 特性已失效。详细请看 升级到 dotnet core 之后 HandleProcessCorruptedStateExceptions 无法接住异常
按照 Win32 消息的定义,可以先获取WINDOWPOS结构体
通过 IsIconic 方法判断当前窗口是否被最小化,如果最小化也不做全屏
如果在最小化也做全屏,将会因为最小化的窗口的 Y 坐标在 -32000 的位置,在全屏的设备上,如果是在副屏最小化的,将会计算出错误的目标位置,然后就跳到主屏了
获取窗口的现在的矩形,用来计算窗口所在显示器信息,然后将显示器的范围设置给窗口
这就是在 Hook 里面的逻辑,接下来看退出全屏的方法
在退出全屏需要设置为窗口进入全屏之前的样式等信息
下面是 FullScreenHelper 的核心代码,此类型依赖一些 Win32 方法的定义,这部分我就不在博客中写出,大家可以从本文最后获取所有源代码
不嫌弃麻烦的话,还请自行下载代码,自己构建。可以通过如下方式获取本文的源代码,先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源
获取代码之后,进入 KenafearcuweYemjecahee 文件夹
特别感谢 lsj 提供的逻辑
通过 lsj 阅读 Avalonia 的逻辑,找到了 ITaskbarList2::MarkFullscreenWindow 方法,通过此方式可以通知任务栏不要显示到最顶,以下是我测试的行为
当调用 ITaskbarList2::MarkFullscreenWindow 方法设置给到某个窗口时,如此窗口处于激活状态,此窗口所在的屏幕的任务栏将不会置顶,任务栏将会在其他窗口下方。这里的其他窗口指的是任意的窗口,即任务栏不再具备最顶层的特性。换句话说就是这个方法不会辅助窗口本身进入全屏,仅仅只是用于处理任务栏在全屏窗口的行为,这也符合 ITaskbarList 接口的含义。而至于设置给到的某个窗口,此窗口是否真的全屏,那 MarkFullscreenWindow 方法也管不了了,也就是说即使设置给一个普通的非全屏的窗口,甚至非最大化的窗口,也是可以的
先编写简单的代码,用于测试 ITaskbarList2::MarkFullscreenWindow 的行为
先定义 ITaskbarList2 这个 COM 接口,代码如下
以上代码里面的 InterfaceType 特性是必须的,需要加上 InterfaceIsIUnknown 参数。因为根据官方文档的如下描述可知道 ITaskbarList2 是继承 ITaskbarList 的,而 ITaskbarList 是继承 IUnknown 的
The ITaskbarList2 interface inherits from ITaskbarList. ITaskbarList2 also has these types of members The ITaskbarList interface inherits from the IUnknown interface.
在 dotnet 里面,需要标记 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
特性,否则将会缺失 IUnknown 的默认几个方法,导致实际 C# 代码调用的代码非预期,可能导致进程炸掉
以上代码里面,咱需要关注使用的只有 MarkFullscreenWindow 方法。为了更好的进行测试,接下来编辑 MainWindow.xaml 添加一个按钮,用于点击时进入或退出全屏模式,即调用 MarkFullscreenWindow 方法时,传入的 fFullscreen
参数的值
编辑后台代码,实现 Button_OnClick
功能
以上的 FullScreenHelper.MarkFullscreenWindowTaskbarList 封装方法的实现如下
完成以上代码运行的界面如下,可以看到这是一个非全屏也非最大化的窗口
以上代码放在 github 和 gitee 上,可以使用如下命令行拉取代码
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码
获取代码之后,进入 KenafearcuweYemjecahee 文件夹,即可获取到源代码
接下来可以做一个测试实现,测试其行为
- 启动进程窗口,即此窗口为主窗口,拖动主窗口在任务栏位置。 此时可见任务栏在主窗口上方
- 点击
全屏
按钮,此时可见主窗口在任务栏上方,即任务栏在主窗口下方不会挡住主窗口 - 启动记事本,拿到记事本窗口。此时可见主窗口失去焦点,显示在任务栏下方,即任务栏挡住主窗口。此时拖动记事本窗口在任务栏位置,再点击激活主窗口,让主窗口获取焦点,可见任务栏显示在最下方,即任务栏在主窗口和记事本窗口下方
通过以上行为测试,大概可以知道,此 MarkFullscreenWindow 方法的作用只是处理任务栏是否在最顶层而已。只要设置给到 MarkFullscreenWindow 的句柄的窗口处于激活获取焦点状态,那么任务栏就不会处于最顶层,将可能处于其他窗口的下方,即使其他窗口没有调用 MarkFullscreenWindow 方法。因为此时完全就是靠窗口层级处理
另外 MarkFullscreenWindow 方法也没有真的判断传入的窗口句柄对应的窗口是否真的处于全屏状态,仅仅只是判断传入的窗口句柄对应处于激活获取焦点时就将任务栏设置为非最顶层模式而已
估计在微软底层实现是为了规避一些坑而作出如此诡异的行为。在此行为之下反而可以用在某些有趣的情况下,让任务栏不要处于最顶层,和是否全屏需求可能没有强关系。但此方法也可以更好的处理全屏窗口时,任务栏冒出来的问题
欢迎大家获取我的代码进行更多的测试
在双屏设备下的 MarkFullscreenWindow 方法就更有趣了,简单说就是双屏模式下 MarkFullscreenWindow 只影响主窗口所在的屏幕的任务栏的状态,另一个屏幕不受影响
在有双屏的设备上可以继续上述测试行为,即上述测试行为在屏幕1上进行,现在还有屏幕2另一个屏幕
- 记原本启动的记事本窗口为记事本1窗口,在屏幕1 启动新的记事本,获取记事本2窗口。此时主窗口自然丢失焦点,前台窗口为刚启动的记事本2窗口。任务栏在最上层,即任务栏盖住主窗口
- 拖动记事本2窗口,从屏幕1 拖动到屏幕2 上,且沿着任务栏拖动。可见当记事本2窗口拖动到屏幕2 时,屏幕1 的任务栏回到主窗口下方,即屏幕1 的任务栏没有挡住主窗口和记事本1窗口。再将记事本2窗口从屏幕2 拖回屏幕1 上,可见当记事本2窗口拖回屏幕1 时,屏幕1 的任务栏回到了最顶层状态,即使任务栏盖住主窗口和两个记事本的窗口
- 将记事本2窗口拖到屏幕2 上,点击屏幕1 的主窗口,让屏幕1 的主窗口获取焦点。此时符合预期的是主窗口在任务栏之上,任务栏没有处于最顶层状态。接着再点击屏幕2 的记事本2窗口,让记事本2窗口获取焦点激活作为前台窗口。此时可见屏幕1 的任务栏依旧处于非最上层状态,即主窗口在任务栏之上,任务栏没有挡住主窗口。在以上过程中,屏幕2 的任务栏都是保持最上层,即会挡住记事本2窗口。再将主窗口从屏幕1 拖动到屏幕2 上,可以看到当主窗口从屏幕1 拖动到屏幕2 时,屏幕1 的任务栏处于最顶层状态,可以挡住记事本1窗口,屏幕2 的任务栏没有处于最顶层状态,在记事本2窗口下方
通过以上的测试可以看到,在 MarkFullscreenWindow 方法的判断,其实只是判断当前屏幕的激活顺序最高的窗口是否设置了 MarkFullscreenWindow 方法。如果是则让此屏幕的任务栏处于非最顶层的模式,相对来说多个屏幕下的逻辑会更加的复杂,从这个方面也能想象微软在这个方法实现上有多少坑
基于 MarkFullscreenWindow 的机制,优化 FullScreenHelper 的代码,优化之后的代码如下
以上代码放在 github 和 gitee 上,可以使用如下命令行拉取代码
先创建一个空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码
以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码,将 gitee 源换成 github 源进行拉取代码
获取代码之后,进入 KenafearcuweYemjecahee 文件夹,即可获取到源代码
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。 欢迎转载、使用、重新发布,但务必保留文章署名 林德熙 (包含链接: https://blog.lindexi.com ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我 联系。