本文来告诉大家在 WPF 中,设置窗口全屏化的一个稳定的设置方法。在设置窗口全屏的时候,经常遇到的问题就是应用程序虽然设置最大化加无边框,但是此方式经常会有任务栏冒出来,或者说窗口没有贴屏幕的边。本文的方法是基于 Win32 的,由 lsj 提供的方法,当前已在 1000 多万台设备上稳定运行超过三年时间,只有很少的电脑才偶尔出现任务栏不消失的情况
简单的 WPF 全屏窗口只需设置 WindowStyle 和 WindowState 属性即可,如以下 XAML 代码
<Window ...Title="MainWindow" Height="450" Width="800"WindowStyle="None" WindowState="Maximized">...
</Window>
或如下的后台 cs 代码
Window window = xxx;window.WindowStyle = WindowStyle.None;window.WindowState = WindowState.Maximized;
尽管以上的方法足够简单且大部分情况下行之有效,然而在很多用户的设备上都会常遇到任务栏冒出来,或者说窗口没有贴屏幕的边等问题
本文提供了基于 win32 的稳定方法,经过了大量设备的运行测试,基本可以确认本文的方法是非常稳定的全屏窗口的方法,只有很少的电脑才偶尔出现任务栏不消失的情况。本文的所使用的方法由 lsj 提供,我只是一个记录此技术的工具人
本文的方法核心方式是通过 Hook 的方式获取当前窗口的 Win32 消息,在消息里面获取显示器信息,根据获取显示器信息来设置窗口的尺寸和左上角的值。可以支持在全屏,多屏的设备上稳定设置全屏。支持在全屏之后,窗口可通过 API 方式(也可以用 Win + Shift + Left/Right)移动,调整大小,但会根据目标矩形寻找显示器重新调整到全屏状态
设置全屏在 Windows 的要求就是覆盖屏幕的每个像素,也就是要求窗口盖住整个屏幕、窗口没有WS_THICKFRAME样式、窗口不能有标题栏且最大化
使用本文提供的 FullScreenHelper 类的 StartFullScreen 方法即可进入全屏。进入全屏的窗口必须具备的要求如上文所述,不能有标题栏。如以下的演示例子,设置窗口样式 WindowStyle="None"
如下面代码
<Window x:Class="KenafearcuweYemjecahee.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:KenafearcuweYemjecahee"mc:Ignorable="d" WindowStyle="None"Title="MainWindow" Height="450" Width="800"/>
窗口样式不是强行要求,可以根据自己的业务决定。但如果有窗口样式,那将根据窗口的样式决定全屏的行为。我推荐默认设置为 WindowStyle="None"
用于解决默认的窗口没有贴边的问题
为了演示如何调用全屏方法,我在窗口添加一个按钮,在点击按钮时,在后台代码进入或退出全屏
<ToggleButton HorizontalAlignment="Center" VerticalAlignment="Center" Click="Button_OnClick">全屏</ToggleButton>
以下是点击按钮的逻辑
private void Button_OnClick(object sender, RoutedEventArgs e){var toggleButton = (ToggleButton)sender;if (toggleButton.IsChecked is true){FullScreenHelper.StartFullScreen(this);}else{FullScreenHelper.EndFullScreen(this);}}
本文其实是将原本团队内部的逻辑抄了一次,虽然我能保证团队内的版本是稳定的,但是我不能保证在抄的过程中,我写了一些逗比逻辑,让这个全屏代码不稳定
以下是具体的实现方法,如不想了解细节,那请到本文最后拷贝代码即可。本文的方法已经合入到 https://github.com/HandyOrg/HandyControl 仓库,不想抄代码的伙伴可以直接使用 https://www.nuget.org/packages/HandyControl 库
先来聊聊 StartFullScreen 方法的实现。此方法需要实现让没有全屏的窗口进入全屏,已进入全屏的窗口啥都不做。在窗口退出全屏时,还原进入全屏之前的窗口的状态。为此,设置两个附加属性,用来分别记录窗口全屏前位置和样式的附加属性,在进入全屏窗口的方法尝试获取窗口信息设置到附加属性
/// <summary>/// 用于记录窗口全屏前位置的附加属性/// </summary>private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty =DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement", typeof(WINDOWPLACEMENT?),typeof(Window));/// <summary>/// 用于记录窗口全屏前样式的附加属性/// </summary>private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty =DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle", typeof(WindowStyles?), typeof(Window));public static void StartFullScreen(Window window){//确保不在全屏模式if (window.GetValue(BeforeFullScreenWindowPlacementProperty) == null &&window.GetValue(BeforeFullScreenWindowStyleProperty) == null){var hwnd = new WindowInteropHelper(window).EnsureHandle();var hwndSource = HwndSource.FromHwnd(hwnd);//获取当前窗口的位置大小状态并保存var placement = new WINDOWPLACEMENT();placement.Size = (uint) Marshal.SizeOf(placement);Win32.User32.GetWindowPlacement(hwnd, ref placement);window.SetValue(BeforeFullScreenWindowPlacementProperty, placement);//获取窗口样式var style = (WindowStyles) Win32.User32.GetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE);window.SetValue(BeforeFullScreenWindowStyleProperty, style);}else{// 窗口在全屏,啥都不用做}}
以上代码用到的 Win32 方法和类型定义,都可以在本文最后获取到,在这里就不详细写出
在进入全屏模式时,需要完成的步骤如下
-
需要将窗口恢复到还原模式,在有标题栏的情况下最大化模式下无法全屏。去掉
WS_MAXIMIZE
样式,使窗口变成还原状。不能使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE)
方法,避免看到窗口变成还原状态这一过程,也避免影响窗口的Visible
状态 -
需要去掉
WS_THICKFRAME
样式,在有该样式的情况下不能全屏 -
去掉
WS_MAXIMIZEBOX
样式,禁用最大化,如果最大化会退出全屏
style &= (~(WindowStyles.WS_THICKFRAME | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_MAXIMIZE));Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, (IntPtr) style);
以上写法是 Win32 函数调用的特有方式,习惯就好。在 Win32 的函数设计中,因为当初每个字节都是十分宝贵的,所以恨不得一个字节当成两个来用,这也就是参数为什么通过枚举的二进制方式,看起来很复杂的逻辑设置的原因
全屏的过程,如果有 DWM 动画,将会看到窗口闪烁。因此如果设备上有开启 DWM 那么进行关闭动画。对应的,需要在退出全屏的时候,重新打开 DWM 过渡动画
//禁用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 1,sizeof(int));
接着就是本文的核心逻辑部分,通过 Hook 的方式修改窗口全屏,使用如下代码添加 Hook 用来拿到窗口消息
//添加Hook,在窗口尺寸位置等要发生变化时,确保全屏hwndSource.AddHook(KeepFullScreenHook);private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{// 代码忽略,在下文将告诉大家
}
为了触发 KeepFullScreenHook 方法进行实际的设置窗口全屏,可以通过设置一下窗口的尺寸的方法,如下面代码
if (Win32.User32.GetWindowRect(hwnd, out var rect)){//不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。//使用窗口当前的矩形调用下设置窗口位置和尺寸的方法,让Hook来进行调整窗口位置和尺寸到全屏模式Win32.User32.SetWindowPos(hwnd, (IntPtr) HwndZOrder.HWND_TOP, rect.Left, rect.Top, rect.Width,rect.Height, (int) WindowPositionFlags.SWP_NOZORDER);}
这就是 StartFullScreen 的所有代码
/// <summary>/// 开始进入全屏模式/// 进入全屏模式后,窗口可通过 API 方式(也可以用 Win + Shift + Left/Right)移动,调整大小,但会根据目标矩形寻找显示器重新调整到全屏状态。/// 进入全屏后,不要修改样式等窗口属性,在退出时,会恢复到进入前的状态/// 进入全屏模式后会禁用 DWM 过渡动画/// </summary>/// <param name="window"></param>public static void StartFullScreen(Window window){if (window == null){throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能为 null");}//确保不在全屏模式if (window.GetValue(BeforeFullScreenWindowPlacementProperty) == null &&window.GetValue(BeforeFullScreenWindowStyleProperty) == null){var hwnd = new WindowInteropHelper(window).EnsureHandle();var hwndSource = HwndSource.FromHwnd(hwnd);//获取当前窗口的位置大小状态并保存var placement = new WINDOWPLACEMENT();placement.Size = (uint) Marshal.SizeOf(placement);Win32.User32.GetWindowPlacement(hwnd, ref placement);window.SetValue(BeforeFullScreenWindowPlacementProperty, placement);//修改窗口样式var style = (WindowStyles) Win32.User32.GetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE);window.SetValue(BeforeFullScreenWindowStyleProperty, style);//将窗口恢复到还原模式,在有标题栏的情况下最大化模式下无法全屏,//这里采用还原,不修改标题栏的方式//在退出全屏时,窗口原有的状态会恢复//去掉WS_THICKFRAME,在有该样式的情况下不能全屏//去掉WS_MAXIMIZEBOX,禁用最大化,如果最大化会退出全屏//去掉WS_MAXIMIZE,使窗口变成还原状态,不使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE),避免看到窗口变成还原状态这一过程(也避免影响窗口的Visible状态)style &= (~(WindowStyles.WS_THICKFRAME | WindowStyles.WS_MAXIMIZEBOX | WindowStyles.WS_MAXIMIZE));Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, (IntPtr) style);//禁用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理Win32.Dwmapi.DwmSetWindowAttribute(hwnd, DWMWINDOWATTRIBUTE.DWMWA_TRANSITIONS_FORCEDISABLED, 1,sizeof(int));//添加Hook,在窗口尺寸位置等要发生变化时,确保全屏hwndSource.AddHook(KeepFullScreenHook);if (Win32.User32.GetWindowRect(hwnd, out var rect)){//不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。//使用窗口当前的矩形调用下设置窗口位置和尺寸的方法,让Hook来进行调整窗口位置和尺寸到全屏模式Win32.User32.SetWindowPos(hwnd, (IntPtr) HwndZOrder.HWND_TOP,