FullScreenHelper.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. using System;
  2. using System.Runtime.ExceptionServices;
  3. using System.Runtime.InteropServices;
  4. using System.Windows;
  5. using System.Windows.Interop;
  6. using HandyControl.Tools.Interop;
  7. namespace HandyControl.Controls;
  8. /// <summary>
  9. /// 用来使窗口变得全屏的辅助类
  10. /// 采用设置窗口位置和尺寸,确保盖住整个屏幕的方式来实现全屏
  11. /// 目前已知需要满足的条件是:窗口盖住整个屏幕、窗口没有WS_THICKFRAME样式、窗口不能有标题栏且最大化
  12. /// </summary>
  13. internal static class FullScreenHelper
  14. {
  15. /// <summary>
  16. /// 用于记录窗口全屏前位置的附加属性
  17. /// </summary>
  18. private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty =
  19. DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement",
  20. typeof(InteropValues.WINDOWPLACEMENT?), typeof(FullScreenHelper));
  21. /// <summary>
  22. /// 用于记录窗口全屏前样式的附加属性
  23. /// </summary>
  24. private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty =
  25. DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle",
  26. typeof(InteropValues.WindowStyles?), typeof(FullScreenHelper));
  27. /// <summary>
  28. /// 开始进入全屏模式
  29. /// 进入全屏模式后,窗口可通过 API 方式(也可以用 Win + Shift + Left/Right)移动,调整大小,但会根据目标矩形寻找显示器重新调整到全屏状态。
  30. /// 进入全屏后,不要修改样式等窗口属性,在退出时,会恢复到进入前的状态
  31. /// 进入全屏模式后会禁用 DWM 过渡动画
  32. /// </summary>
  33. public static void StartFullScreen(System.Windows.Window window)
  34. {
  35. if (window == null)
  36. {
  37. throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能为 null");
  38. }
  39. //确保不在全屏模式
  40. if (window.GetValue(BeforeFullScreenWindowPlacementProperty) == null &&
  41. window.GetValue(BeforeFullScreenWindowStyleProperty) == null)
  42. {
  43. var hwnd = new WindowInteropHelper(window).EnsureHandle();
  44. var hwndSource = HwndSource.FromHwnd(hwnd);
  45. //获取当前窗口的位置大小状态并保存
  46. var placement = InteropMethods.GetWindowPlacement(hwnd);
  47. window.SetValue(BeforeFullScreenWindowPlacementProperty, placement);
  48. //修改窗口样式
  49. var style = (InteropValues.WindowStyles) InteropMethods.GetWindowLongPtr(hwnd, InteropValues.GWL_STYLE);
  50. window.SetValue(BeforeFullScreenWindowStyleProperty, style);
  51. //将窗口恢复到还原模式,在有标题栏的情况下最大化模式下无法全屏,
  52. //这里采用还原,不修改标题栏的方式
  53. //在退出全屏时,窗口原有的状态会恢复
  54. //去掉WS_THICKFRAME,在有该样式的情况下不能全屏
  55. //去掉WS_MAXIMIZEBOX,禁用最大化,如果最大化会退出全屏
  56. //去掉WS_MAXIMIZE,使窗口变成还原状态,不使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE),避免看到窗口变成还原状态这一过程(也避免影响窗口的Visible状态)
  57. style &= ~(InteropValues.WindowStyles.WS_THICKFRAME | InteropValues.WindowStyles.WS_MAXIMIZEBOX | InteropValues.WindowStyles.WS_MAXIMIZE);
  58. InteropMethods.SetWindowLong(hwnd, InteropValues.GWL_STYLE, (IntPtr) style);
  59. //禁用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理
  60. InteropMethods.DwmSetWindowAttribute(hwnd, InteropValues.DwmWindowAttribute.DWMWA_TRANSITIONS_FORCEDISABLED, 1,
  61. sizeof(int));
  62. //添加Hook,在窗口尺寸位置等要发生变化时,确保全屏
  63. hwndSource.AddHook(KeepFullScreenHook);
  64. if (InteropMethods.GetWindowRect(hwnd, out var rect))
  65. {
  66. //不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。
  67. //使用窗口当前的矩形调用下设置窗口位置和尺寸的方法,让Hook来进行调整窗口位置和尺寸到全屏模式
  68. InteropMethods.SetWindowPos(hwnd, (IntPtr) InteropValues.HWND_TOP, rect.Left, rect.Top, rect.Width,
  69. rect.Height, (int) InteropValues.WindowPositionFlags.SWP_NOZORDER);
  70. }
  71. }
  72. }
  73. /// <summary>
  74. /// 退出全屏模式
  75. /// 窗口会回到进入全屏模式时保存的状态
  76. /// 退出全屏模式后会重新启用 DWM 过渡动画
  77. /// </summary>
  78. public static void EndFullScreen(System.Windows.Window window)
  79. {
  80. if (window == null)
  81. {
  82. throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能为 null");
  83. }
  84. //确保在全屏模式并获取之前保存的状态
  85. if (window.GetValue(BeforeFullScreenWindowPlacementProperty) is InteropValues.WINDOWPLACEMENT placement
  86. && window.GetValue(BeforeFullScreenWindowStyleProperty) is InteropValues.WindowStyles style)
  87. {
  88. var hwnd = new WindowInteropHelper(window).Handle;
  89. if (hwnd == IntPtr.Zero)
  90. {
  91. // 句柄为 0 只有两种情况:
  92. // 1. 虽然窗口已进入全屏,但窗口已被关闭;
  93. // 2. 窗口初始化前,在还没有调用 StartFullScreen 的前提下就调用了此方法。
  94. // 所以,直接 return 就好。
  95. return;
  96. }
  97. var hwndSource = HwndSource.FromHwnd(hwnd);
  98. //去除hook
  99. hwndSource.RemoveHook(KeepFullScreenHook);
  100. //恢复保存的状态
  101. //不要改变Style里的WS_MAXIMIZE,否则会使窗口变成最大化状态,但是尺寸不对
  102. //也不要设置回Style里的WS_MINIMIZE,否则会导致窗口最小化按钮显示成还原按钮
  103. InteropMethods.SetWindowLong(hwnd, InteropValues.GWL_STYLE,
  104. (IntPtr) (style & ~(InteropValues.WindowStyles.WS_MAXIMIZE | InteropValues.WindowStyles.WS_MINIMIZE)));
  105. if ((style & InteropValues.WindowStyles.WS_MINIMIZE) != 0)
  106. {
  107. //如果窗口进入全屏前是最小化的,这里不让窗口恢复到之前的最小化状态,而是到还原的状态。
  108. //大多数情况下,都不期望在退出全屏的时候,恢复到最小化。
  109. placement.showCmd = InteropValues.SW.RESTORE;
  110. }
  111. if ((style & InteropValues.WindowStyles.WS_MAXIMIZE) != 0)
  112. {
  113. //提前调用 ShowWindow 使窗口恢复最大化,若通过 SetWindowPlacement 最大化会导致闪烁,只靠其恢复 RestoreBounds.
  114. InteropMethods.ShowWindow(hwnd, InteropValues.SW.MAXIMIZE);
  115. }
  116. InteropMethods.SetWindowPlacement(hwnd, ref placement);
  117. if ((style & InteropValues.WindowStyles.WS_MAXIMIZE) ==
  118. 0) //如果窗口是最大化就不要修改WPF属性,否则会破坏RestoreBounds,且WPF窗口自身在最大化时,不会修改 Left Top Width Height 属性
  119. {
  120. if (InteropMethods.GetWindowRect(hwnd, out var rect))
  121. {
  122. //不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。
  123. //确保窗口的 WPF 属性与 Win32 位置一致
  124. var logicalPos =
  125. hwndSource.CompositionTarget.TransformFromDevice.Transform(
  126. new Point(rect.Left, rect.Top));
  127. var logicalSize =
  128. hwndSource.CompositionTarget.TransformFromDevice.Transform(
  129. new Point(rect.Width, rect.Height));
  130. window.Left = logicalPos.X;
  131. window.Top = logicalPos.Y;
  132. window.Width = logicalSize.X;
  133. window.Height = logicalSize.Y;
  134. }
  135. }
  136. //重新启用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理
  137. InteropMethods.DwmSetWindowAttribute(hwnd, InteropValues.DwmWindowAttribute.DWMWA_TRANSITIONS_FORCEDISABLED, 0,
  138. sizeof(int));
  139. //删除保存的状态
  140. window.ClearValue(BeforeFullScreenWindowPlacementProperty);
  141. window.ClearValue(BeforeFullScreenWindowStyleProperty);
  142. }
  143. }
  144. /// <summary>
  145. /// 确保窗口全屏的Hook
  146. /// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃
  147. /// </summary>
  148. [HandleProcessCorruptedStateExceptions]
  149. private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
  150. {
  151. //处理WM_WINDOWPOSCHANGING消息
  152. const int WINDOWPOSCHANGING = 0x0046;
  153. if (msg == WINDOWPOSCHANGING)
  154. {
  155. try
  156. {
  157. //得到WINDOWPOS结构体
  158. var pos = (InteropValues.WindowPosition) Marshal.PtrToStructure(lParam, typeof(InteropValues.WindowPosition));
  159. if ((pos.Flags & InteropValues.WindowPositionFlags.SWP_NOMOVE) != 0 &&
  160. (pos.Flags & InteropValues.WindowPositionFlags.SWP_NOSIZE) != 0)
  161. {
  162. //既然你既不改变位置,也不改变尺寸,我就不管了...
  163. return IntPtr.Zero;
  164. }
  165. if (InteropMethods.IsIconic(hwnd))
  166. {
  167. // 如果在全屏期间最小化了窗口,那么忽略后续的位置调整。
  168. // 否则按后续逻辑,会根据窗口在 -32000 的位置,计算出错误的目标位置,然后就跳到主屏了。
  169. return IntPtr.Zero;
  170. }
  171. //获取窗口现在的矩形,下面用来参考计算目标矩形
  172. if (InteropMethods.GetWindowRect(hwnd, out var rect))
  173. {
  174. var targetRect = rect; //窗口想要变化的目标矩形
  175. if ((pos.Flags & InteropValues.WindowPositionFlags.SWP_NOMOVE) == 0)
  176. {
  177. //需要移动
  178. targetRect.Left = pos.X;
  179. targetRect.Top = pos.Y;
  180. }
  181. if ((pos.Flags & InteropValues.WindowPositionFlags.SWP_NOSIZE) == 0)
  182. {
  183. //要改变尺寸
  184. targetRect.Right = targetRect.Left + pos.Width;
  185. targetRect.Bottom = targetRect.Top + pos.Height;
  186. }
  187. else
  188. {
  189. //不改变尺寸
  190. targetRect.Right = targetRect.Left + rect.Width;
  191. targetRect.Bottom = targetRect.Top + rect.Height;
  192. }
  193. //使用目标矩形获取显示器信息
  194. var monitor = InteropMethods.MonitorFromRect(ref targetRect, InteropValues.MONITOR_DEFAULTTOPRIMARY);
  195. var info = new InteropValues.MONITORINFO();
  196. info.cbSize = (uint) Marshal.SizeOf(info);
  197. if (InteropMethods.GetMonitorInfo(monitor, ref info))
  198. {
  199. //基于显示器信息设置窗口尺寸位置
  200. pos.X = info.rcMonitor.Left;
  201. pos.Y = info.rcMonitor.Top;
  202. pos.Width = info.rcMonitor.Right - info.rcMonitor.Left;
  203. pos.Height = info.rcMonitor.Bottom - info.rcMonitor.Top;
  204. pos.Flags &= ~(InteropValues.WindowPositionFlags.SWP_NOSIZE | InteropValues.WindowPositionFlags.SWP_NOMOVE |
  205. InteropValues.WindowPositionFlags.SWP_NOREDRAW);
  206. pos.Flags |= InteropValues.WindowPositionFlags.SWP_NOCOPYBITS;
  207. if (rect == info.rcMonitor)
  208. {
  209. var hwndSource = HwndSource.FromHwnd(hwnd);
  210. if (hwndSource?.RootVisual is Window window)
  211. {
  212. //确保窗口的 WPF 属性与 Win32 位置一致,防止有逗比全屏后改 WPF 的属性,发生一些诡异的行为
  213. //下面这样做其实不太好,会再次触发 WM_WINDOWPOSCHANGING 来着.....但是又没有其他时机了
  214. // WM_WINDOWPOSCHANGED 不能用
  215. //(例如:在进入全屏后,修改 Left 属性,会进入 WM_WINDOWPOSCHANGING,然后在这里将消息里的结构体中的 Left 改回,
  216. // 使对 Left 的修改无效,那么将不会进入 WM_WINDOWPOSCHANGED,窗口尺寸正常,但窗口的 Left 属性值错误。)
  217. var logicalPos =
  218. hwndSource.CompositionTarget.TransformFromDevice.Transform(
  219. new Point(pos.X, pos.Y));
  220. var logicalSize =
  221. hwndSource.CompositionTarget.TransformFromDevice.Transform(
  222. new Point(pos.Width, pos.Height));
  223. window.Left = logicalPos.X;
  224. window.Top = logicalPos.Y;
  225. window.Width = logicalSize.X;
  226. window.Height = logicalSize.Y;
  227. }
  228. }
  229. //将修改后的结构体拷贝回去
  230. Marshal.StructureToPtr(pos, lParam, false);
  231. }
  232. }
  233. }
  234. catch
  235. {
  236. // 这里也不需要日志啥的,只是为了防止上面有逗比逻辑,在消息循环里面炸了
  237. }
  238. }
  239. return IntPtr.Zero;
  240. }
  241. }