diff --git a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems index f0e8721a..9f73d850 100644 --- a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems +++ b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems @@ -85,6 +85,7 @@ + diff --git a/src/Shared/HandyControl_Shared/Tools/Helper/FullScreenHelper.cs b/src/Shared/HandyControl_Shared/Tools/Helper/FullScreenHelper.cs new file mode 100644 index 00000000..c216c20a --- /dev/null +++ b/src/Shared/HandyControl_Shared/Tools/Helper/FullScreenHelper.cs @@ -0,0 +1,277 @@ +using System; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Interop; +using HandyControl.Tools.Interop; + +namespace HandyControl.Controls +{ + /// + /// 用来使窗口变得全屏的辅助类 + /// 采用设置窗口位置和尺寸,确保盖住整个屏幕的方式来实现全屏 + /// 目前已知需要满足的条件是:窗口盖住整个屏幕、窗口没有WS_THICKFRAME样式、窗口不能有标题栏且最大化 + /// + internal static class FullScreenHelper + { + /// + /// 用于记录窗口全屏前位置的附加属性 + /// + private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty = + DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement", + typeof(InteropValues.WINDOWPLACEMENT?), typeof(FullScreenHelper)); + + /// + /// 用于记录窗口全屏前样式的附加属性 + /// + private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty = + DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle", + typeof(InteropValues.WindowStyles?), typeof(FullScreenHelper)); + + /// + /// 开始进入全屏模式 + /// 进入全屏模式后,窗口可通过 API 方式(也可以用 Win + Shift + Left/Right)移动,调整大小,但会根据目标矩形寻找显示器重新调整到全屏状态。 + /// 进入全屏后,不要修改样式等窗口属性,在退出时,会恢复到进入前的状态 + /// 进入全屏模式后会禁用 DWM 过渡动画 + /// + public static void StartFullScreen(System.Windows.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 = InteropMethods.GetWindowPlacement(hwnd); + window.SetValue(BeforeFullScreenWindowPlacementProperty, placement); + + //修改窗口样式 + var style = (InteropValues.WindowStyles) InteropMethods.GetWindowLongPtr(hwnd, InteropValues.GWL_STYLE); + window.SetValue(BeforeFullScreenWindowStyleProperty, style); + //将窗口恢复到还原模式,在有标题栏的情况下最大化模式下无法全屏, + //这里采用还原,不修改标题栏的方式 + //在退出全屏时,窗口原有的状态会恢复 + //去掉WS_THICKFRAME,在有该样式的情况下不能全屏 + //去掉WS_MAXIMIZEBOX,禁用最大化,如果最大化会退出全屏 + //去掉WS_MAXIMIZE,使窗口变成还原状态,不使用ShowWindow(hwnd, ShowWindowCommands.SW_RESTORE),避免看到窗口变成还原状态这一过程(也避免影响窗口的Visible状态) + style &= ~(InteropValues.WindowStyles.WS_THICKFRAME | InteropValues.WindowStyles.WS_MAXIMIZEBOX | InteropValues.WindowStyles.WS_MAXIMIZE); + InteropMethods.SetWindowLong(hwnd, InteropValues.GWL_STYLE, (IntPtr) style); + + //禁用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理 + InteropMethods.DwmSetWindowAttribute(hwnd, InteropValues.DwmWindowAttribute.DWMWA_TRANSITIONS_FORCEDISABLED, 1, + sizeof(int)); + + //添加Hook,在窗口尺寸位置等要发生变化时,确保全屏 + hwndSource.AddHook(KeepFullScreenHook); + + if (InteropMethods.GetWindowRect(hwnd, out var rect)) + { + //不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。 + + //使用窗口当前的矩形调用下设置窗口位置和尺寸的方法,让Hook来进行调整窗口位置和尺寸到全屏模式 + InteropMethods.SetWindowPos(hwnd, (IntPtr) InteropValues.HWND_TOP, rect.Left, rect.Top, rect.Width, + rect.Height, (int) InteropValues.WindowPositionFlags.SWP_NOZORDER); + } + } + } + + /// + /// 退出全屏模式 + /// 窗口会回到进入全屏模式时保存的状态 + /// 退出全屏模式后会重新启用 DWM 过渡动画 + /// + public static void EndFullScreen(System.Windows.Window window) + { + if (window == null) + { + throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能为 null"); + } + + //确保在全屏模式并获取之前保存的状态 + if (window.GetValue(BeforeFullScreenWindowPlacementProperty) is InteropValues.WINDOWPLACEMENT placement + && window.GetValue(BeforeFullScreenWindowStyleProperty) is InteropValues.WindowStyles style) + { + var hwnd = new WindowInteropHelper(window).Handle; + + if (hwnd == IntPtr.Zero) + { + // 句柄为 0 只有两种情况: + // 1. 虽然窗口已进入全屏,但窗口已被关闭; + // 2. 窗口初始化前,在还没有调用 StartFullScreen 的前提下就调用了此方法。 + // 所以,直接 return 就好。 + return; + } + + + var hwndSource = HwndSource.FromHwnd(hwnd); + + //去除hook + hwndSource.RemoveHook(KeepFullScreenHook); + + //恢复保存的状态 + //不要改变Style里的WS_MAXIMIZE,否则会使窗口变成最大化状态,但是尺寸不对 + //也不要设置回Style里的WS_MINIMIZE,否则会导致窗口最小化按钮显示成还原按钮 + InteropMethods.SetWindowLong(hwnd, InteropValues.GWL_STYLE, + (IntPtr) (style & ~(InteropValues.WindowStyles.WS_MAXIMIZE | InteropValues.WindowStyles.WS_MINIMIZE))); + + if ((style & InteropValues.WindowStyles.WS_MINIMIZE) != 0) + { + //如果窗口进入全屏前是最小化的,这里不让窗口恢复到之前的最小化状态,而是到还原的状态。 + //大多数情况下,都不期望在退出全屏的时候,恢复到最小化。 + placement.showCmd = InteropValues.SW.RESTORE; + } + + if ((style & InteropValues.WindowStyles.WS_MAXIMIZE) != 0) + { + //提前调用 ShowWindow 使窗口恢复最大化,若通过 SetWindowPlacement 最大化会导致闪烁,只靠其恢复 RestoreBounds. + InteropMethods.ShowWindow(hwnd, InteropValues.SW.MAXIMIZE); + } + + InteropMethods.SetWindowPlacement(hwnd, ref placement); + + if ((style & InteropValues.WindowStyles.WS_MAXIMIZE) == + 0) //如果窗口是最大化就不要修改WPF属性,否则会破坏RestoreBounds,且WPF窗口自身在最大化时,不会修改 Left Top Width Height 属性 + { + if (InteropMethods.GetWindowRect(hwnd, out var rect)) + { + //不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。 + + //确保窗口的 WPF 属性与 Win32 位置一致 + var logicalPos = + hwndSource.CompositionTarget.TransformFromDevice.Transform( + new Point(rect.Left, rect.Top)); + var logicalSize = + hwndSource.CompositionTarget.TransformFromDevice.Transform( + new Point(rect.Width, rect.Height)); + window.Left = logicalPos.X; + window.Top = logicalPos.Y; + window.Width = logicalSize.X; + window.Height = logicalSize.Y; + } + } + + //重新启用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理 + InteropMethods.DwmSetWindowAttribute(hwnd, InteropValues.DwmWindowAttribute.DWMWA_TRANSITIONS_FORCEDISABLED, 0, + sizeof(int)); + + //删除保存的状态 + window.ClearValue(BeforeFullScreenWindowPlacementProperty); + window.ClearValue(BeforeFullScreenWindowStyleProperty); + } + } + + /// + /// 确保窗口全屏的Hook + /// 使用HandleProcessCorruptedStateExceptions,防止访问内存过程中因为一些致命异常导致程序崩溃 + /// + [HandleProcessCorruptedStateExceptions] + private static IntPtr KeepFullScreenHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + //处理WM_WINDOWPOSCHANGING消息 + const int WINDOWPOSCHANGING = 0x0046; + if (msg == WINDOWPOSCHANGING) + { + try + { + //得到WINDOWPOS结构体 + var pos = (InteropValues.WindowPosition) Marshal.PtrToStructure(lParam, typeof(InteropValues.WindowPosition)); + + if ((pos.Flags & InteropValues.WindowPositionFlags.SWP_NOMOVE) != 0 && + (pos.Flags & InteropValues.WindowPositionFlags.SWP_NOSIZE) != 0) + { + //既然你既不改变位置,也不改变尺寸,我就不管了... + return IntPtr.Zero; + } + + if (InteropMethods.IsIconic(hwnd)) + { + // 如果在全屏期间最小化了窗口,那么忽略后续的位置调整。 + // 否则按后续逻辑,会根据窗口在 -32000 的位置,计算出错误的目标位置,然后就跳到主屏了。 + return IntPtr.Zero; + } + + //获取窗口现在的矩形,下面用来参考计算目标矩形 + if (InteropMethods.GetWindowRect(hwnd, out var rect)) + { + var targetRect = rect; //窗口想要变化的目标矩形 + + if ((pos.Flags & InteropValues.WindowPositionFlags.SWP_NOMOVE) == 0) + { + //需要移动 + targetRect.Left = pos.X; + targetRect.Top = pos.Y; + } + + if ((pos.Flags & InteropValues.WindowPositionFlags.SWP_NOSIZE) == 0) + { + //要改变尺寸 + targetRect.Right = targetRect.Left + pos.Width; + targetRect.Bottom = targetRect.Top + pos.Height; + } + else + { + //不改变尺寸 + targetRect.Right = targetRect.Left + rect.Width; + targetRect.Bottom = targetRect.Top + rect.Height; + } + + //使用目标矩形获取显示器信息 + var monitor = InteropMethods.MonitorFromRect(ref targetRect, InteropValues.MONITOR_DEFAULTTOPRIMARY); + var info = new InteropValues.MONITORINFO(); + info.cbSize = (uint) Marshal.SizeOf(info); + if (InteropMethods.GetMonitorInfo(monitor, ref info)) + { + //基于显示器信息设置窗口尺寸位置 + pos.X = info.rcMonitor.Left; + pos.Y = info.rcMonitor.Top; + pos.Width = info.rcMonitor.Right - info.rcMonitor.Left; + pos.Height = info.rcMonitor.Bottom - info.rcMonitor.Top; + pos.Flags &= ~(InteropValues.WindowPositionFlags.SWP_NOSIZE | InteropValues.WindowPositionFlags.SWP_NOMOVE | + InteropValues.WindowPositionFlags.SWP_NOREDRAW); + pos.Flags |= InteropValues.WindowPositionFlags.SWP_NOCOPYBITS; + + if (rect == info.rcMonitor) + { + var hwndSource = HwndSource.FromHwnd(hwnd); + if (hwndSource?.RootVisual is Window window) + { + //确保窗口的 WPF 属性与 Win32 位置一致,防止有逗比全屏后改 WPF 的属性,发生一些诡异的行为 + //下面这样做其实不太好,会再次触发 WM_WINDOWPOSCHANGING 来着.....但是又没有其他时机了 + // WM_WINDOWPOSCHANGED 不能用 + //(例如:在进入全屏后,修改 Left 属性,会进入 WM_WINDOWPOSCHANGING,然后在这里将消息里的结构体中的 Left 改回, + // 使对 Left 的修改无效,那么将不会进入 WM_WINDOWPOSCHANGED,窗口尺寸正常,但窗口的 Left 属性值错误。) + var logicalPos = + hwndSource.CompositionTarget.TransformFromDevice.Transform( + new Point(pos.X, pos.Y)); + var logicalSize = + hwndSource.CompositionTarget.TransformFromDevice.Transform( + new Point(pos.Width, pos.Height)); + window.Left = logicalPos.X; + window.Top = logicalPos.Y; + window.Width = logicalSize.X; + window.Height = logicalSize.Y; + } + } + + //将修改后的结构体拷贝回去 + Marshal.StructureToPtr(pos, lParam, false); + } + } + } + catch + { + // 这里也不需要日志啥的,只是为了防止上面有逗比逻辑,在消息循环里面炸了 + } + } + + return IntPtr.Zero; + } + } +} diff --git a/src/Shared/HandyControl_Shared/Tools/Helper/WindowHelper.cs b/src/Shared/HandyControl_Shared/Tools/Helper/WindowHelper.cs index 5a744d8e..2d155bb4 100644 --- a/src/Shared/HandyControl_Shared/Tools/Helper/WindowHelper.cs +++ b/src/Shared/HandyControl_Shared/Tools/Helper/WindowHelper.cs @@ -6,9 +6,11 @@ using System.Runtime.InteropServices; using System.Security; using System.Windows; using System.Windows.Interop; +using HandyControl.Controls; using HandyControl.Tools.Extension; using HandyControl.Tools.Helper; using HandyControl.Tools.Interop; +using Window = System.Windows.Window; namespace HandyControl.Tools { @@ -195,5 +197,9 @@ namespace HandyControl.Tools /// 开始使用触摸拖动窗口,在触摸抬起后自动结束 /// public static void TouchDragMove(this Window window) => new TouchDragMoveWindowHelper(window).Start(); + + public static void StartFullScreen(this Window window) => FullScreenHelper.StartFullScreen(window); + + public static void EndFullScreen(this Window window) => FullScreenHelper.EndFullScreen(window); } } diff --git a/src/Shared/HandyControl_Shared/Tools/Interop/InteropMethods.cs b/src/Shared/HandyControl_Shared/Tools/Interop/InteropMethods.cs index 05c606ee..7fc72dd9 100644 --- a/src/Shared/HandyControl_Shared/Tools/Interop/InteropMethods.cs +++ b/src/Shared/HandyControl_Shared/Tools/Interop/InteropMethods.cs @@ -72,7 +72,7 @@ namespace HandyControl.Tools.Interop string lpszWindow); [DllImport(InteropValues.ExternDll.User32)] - internal static extern int GetWindowRect(IntPtr hwnd, out InteropValues.RECT lpRect); + internal static extern bool GetWindowRect(IntPtr hwnd, out InteropValues.RECT lpRect); [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto)] internal static extern bool GetCursorPos(out InteropValues.POINT pt); @@ -380,14 +380,9 @@ namespace HandyControl.Tools.Interop internal static int GetWindowLong(IntPtr hWnd, InteropValues.GWL nIndex) => GetWindowLong(hWnd, (int) nIndex); - internal static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong) - { - if (IntPtr.Size == 4) - { - return SetWindowLongPtr32(hWnd, nIndex, dwNewLong); - } - return SetWindowLongPtr64(hWnd, nIndex, dwNewLong); - } + internal static IntPtr SetWindowLong(IntPtr hWnd, int nIndex, IntPtr dwNewLong) => IntPtr.Size == 4 + ? SetWindowLongPtr32(hWnd, nIndex, dwNewLong) + : SetWindowLongPtr64(hWnd, nIndex, dwNewLong); [DllImport(InteropValues.ExternDll.User32, CharSet = CharSet.Auto, EntryPoint = "SetWindowLong")] internal static extern IntPtr SetWindowLongPtr32(IntPtr hWnd, int nIndex, IntPtr dwNewLong); @@ -448,6 +443,10 @@ namespace HandyControl.Tools.Interop [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool GetMonitorInfo(IntPtr hMonitor, ref InteropValues.MONITORINFO monitorInfo); + [DllImport(InteropValues.ExternDll.User32, ExactSpelling = true)] + [ResourceExposure(ResourceScope.None)] + public static extern IntPtr MonitorFromRect(ref InteropValues.RECT rect, int flags); + [DllImport(InteropValues.ExternDll.Gdi32, SetLastError = true)] internal static extern IntPtr CreateDIBSection(IntPtr hdc, ref InteropValues.BITMAPINFO pbmi, uint iUsage, out IntPtr ppvBits, IntPtr hSection, uint dwOffset); @@ -470,6 +469,9 @@ namespace HandyControl.Tools.Interop [ResourceExposure(ResourceScope.None)] internal static extern bool EnableWindow(IntPtr hWnd, bool enable); + [DllImport(InteropValues.ExternDll.User32)] + public static extern bool ShowWindow(IntPtr hwnd, InteropValues.SW nCmdShow); + [ReflectionPermission(SecurityAction.Assert, Unrestricted = true), SecurityPermission(SecurityAction.Assert, Flags = SecurityPermissionFlag.UnmanagedCode)] internal static object PtrToStructure(IntPtr lparam, Type cls) => Marshal.PtrToStructure(lparam, cls); @@ -484,6 +486,23 @@ namespace HandyControl.Tools.Interop [DllImport(InteropValues.ExternDll.DwmApi, EntryPoint = "DwmGetColorizationColor", PreserveSig = true)] internal static extern int DwmGetColorizationColor(out uint pcrColorization, out bool pfOpaqueBlend); + [DllImport(InteropValues.ExternDll.DwmApi, ExactSpelling = true, SetLastError = true)] + internal static extern int DwmSetWindowAttribute(IntPtr hwnd, InteropValues.DwmWindowAttribute dwAttribute, + in int pvAttribute, uint cbAttribute); + + [DllImport(InteropValues.ExternDll.User32, EntryPoint = "GetWindowLong")] + private static extern IntPtr GetWindowLongPtr32(IntPtr hWnd, int nIndex); + + [DllImport(InteropValues.ExternDll.User32, EntryPoint = "GetWindowLongPtr")] + private static extern IntPtr GetWindowLongPtr64(IntPtr hWnd, int nIndex); + + internal static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) => + IntPtr.Size == 8 ? GetWindowLongPtr64(hWnd, nIndex) : GetWindowLongPtr32(hWnd, nIndex); + + [DllImport(InteropValues.ExternDll.User32, ExactSpelling = true, CharSet = CharSet.Auto)] + [ResourceExposure(ResourceScope.None)] + internal static extern bool SetWindowPlacement(IntPtr hWnd, [In] ref InteropValues.WINDOWPLACEMENT placement); + #endregion internal class Gdip diff --git a/src/Shared/HandyControl_Shared/Tools/Interop/InteropValues.cs b/src/Shared/HandyControl_Shared/Tools/Interop/InteropValues.cs index a21ecb6e..78ef2903 100644 --- a/src/Shared/HandyControl_Shared/Tools/Interop/InteropValues.cs +++ b/src/Shared/HandyControl_Shared/Tools/Interop/InteropValues.cs @@ -37,6 +37,8 @@ namespace HandyControl.Tools.Interop BI_RGB = 0, DIB_RGB_COLORS = 0, E_FAIL = unchecked((int) 0x80004005), + HWND_TOP = 0, + GWL_STYLE = -16, NIF_MESSAGE = 0x00000001, NIF_ICON = 0x00000002, NIF_TIP = 0x00000004, @@ -95,6 +97,7 @@ namespace HandyControl.Tools.Interop SC_MAXIMIZE = 0xF030, SC_RESTORE = 0xF120, SRCCOPY = 0x00CC0020, + MONITOR_DEFAULTTOPRIMARY = 0x00000001, MONITOR_DEFAULTTONEAREST = 0x00000002; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] @@ -257,6 +260,38 @@ namespace HandyControl.Tools.Interop get => Right - Left; set => Right = Left + value; } + + public bool Equals(RECT other) + { + return Left == other.Left && Right == other.Right && Top == other.Top && Bottom == other.Bottom; + } + + public override bool Equals(object obj) + { + return obj is RECT rectangle && Equals(rectangle); + } + + public static bool operator ==(RECT left, RECT right) + { + return left.Equals(right); + } + + public static bool operator !=(RECT left, RECT right) + { + return !(left == right); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = Left; + hashCode = (hashCode * 397) ^ Top; + hashCode = (hashCode * 397) ^ Right; + hashCode = (hashCode * 397) ^ Bottom; + return hashCode; + } + } } internal struct BLENDFUNCTION @@ -327,11 +362,11 @@ namespace HandyControl.Tools.Interop } [StructLayout(LayoutKind.Sequential)] - internal class WINDOWPLACEMENT + internal struct WINDOWPLACEMENT { public int length = Marshal.SizeOf(typeof(WINDOWPLACEMENT)); public int flags; - public int showCmd; + public SW showCmd; public POINT ptMinPosition; public POINT ptMaxPosition; public RECT rcNormalPosition; @@ -978,7 +1013,7 @@ namespace HandyControl.Tools.Interop } [Flags] - public enum WindowPositionFlags + internal enum WindowPositionFlags { /// /// If the calling thread and the thread that owns the window are attached to different input queues, the system posts @@ -1064,5 +1099,81 @@ namespace HandyControl.Tools.Interop /// SWP_SHOWWINDOW = 0x0040 } + + [StructLayout(LayoutKind.Sequential)] + internal struct WindowPosition + { + public IntPtr Hwnd; + public IntPtr HwndZOrderInsertAfter; + public int X; + public int Y; + public int Width; + public int Height; + public WindowPositionFlags Flags; + } + + [Flags] + internal enum DwmWindowAttribute : uint + { + DWMWA_NCRENDERING_ENABLED = 1, + DWMWA_NCRENDERING_POLICY, + DWMWA_TRANSITIONS_FORCEDISABLED, + DWMWA_ALLOW_NCPAINT, + DWMWA_CAPTION_BUTTON_BOUNDS, + DWMWA_NONCLIENT_RTL_LAYOUT, + DWMWA_FORCE_ICONIC_REPRESENTATION, + DWMWA_FLIP3D_POLICY, + DWMWA_EXTENDED_FRAME_BOUNDS, + DWMWA_HAS_ICONIC_BITMAP, + DWMWA_DISALLOW_PEEK, + DWMWA_EXCLUDED_FROM_PEEK, + DWMWA_LAST + } + + [Flags] + internal enum WindowStyles + { + /// + /// The window is initially maximized. + /// + WS_MAXIMIZE = 0x01000000, + + /// + /// The window has a maximize button. Cannot be combined with the WS_EX_CONTEXTHELP style. The WS_SYSMENU style must + /// also be specified. + /// + WS_MAXIMIZEBOX = 0x00010000, + + /// + /// The window is initially minimized. Same as the WS_ICONIC style. + /// + WS_MINIMIZE = 0x20000000, + + /// + /// The window has a sizing border. Same as the WS_SIZEBOX style. + /// + WS_THICKFRAME = 0x00040000, + } + + /// + /// ShowWindow options + /// + internal enum SW + { + HIDE = 0, + SHOWNORMAL = 1, + NORMAL = 1, + SHOWMINIMIZED = 2, + SHOWMAXIMIZED = 3, + MAXIMIZE = 3, + SHOWNOACTIVATE = 4, + SHOW = 5, + MINIMIZE = 6, + SHOWMINNOACTIVE = 7, + SHOWNA = 8, + RESTORE = 9, + SHOWDEFAULT = 10, + FORCEMINIMIZE = 11, + } } }