From 0fde3d1838f1aafaf4a3cbbb2b822f462397fccc Mon Sep 17 00:00:00 2001 From: lindexi Date: Wed, 11 Aug 2021 20:05:03 +0800 Subject: [PATCH 01/51] =?UTF-8?q?=E5=8A=A0=E4=B8=8A=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E7=9A=84=E5=85=A8=E5=B1=8F=E7=AA=97=E5=8F=A3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/Window/FullScreenHelper.cs | 622 ++++++++++++++++++ .../HandyControl_Shared.projitems | 1 + 2 files changed, 623 insertions(+) create mode 100644 src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs diff --git a/src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs b/src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs new file mode 100644 index 00000000..3f629228 --- /dev/null +++ b/src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs @@ -0,0 +1,622 @@ +using System; +using System.Collections.Generic; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Windows; +using System.Windows.Interop; + +namespace HandyControl.Controls +{ + /// + /// 用来使窗口变得全屏的辅助类 + /// 采用设置窗口位置和尺寸,确保盖住整个屏幕的方式来实现全屏 + /// 目前已知需要满足的条件是:窗口盖住整个屏幕、窗口没有WS_THICKFRAME样式、窗口不能有标题栏且最大化 + /// + public static class FullScreenHelper + { + /// + /// 用于记录窗口全屏前位置的附加属性 + /// + private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty = + DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement", typeof(WINDOWPLACEMENT?), + typeof(Window)); + + /// + /// 用于记录窗口全屏前样式的附加属性 + /// + private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty = + DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle", typeof(WindowStyles?), typeof(Window)); + + /// + /// 开始进入全屏模式 + /// 进入全屏模式后,窗口可通过 API 方式(也可以用 Win + Shift + Left/Right)移动,调整大小,但会根据目标矩形寻找显示器重新调整到全屏状态。 + /// 进入全屏后,不要修改样式等窗口属性,在退出时,会恢复到进入前的状态 + /// 进入全屏模式后会禁用 DWM 过渡动画 + /// + /// + 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, rect.Left, rect.Top, rect.Width, + rect.Height, (int) WindowPositionFlags.SWP_NOZORDER); + } + } + } + + /// + /// 退出全屏模式 + /// 窗口会回到进入全屏模式时保存的状态 + /// 退出全屏模式后会重新启用 DWM 过渡动画 + /// + /// + public static void EndFullScreen(Window window) + { + if (window == null) + { + throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能为 null"); + } + + //确保在全屏模式并获取之前保存的状态 + if (window.GetValue(BeforeFullScreenWindowPlacementProperty) is WINDOWPLACEMENT placement + && window.GetValue(BeforeFullScreenWindowStyleProperty) is 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,否则会导致窗口最小化按钮显示成还原按钮 + Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, + (IntPtr) (style & (~(WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE)))); + + if ((style & WindowStyles.WS_MINIMIZE) != 0) + { + //如果窗口进入全屏前是最小化的,这里不让窗口恢复到之前的最小化状态,而是到还原的状态。 + //大多数情况下,都不期望在退出全屏的时候,恢复到最小化。 + placement.ShowCmd = Win32.ShowWindowCommands.SW_RESTORE; + } + + if ((style & WindowStyles.WS_MAXIMIZE) != 0) + { + //提前调用 ShowWindow 使窗口恢复最大化,若通过 SetWindowPlacement 最大化会导致闪烁,只靠其恢复 RestoreBounds. + Win32.User32.ShowWindow(hwnd, Win32.ShowWindowCommands.SW_MAXIMIZE); + } + + Win32.User32.SetWindowPlacement(hwnd, ref placement); + + if ((style & WindowStyles.WS_MAXIMIZE) == + 0) //如果窗口是最大化就不要修改WPF属性,否则会破坏RestoreBounds,且WPF窗口自身在最大化时,不会修改 Left Top Width Height 属性 + { + if (Win32.User32.GetWindowRect(hwnd, out var rect)) + { + //不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。 + + //确保窗口的 WPF 属性与 Win32 位置一致 + var logicalPos = + hwndSource.CompositionTarget.TransformFromDevice.Transform( + new System.Windows.Point(rect.Left, rect.Top)); + var logicalSize = + hwndSource.CompositionTarget.TransformFromDevice.Transform( + new System.Windows.Point(rect.Width, rect.Height)); + window.Left = logicalPos.X; + window.Top = logicalPos.Y; + window.Width = logicalSize.X; + window.Height = logicalSize.Y; + } + } + + //重新启用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理 + Win32.Dwmapi.DwmSetWindowAttribute(hwnd, 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 = (WindowPosition) Marshal.PtrToStructure(lParam, typeof(WindowPosition)); + + if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) != 0 && + (pos.Flags & WindowPositionFlags.SWP_NOSIZE) != 0) + { + //既然你既不改变位置,也不改变尺寸,我就不管了... + return IntPtr.Zero; + } + + if (Win32.User32.IsIconic(hwnd)) + { + // 如果在全屏期间最小化了窗口,那么忽略后续的位置调整。 + // 否则按后续逻辑,会根据窗口在 -32000 的位置,计算出错误的目标位置,然后就跳到主屏了。 + return IntPtr.Zero; + } + + //获取窗口现在的矩形,下面用来参考计算目标矩形 + if (Win32.User32.GetWindowRect(hwnd, out var rect)) + { + var targetRect = rect; //窗口想要变化的目标矩形 + + if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) == 0) + { + //需要移动 + targetRect.Left = pos.X; + targetRect.Top = pos.Y; + } + + if ((pos.Flags & 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 = Win32.User32.MonitorFromRect(targetRect, MonitorFlag.MONITOR_DEFAULTTOPRIMARY); + var info = new MonitorInfo(); + info.Size = (uint) Marshal.SizeOf(info); + if (Win32.User32.GetMonitorInfo(monitor, ref info)) + { + //基于显示器信息设置窗口尺寸位置 + pos.X = info.MonitorRect.Left; + pos.Y = info.MonitorRect.Top; + pos.Width = info.MonitorRect.Right - info.MonitorRect.Left; + pos.Height = info.MonitorRect.Bottom - info.MonitorRect.Top; + pos.Flags &= ~(WindowPositionFlags.SWP_NOSIZE | WindowPositionFlags.SWP_NOMOVE | + WindowPositionFlags.SWP_NOREDRAW); + pos.Flags |= WindowPositionFlags.SWP_NOCOPYBITS; + + if (rect == info.MonitorRect) + { + 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 System.Windows.Point(pos.X, pos.Y)); + var logicalSize = + hwndSource.CompositionTarget.TransformFromDevice.Transform( + new System.Windows.Point(pos.Width, pos.Height)); + window.Left = logicalPos.X; + window.Top = logicalPos.Y; + window.Width = logicalSize.X; + window.Height = logicalSize.Y; + } + else + { + //这个hwnd是前面从Window来的,如果现在他不是Window...... 你信么 + } + } + + //将修改后的结构体拷贝回去 + Marshal.StructureToPtr(pos, lParam, false); + } + } + } + catch + { + // 这里也不需要日志啥的,只是为了防止上面有逗比逻辑,在消息循环里面炸了 + } + } + + return IntPtr.Zero; + } + + static class Win32 + { + [Flags] + public enum ShowWindowCommands + { + /// + /// Maximizes the specified window. + /// + SW_MAXIMIZE = 3, + + /// + /// Activates and displays the window. If the window is minimized or maximized, the system restores it to its original + /// size and position. An application should specify this flag when restoring a minimized window. + /// + SW_RESTORE = 9, + } + + + internal static class Properties + { +#if !ANSI + public const CharSet BuildCharSet = CharSet.Unicode; +#else + public const CharSet BuildCharSet = CharSet.Ansi; +#endif + } + + public static class Dwmapi + { + public const string LibraryName = "Dwmapi.dll"; + + [DllImport(LibraryName, ExactSpelling = true, PreserveSig = false)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool DwmIsCompositionEnabled(); + + [DllImport("Dwmapi.dll", ExactSpelling = true, SetLastError = true)] + public static extern int DwmSetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE dwAttribute, + in int pvAttribute, uint cbAttribute); + } + + public static class User32 + { + public const string LibraryName = "user32"; + + [DllImport(LibraryName, CharSet = Properties.BuildCharSet)] + public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi); + + [DllImport(LibraryName, ExactSpelling = true)] + public static extern IntPtr MonitorFromRect(in Rectangle lprc, MonitorFlag dwFlags); + + [DllImport(LibraryName, ExactSpelling = true)] + public static extern bool IsIconic(IntPtr hwnd); + + [DllImport(LibraryName, ExactSpelling = true)] + public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommands nCmdShow); + + [DllImport(LibraryName, ExactSpelling = true)] + public static extern bool SetWindowPlacement(IntPtr hWnd, + [In] ref WINDOWPLACEMENT lpwndpl); + + [return: MarshalAs(UnmanagedType.Bool)] + [DllImport(LibraryName, ExactSpelling = true)] + public static extern bool GetWindowRect(IntPtr hWnd, out Rectangle lpRect); + + [DllImport(LibraryName, ExactSpelling = true, SetLastError = true)] + public static extern Int32 SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, Int32 x, Int32 y, Int32 cx, + Int32 cy, Int32 wFlagslong); + + [DllImport(LibraryName, ExactSpelling = true)] + public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); + + public static IntPtr GetWindowLongPtr(IntPtr hWnd, GetWindowLongFields nIndex) => + GetWindowLongPtr(hWnd, (int) nIndex); + + public static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) + { + return IntPtr.Size > 4 +#pragma warning disable CS0618 // 类型或成员已过时 + ? GetWindowLongPtr_x64(hWnd, nIndex) + : new IntPtr(GetWindowLong(hWnd, nIndex)); +#pragma warning restore CS0618 // 类型或成员已过时 + } + + [DllImport(LibraryName, CharSet = Properties.BuildCharSet)] + public static extern int GetWindowLong(IntPtr hWnd, int nIndex); + + [DllImport(LibraryName, CharSet = Properties.BuildCharSet, EntryPoint = "GetWindowLongPtr")] + public static extern IntPtr GetWindowLongPtr_x64(IntPtr hWnd, int nIndex); + + public static IntPtr SetWindowLongPtr(IntPtr hWnd, GetWindowLongFields nIndex, IntPtr dwNewLong) => + SetWindowLongPtr(hWnd, (int) nIndex, dwNewLong); + + public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) + { + return IntPtr.Size > 4 +#pragma warning disable CS0618 // 类型或成员已过时 + ? SetWindowLongPtr_x64(hWnd, nIndex, dwNewLong) + : new IntPtr(SetWindowLong(hWnd, nIndex, dwNewLong.ToInt32())); +#pragma warning restore CS0618 // 类型或成员已过时 + } + + [DllImport(LibraryName, CharSet = Properties.BuildCharSet, EntryPoint = "SetWindowLongPtr")] + public static extern IntPtr SetWindowLongPtr_x64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport(LibraryName, CharSet = Properties.BuildCharSet)] + public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); + } + } + + [StructLayout(LayoutKind.Sequential)] + struct MonitorInfo + { + /// + /// The size of the structure, in bytes. + /// + public uint Size; + + /// + /// A RECT structure that specifies the display monitor rectangle, expressed in virtual-screen coordinates. Note that + /// if the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. + /// + public Rectangle MonitorRect; + + /// + /// A RECT structure that specifies the work area rectangle of the display monitor, expressed in virtual-screen + /// coordinates. Note that if the monitor is not the primary display monitor, some of the rectangle's coordinates may + /// be negative values. + /// + public Rectangle WorkRect; + + /// + /// A set of flags that represent attributes of the display monitor. + /// + public MonitorInfoFlag Flags; + } + + enum MonitorInfoFlag + { + } + + enum MonitorFlag + { + /// + /// Returns a handle to the primary display monitor. + /// + MONITOR_DEFAULTTOPRIMARY = 1, + } + + [StructLayout(LayoutKind.Sequential)] + struct WindowPosition + { + public IntPtr Hwnd; + public IntPtr HwndZOrderInsertAfter; + public int X; + public int Y; + public int Width; + public int Height; + public WindowPositionFlags Flags; + } + + enum HwndZOrder + { + /// + /// Places the window at the top of the Z order. + /// + HWND_TOP = 0, + } + + enum DWMWINDOWATTRIBUTE : uint + { + DWMWA_TRANSITIONS_FORCEDISABLED = 3, + } + + enum GetWindowLongFields + { + /// + /// 设定一个新的窗口风格 + /// Retrieves the window styles + /// + GWL_STYLE = -16, + } + + [StructLayout(LayoutKind.Sequential)] + struct WINDOWPLACEMENT // WindowPlacement + { + public uint Size; + public WindowPlacementFlags Flags; + public Win32.ShowWindowCommands ShowCmd; + public Point MinPosition; + public Point MaxPosition; + public Rectangle NormalPosition; + } + + [Flags] + public enum WindowPositionFlags + { + /// + /// + /// 清除客户区的所有内容。如果未设置该标志,客户区的有效内容被保存并且在窗口尺寸更新和重定位后拷贝回客户区 + /// + /// Discards the entire contents of the client area. If this flag is not specified, the valid contents of the client + /// area are saved and copied back into the client area after the window is sized or repositioned. + /// + SWP_NOCOPYBITS = 0x0100, + + /// + /// + /// 维持当前位置(忽略X和Y参数) + /// + /// Retains the current position (ignores X and Y parameters). + /// + SWP_NOMOVE = 0x0002, + + /// + /// + /// 不重画改变的内容。如果设置了这个标志,则不发生任何重画动作。适用于客户区和非客户区(包括标题栏和滚动条)和任何由于窗回移动而露出的父窗口的所有部分。如果设置了这个标志,应用程序必须明确地使窗口无效并区重画窗口的任何部分和父窗口需要重画的部分 + /// + /// Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to the client area, + /// the nonclient area (including the title bar and scroll bars), and any part of the parent window uncovered as a + /// result of the window being moved. When this flag is set, the application must explicitly invalidate or redraw any + /// parts of the window and parent window that need redrawing. + /// + SWP_NOREDRAW = 0x0008, + + /// + /// + /// 维持当前尺寸(忽略 cx 和 cy 参数) + /// + /// Retains the current size (ignores the cx and cy parameters). + /// + SWP_NOSIZE = 0x0001, + + /// + /// + /// 维持当前 Z 序(忽略 hWndlnsertAfter 参数) + /// + /// Retains the current Z order (ignores the hWndInsertAfter parameter). + /// + SWP_NOZORDER = 0x0004, + } + + [StructLayout(LayoutKind.Sequential)] + struct Rectangle + { + public int Left; + public int Top; + public int Right; + public int Bottom; + + /// + /// 矩形的宽度 + /// + public int Width + { + get { return unchecked((int) (Right - Left)); } + set { Right = unchecked((int) (Left + value)); } + } + + /// + /// 矩形的高度 + /// + public int Height + { + get { return unchecked((int) (Bottom - Top)); } + set { Bottom = unchecked((int) (Top + value)); } + } + + public bool Equals(Rectangle other) + { + return (Left == other.Left) && (Right == other.Right) && (Top == other.Top) && (Bottom == other.Bottom); + } + + public override bool Equals(object obj) + { + return obj is Rectangle rectangle && Equals(rectangle); + } + + public static bool operator ==(Rectangle left, Rectangle right) + { + return left.Equals(right); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (int) Left; + hashCode = (hashCode * 397) ^ (int) Top; + hashCode = (hashCode * 397) ^ (int) Right; + hashCode = (hashCode * 397) ^ (int) Bottom; + return hashCode; + } + } + + public static bool operator !=(Rectangle left, Rectangle right) + { + return !(left == right); + } + } + + [StructLayout(LayoutKind.Sequential)] + struct Point + { + public int X; + public int Y; + } + + [Flags] + enum WindowPlacementFlags + { + } + + [Flags] + 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, + } + } +} diff --git a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems index 7032cb29..d5a8e59a 100644 --- a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems +++ b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems @@ -82,6 +82,7 @@ + From e7a5aa275f9091a4b8967ba12dc80cce0166deef Mon Sep 17 00:00:00 2001 From: Mahdi Hosseini Date: Thu, 28 Oct 2021 14:30:43 +0330 Subject: [PATCH 02/51] Update bug report template --- .github/ISSUE_TEMPLATE/bug_report.md | 32 --------- .github/ISSUE_TEMPLATE/bug_report.yaml | 90 ++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 32 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 150c58a4..00000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Environment (please complete the following information):** - - .net: [e.g. 4.6.2] - - IDE [e.g. vs2017] - - Version [e.g. 1.0.0] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 00000000..ea3be13c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,90 @@ +name: Bug report +description: File a bug report +title: "Bug title" +labels: [] +body: + - type: textarea + validations: + required: true + attributes: + label: Describe the bug + description: Please enter a short, clear description of the bug. + - type: textarea + validations: + required: true + attributes: + label: Steps to reproduce the bug + description: Please provide any required setup and steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 2. Click on '....' + - type: textarea + attributes: + label: Expected behavior + description: Please provide a description of what you expected to happen + - type: textarea + attributes: + label: Screenshots + description: If applicable, add screenshots here to help explain your problem + - type: dropdown + attributes: + label: NuGet package version + options: + - "HandyControl 3.2.0" + - "HandyControl 3.1.0" + - "HandyControl 3.0.0" + - "HandyControls (Custom version) 3.3.8.1" + - "HandyControls (Custom version) 3.3.8" + - "HandyControls (Custom version) 3.3.7" + - type: dropdown + attributes: + label: IDE + description: Which Visual Studio versions did you see the issue on? + multiple: true + options: + - "Visual Studio 2022-preview" + - "Visual Studio 2022" + - "Visual Studio 2019" + - "Visual Studio 2017" + - type: dropdown + attributes: + label: Framework type + description: please specify for which framework type you have encountered the issue. + multiple: true + options: + - ".Net 6.0" + - ".Net 5.0" + - ".Net Core 3.1" + - ".Net Core 3.0" + - ".Net Framework 4.8" + - ".Net Framework 4.7.2" + - ".Net Framework 4.7.1" + - ".Net Framework 4.7.0" + - ".Net Framework 4.6.2" + - ".Net Framework 4.6.1" + - ".Net Framework 4.6.0" + - ".Net Framework 4.5.2" + - ".Net Framework 4.5.1" + - ".Net Framework 4.5.0" + - ".Net Framework 4.0.0" + - type: dropdown + attributes: + label: Windows version + description: Which Windows versions did you see the issue on? + multiple: true + options: + - "Insider Build (xxxxx)" + - "Windows 11 (22000)" + - "May 2021 Update (19043)" + - "October 2020 Update (19042)" + - "May 2020 Update (19041)" + - "November 2019 Update (18363)" + - "May 2019 Update (18362)" + - "October 2018 Update (17763)" + - "April 2018 Update (17134)" + - "Fall Creators Update (16299)" + - "Creators Update (15063)" + - type: textarea + attributes: + label: Additional context + description: Enter any other applicable info here From d1e117993abbbd3f2c80d3e098b39afa51455a01 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Mon, 20 Dec 2021 23:19:49 +0800 Subject: [PATCH 03/51] chore: formatted FullScreenHelper code --- .../Controls/Window/FullScreenHelper.cs | 622 ------------------ .../HandyControl_Shared.projitems | 2 +- .../Tools/Helper/FullScreenHelper.cs | 277 ++++++++ .../Tools/Helper/WindowHelper.cs | 6 + .../Tools/Interop/InteropMethods.cs | 37 +- .../Tools/Interop/InteropValues.cs | 117 +++- 6 files changed, 426 insertions(+), 635 deletions(-) delete mode 100644 src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs create mode 100644 src/Shared/HandyControl_Shared/Tools/Helper/FullScreenHelper.cs diff --git a/src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs b/src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs deleted file mode 100644 index 3f629228..00000000 --- a/src/Shared/HandyControl_Shared/Controls/Window/FullScreenHelper.cs +++ /dev/null @@ -1,622 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.ExceptionServices; -using System.Runtime.InteropServices; -using System.Text; -using System.Windows; -using System.Windows.Interop; - -namespace HandyControl.Controls -{ - /// - /// 用来使窗口变得全屏的辅助类 - /// 采用设置窗口位置和尺寸,确保盖住整个屏幕的方式来实现全屏 - /// 目前已知需要满足的条件是:窗口盖住整个屏幕、窗口没有WS_THICKFRAME样式、窗口不能有标题栏且最大化 - /// - public static class FullScreenHelper - { - /// - /// 用于记录窗口全屏前位置的附加属性 - /// - private static readonly DependencyProperty BeforeFullScreenWindowPlacementProperty = - DependencyProperty.RegisterAttached("BeforeFullScreenWindowPlacement", typeof(WINDOWPLACEMENT?), - typeof(Window)); - - /// - /// 用于记录窗口全屏前样式的附加属性 - /// - private static readonly DependencyProperty BeforeFullScreenWindowStyleProperty = - DependencyProperty.RegisterAttached("BeforeFullScreenWindowStyle", typeof(WindowStyles?), typeof(Window)); - - /// - /// 开始进入全屏模式 - /// 进入全屏模式后,窗口可通过 API 方式(也可以用 Win + Shift + Left/Right)移动,调整大小,但会根据目标矩形寻找显示器重新调整到全屏状态。 - /// 进入全屏后,不要修改样式等窗口属性,在退出时,会恢复到进入前的状态 - /// 进入全屏模式后会禁用 DWM 过渡动画 - /// - /// - 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, rect.Left, rect.Top, rect.Width, - rect.Height, (int) WindowPositionFlags.SWP_NOZORDER); - } - } - } - - /// - /// 退出全屏模式 - /// 窗口会回到进入全屏模式时保存的状态 - /// 退出全屏模式后会重新启用 DWM 过渡动画 - /// - /// - public static void EndFullScreen(Window window) - { - if (window == null) - { - throw new ArgumentNullException(nameof(window), $"{nameof(window)} 不能为 null"); - } - - //确保在全屏模式并获取之前保存的状态 - if (window.GetValue(BeforeFullScreenWindowPlacementProperty) is WINDOWPLACEMENT placement - && window.GetValue(BeforeFullScreenWindowStyleProperty) is 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,否则会导致窗口最小化按钮显示成还原按钮 - Win32.User32.SetWindowLongPtr(hwnd, GetWindowLongFields.GWL_STYLE, - (IntPtr) (style & (~(WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE)))); - - if ((style & WindowStyles.WS_MINIMIZE) != 0) - { - //如果窗口进入全屏前是最小化的,这里不让窗口恢复到之前的最小化状态,而是到还原的状态。 - //大多数情况下,都不期望在退出全屏的时候,恢复到最小化。 - placement.ShowCmd = Win32.ShowWindowCommands.SW_RESTORE; - } - - if ((style & WindowStyles.WS_MAXIMIZE) != 0) - { - //提前调用 ShowWindow 使窗口恢复最大化,若通过 SetWindowPlacement 最大化会导致闪烁,只靠其恢复 RestoreBounds. - Win32.User32.ShowWindow(hwnd, Win32.ShowWindowCommands.SW_MAXIMIZE); - } - - Win32.User32.SetWindowPlacement(hwnd, ref placement); - - if ((style & WindowStyles.WS_MAXIMIZE) == - 0) //如果窗口是最大化就不要修改WPF属性,否则会破坏RestoreBounds,且WPF窗口自身在最大化时,不会修改 Left Top Width Height 属性 - { - if (Win32.User32.GetWindowRect(hwnd, out var rect)) - { - //不能用 placement 的坐标,placement是工作区坐标,不是屏幕坐标。 - - //确保窗口的 WPF 属性与 Win32 位置一致 - var logicalPos = - hwndSource.CompositionTarget.TransformFromDevice.Transform( - new System.Windows.Point(rect.Left, rect.Top)); - var logicalSize = - hwndSource.CompositionTarget.TransformFromDevice.Transform( - new System.Windows.Point(rect.Width, rect.Height)); - window.Left = logicalPos.X; - window.Top = logicalPos.Y; - window.Width = logicalSize.X; - window.Height = logicalSize.Y; - } - } - - //重新启用 DWM 过渡动画 忽略返回值,若DWM关闭不做处理 - Win32.Dwmapi.DwmSetWindowAttribute(hwnd, 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 = (WindowPosition) Marshal.PtrToStructure(lParam, typeof(WindowPosition)); - - if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) != 0 && - (pos.Flags & WindowPositionFlags.SWP_NOSIZE) != 0) - { - //既然你既不改变位置,也不改变尺寸,我就不管了... - return IntPtr.Zero; - } - - if (Win32.User32.IsIconic(hwnd)) - { - // 如果在全屏期间最小化了窗口,那么忽略后续的位置调整。 - // 否则按后续逻辑,会根据窗口在 -32000 的位置,计算出错误的目标位置,然后就跳到主屏了。 - return IntPtr.Zero; - } - - //获取窗口现在的矩形,下面用来参考计算目标矩形 - if (Win32.User32.GetWindowRect(hwnd, out var rect)) - { - var targetRect = rect; //窗口想要变化的目标矩形 - - if ((pos.Flags & WindowPositionFlags.SWP_NOMOVE) == 0) - { - //需要移动 - targetRect.Left = pos.X; - targetRect.Top = pos.Y; - } - - if ((pos.Flags & 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 = Win32.User32.MonitorFromRect(targetRect, MonitorFlag.MONITOR_DEFAULTTOPRIMARY); - var info = new MonitorInfo(); - info.Size = (uint) Marshal.SizeOf(info); - if (Win32.User32.GetMonitorInfo(monitor, ref info)) - { - //基于显示器信息设置窗口尺寸位置 - pos.X = info.MonitorRect.Left; - pos.Y = info.MonitorRect.Top; - pos.Width = info.MonitorRect.Right - info.MonitorRect.Left; - pos.Height = info.MonitorRect.Bottom - info.MonitorRect.Top; - pos.Flags &= ~(WindowPositionFlags.SWP_NOSIZE | WindowPositionFlags.SWP_NOMOVE | - WindowPositionFlags.SWP_NOREDRAW); - pos.Flags |= WindowPositionFlags.SWP_NOCOPYBITS; - - if (rect == info.MonitorRect) - { - 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 System.Windows.Point(pos.X, pos.Y)); - var logicalSize = - hwndSource.CompositionTarget.TransformFromDevice.Transform( - new System.Windows.Point(pos.Width, pos.Height)); - window.Left = logicalPos.X; - window.Top = logicalPos.Y; - window.Width = logicalSize.X; - window.Height = logicalSize.Y; - } - else - { - //这个hwnd是前面从Window来的,如果现在他不是Window...... 你信么 - } - } - - //将修改后的结构体拷贝回去 - Marshal.StructureToPtr(pos, lParam, false); - } - } - } - catch - { - // 这里也不需要日志啥的,只是为了防止上面有逗比逻辑,在消息循环里面炸了 - } - } - - return IntPtr.Zero; - } - - static class Win32 - { - [Flags] - public enum ShowWindowCommands - { - /// - /// Maximizes the specified window. - /// - SW_MAXIMIZE = 3, - - /// - /// Activates and displays the window. If the window is minimized or maximized, the system restores it to its original - /// size and position. An application should specify this flag when restoring a minimized window. - /// - SW_RESTORE = 9, - } - - - internal static class Properties - { -#if !ANSI - public const CharSet BuildCharSet = CharSet.Unicode; -#else - public const CharSet BuildCharSet = CharSet.Ansi; -#endif - } - - public static class Dwmapi - { - public const string LibraryName = "Dwmapi.dll"; - - [DllImport(LibraryName, ExactSpelling = true, PreserveSig = false)] - [return: MarshalAs(UnmanagedType.Bool)] - public static extern bool DwmIsCompositionEnabled(); - - [DllImport("Dwmapi.dll", ExactSpelling = true, SetLastError = true)] - public static extern int DwmSetWindowAttribute(IntPtr hwnd, DWMWINDOWATTRIBUTE dwAttribute, - in int pvAttribute, uint cbAttribute); - } - - public static class User32 - { - public const string LibraryName = "user32"; - - [DllImport(LibraryName, CharSet = Properties.BuildCharSet)] - public static extern bool GetMonitorInfo(IntPtr hMonitor, ref MonitorInfo lpmi); - - [DllImport(LibraryName, ExactSpelling = true)] - public static extern IntPtr MonitorFromRect(in Rectangle lprc, MonitorFlag dwFlags); - - [DllImport(LibraryName, ExactSpelling = true)] - public static extern bool IsIconic(IntPtr hwnd); - - [DllImport(LibraryName, ExactSpelling = true)] - public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommands nCmdShow); - - [DllImport(LibraryName, ExactSpelling = true)] - public static extern bool SetWindowPlacement(IntPtr hWnd, - [In] ref WINDOWPLACEMENT lpwndpl); - - [return: MarshalAs(UnmanagedType.Bool)] - [DllImport(LibraryName, ExactSpelling = true)] - public static extern bool GetWindowRect(IntPtr hWnd, out Rectangle lpRect); - - [DllImport(LibraryName, ExactSpelling = true, SetLastError = true)] - public static extern Int32 SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, Int32 x, Int32 y, Int32 cx, - Int32 cy, Int32 wFlagslong); - - [DllImport(LibraryName, ExactSpelling = true)] - public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); - - public static IntPtr GetWindowLongPtr(IntPtr hWnd, GetWindowLongFields nIndex) => - GetWindowLongPtr(hWnd, (int) nIndex); - - public static IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex) - { - return IntPtr.Size > 4 -#pragma warning disable CS0618 // 类型或成员已过时 - ? GetWindowLongPtr_x64(hWnd, nIndex) - : new IntPtr(GetWindowLong(hWnd, nIndex)); -#pragma warning restore CS0618 // 类型或成员已过时 - } - - [DllImport(LibraryName, CharSet = Properties.BuildCharSet)] - public static extern int GetWindowLong(IntPtr hWnd, int nIndex); - - [DllImport(LibraryName, CharSet = Properties.BuildCharSet, EntryPoint = "GetWindowLongPtr")] - public static extern IntPtr GetWindowLongPtr_x64(IntPtr hWnd, int nIndex); - - public static IntPtr SetWindowLongPtr(IntPtr hWnd, GetWindowLongFields nIndex, IntPtr dwNewLong) => - SetWindowLongPtr(hWnd, (int) nIndex, dwNewLong); - - public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong) - { - return IntPtr.Size > 4 -#pragma warning disable CS0618 // 类型或成员已过时 - ? SetWindowLongPtr_x64(hWnd, nIndex, dwNewLong) - : new IntPtr(SetWindowLong(hWnd, nIndex, dwNewLong.ToInt32())); -#pragma warning restore CS0618 // 类型或成员已过时 - } - - [DllImport(LibraryName, CharSet = Properties.BuildCharSet, EntryPoint = "SetWindowLongPtr")] - public static extern IntPtr SetWindowLongPtr_x64(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - - [DllImport(LibraryName, CharSet = Properties.BuildCharSet)] - public static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong); - } - } - - [StructLayout(LayoutKind.Sequential)] - struct MonitorInfo - { - /// - /// The size of the structure, in bytes. - /// - public uint Size; - - /// - /// A RECT structure that specifies the display monitor rectangle, expressed in virtual-screen coordinates. Note that - /// if the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values. - /// - public Rectangle MonitorRect; - - /// - /// A RECT structure that specifies the work area rectangle of the display monitor, expressed in virtual-screen - /// coordinates. Note that if the monitor is not the primary display monitor, some of the rectangle's coordinates may - /// be negative values. - /// - public Rectangle WorkRect; - - /// - /// A set of flags that represent attributes of the display monitor. - /// - public MonitorInfoFlag Flags; - } - - enum MonitorInfoFlag - { - } - - enum MonitorFlag - { - /// - /// Returns a handle to the primary display monitor. - /// - MONITOR_DEFAULTTOPRIMARY = 1, - } - - [StructLayout(LayoutKind.Sequential)] - struct WindowPosition - { - public IntPtr Hwnd; - public IntPtr HwndZOrderInsertAfter; - public int X; - public int Y; - public int Width; - public int Height; - public WindowPositionFlags Flags; - } - - enum HwndZOrder - { - /// - /// Places the window at the top of the Z order. - /// - HWND_TOP = 0, - } - - enum DWMWINDOWATTRIBUTE : uint - { - DWMWA_TRANSITIONS_FORCEDISABLED = 3, - } - - enum GetWindowLongFields - { - /// - /// 设定一个新的窗口风格 - /// Retrieves the window styles - /// - GWL_STYLE = -16, - } - - [StructLayout(LayoutKind.Sequential)] - struct WINDOWPLACEMENT // WindowPlacement - { - public uint Size; - public WindowPlacementFlags Flags; - public Win32.ShowWindowCommands ShowCmd; - public Point MinPosition; - public Point MaxPosition; - public Rectangle NormalPosition; - } - - [Flags] - public enum WindowPositionFlags - { - /// - /// - /// 清除客户区的所有内容。如果未设置该标志,客户区的有效内容被保存并且在窗口尺寸更新和重定位后拷贝回客户区 - /// - /// Discards the entire contents of the client area. If this flag is not specified, the valid contents of the client - /// area are saved and copied back into the client area after the window is sized or repositioned. - /// - SWP_NOCOPYBITS = 0x0100, - - /// - /// - /// 维持当前位置(忽略X和Y参数) - /// - /// Retains the current position (ignores X and Y parameters). - /// - SWP_NOMOVE = 0x0002, - - /// - /// - /// 不重画改变的内容。如果设置了这个标志,则不发生任何重画动作。适用于客户区和非客户区(包括标题栏和滚动条)和任何由于窗回移动而露出的父窗口的所有部分。如果设置了这个标志,应用程序必须明确地使窗口无效并区重画窗口的任何部分和父窗口需要重画的部分 - /// - /// Does not redraw changes. If this flag is set, no repainting of any kind occurs. This applies to the client area, - /// the nonclient area (including the title bar and scroll bars), and any part of the parent window uncovered as a - /// result of the window being moved. When this flag is set, the application must explicitly invalidate or redraw any - /// parts of the window and parent window that need redrawing. - /// - SWP_NOREDRAW = 0x0008, - - /// - /// - /// 维持当前尺寸(忽略 cx 和 cy 参数) - /// - /// Retains the current size (ignores the cx and cy parameters). - /// - SWP_NOSIZE = 0x0001, - - /// - /// - /// 维持当前 Z 序(忽略 hWndlnsertAfter 参数) - /// - /// Retains the current Z order (ignores the hWndInsertAfter parameter). - /// - SWP_NOZORDER = 0x0004, - } - - [StructLayout(LayoutKind.Sequential)] - struct Rectangle - { - public int Left; - public int Top; - public int Right; - public int Bottom; - - /// - /// 矩形的宽度 - /// - public int Width - { - get { return unchecked((int) (Right - Left)); } - set { Right = unchecked((int) (Left + value)); } - } - - /// - /// 矩形的高度 - /// - public int Height - { - get { return unchecked((int) (Bottom - Top)); } - set { Bottom = unchecked((int) (Top + value)); } - } - - public bool Equals(Rectangle other) - { - return (Left == other.Left) && (Right == other.Right) && (Top == other.Top) && (Bottom == other.Bottom); - } - - public override bool Equals(object obj) - { - return obj is Rectangle rectangle && Equals(rectangle); - } - - public static bool operator ==(Rectangle left, Rectangle right) - { - return left.Equals(right); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (int) Left; - hashCode = (hashCode * 397) ^ (int) Top; - hashCode = (hashCode * 397) ^ (int) Right; - hashCode = (hashCode * 397) ^ (int) Bottom; - return hashCode; - } - } - - public static bool operator !=(Rectangle left, Rectangle right) - { - return !(left == right); - } - } - - [StructLayout(LayoutKind.Sequential)] - struct Point - { - public int X; - public int Y; - } - - [Flags] - enum WindowPlacementFlags - { - } - - [Flags] - 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, - } - } -} diff --git a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems index d5a8e59a..895d18dc 100644 --- a/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems +++ b/src/Shared/HandyControl_Shared/HandyControl_Shared.projitems @@ -82,7 +82,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, + } } } From ee8f18aaea11a5e6d8f0714f1c88642b6d33c968 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Tue, 21 Dec 2021 22:48:24 +0800 Subject: [PATCH 04/51] enhance: add Orientation prop for ScrollViewerAttach --- .../HandyControl_Net_40/Themes/Theme.xaml | 2 +- .../Controls/ScrollViewerDemoCtl.xaml | 2 +- .../Controls/WaterfallPanelDemoCtl.xaml | 2 +- .../Styles/NativeScrollViewerDemoCtl.xaml | 2 +- .../Controls/Attach/ScrollViewerAttach.cs | 41 ++++++++++++++++++- .../Controls/Other/ScrollViewer.cs | 19 +-------- .../HandyControl_Shared/Data/ValueBoxes.cs | 11 ++++- .../Styles/Base/TabControlBaseStyle.xaml | 2 +- .../HandyControl_Shared/Themes/Theme.xaml | 2 +- 9 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml b/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml index f1ce9662..a04e38c2 100644 --- a/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml +++ b/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml @@ -10874,7 +10874,7 @@ - + diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ScrollViewerDemoCtl.xaml b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ScrollViewerDemoCtl.xaml index 8f4ce600..08918f62 100644 --- a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ScrollViewerDemoCtl.xaml +++ b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/ScrollViewerDemoCtl.xaml @@ -14,7 +14,7 @@ - + diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/WaterfallPanelDemoCtl.xaml b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/WaterfallPanelDemoCtl.xaml index 253b7707..37218e9a 100644 --- a/src/Shared/HandyControlDemo_Shared/UserControl/Controls/WaterfallPanelDemoCtl.xaml +++ b/src/Shared/HandyControlDemo_Shared/UserControl/Controls/WaterfallPanelDemoCtl.xaml @@ -32,7 +32,7 @@ - + diff --git a/src/Shared/HandyControlDemo_Shared/UserControl/Styles/NativeScrollViewerDemoCtl.xaml b/src/Shared/HandyControlDemo_Shared/UserControl/Styles/NativeScrollViewerDemoCtl.xaml index 2478bb37..b0447226 100644 --- a/src/Shared/HandyControlDemo_Shared/UserControl/Styles/NativeScrollViewerDemoCtl.xaml +++ b/src/Shared/HandyControlDemo_Shared/UserControl/Styles/NativeScrollViewerDemoCtl.xaml @@ -14,7 +14,7 @@ - + diff --git a/src/Shared/HandyControl_Shared/Controls/Attach/ScrollViewerAttach.cs b/src/Shared/HandyControl_Shared/Controls/Attach/ScrollViewerAttach.cs index 40c1df2f..f78e4545 100644 --- a/src/Shared/HandyControl_Shared/Controls/Attach/ScrollViewerAttach.cs +++ b/src/Shared/HandyControl_Shared/Controls/Attach/ScrollViewerAttach.cs @@ -1,4 +1,6 @@ -using System.Windows; +using System; +using System.Windows; +using System.Windows.Controls; using HandyControl.Data; namespace HandyControl.Controls @@ -13,5 +15,42 @@ namespace HandyControl.Controls public static bool GetAutoHide(DependencyObject element) => (bool) element.GetValue(AutoHideProperty); + + public static readonly DependencyProperty OrientationProperty = DependencyProperty.RegisterAttached( + "Orientation", typeof(Orientation), typeof(ScrollViewerAttach), new FrameworkPropertyMetadata(ValueBoxes.VerticalBox, FrameworkPropertyMetadataOptions.Inherits, OnOrientationChanged)); + + private static void OnOrientationChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is ScrollViewer) + { + return; + } + + if (d is System.Windows.Controls.ScrollViewer scrollViewer) + { + if ((Orientation) e.NewValue == Orientation.Horizontal) + { + scrollViewer.PreviewMouseWheel += ScrollViewer_PreviewMouseWheel; + } + else + { + scrollViewer.PreviewMouseWheel -= ScrollViewer_PreviewMouseWheel; + } + } + } + + private static void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e) + { + var scrollViewer = (System.Windows.Controls.ScrollViewer) sender; + scrollViewer.ScrollToHorizontalOffset(Math.Min(Math.Max(0, scrollViewer.HorizontalOffset - e.Delta), scrollViewer.ScrollableWidth)); + + e.Handled = true; + } + + public static void SetOrientation(DependencyObject element, Orientation value) + => element.SetValue(OrientationProperty, ValueBoxes.OrientationBox(value)); + + public static Orientation GetOrientation(DependencyObject element) + => (Orientation) element.GetValue(OrientationProperty); } } diff --git a/src/Shared/HandyControl_Shared/Controls/Other/ScrollViewer.cs b/src/Shared/HandyControl_Shared/Controls/Other/ScrollViewer.cs index 6cc7cf4e..9b0e794d 100644 --- a/src/Shared/HandyControl_Shared/Controls/Other/ScrollViewer.cs +++ b/src/Shared/HandyControl_Shared/Controls/Other/ScrollViewer.cs @@ -17,21 +17,6 @@ namespace HandyControl.Controls private bool _isRunning; - /// - /// 滚动方向 - /// - public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register( - "Orientation", typeof(Orientation), typeof(ScrollViewer), new PropertyMetadata(Orientation.Vertical)); - - /// - /// 滚动方向 - /// - public Orientation Orientation - { - get => (Orientation) GetValue(OrientationProperty); - set => SetValue(OrientationProperty, value); - } - /// /// 是否响应鼠标滚轮操作 /// @@ -53,7 +38,7 @@ namespace HandyControl.Controls if (!IsInertiaEnabled) { - if (Orientation == Orientation.Vertical) + if (ScrollViewerAttach.GetOrientation(this) == Orientation.Vertical) { base.OnMouseWheel(e); } @@ -68,7 +53,7 @@ namespace HandyControl.Controls } e.Handled = true; - if (Orientation == Orientation.Vertical) + if (ScrollViewerAttach.GetOrientation(this) == Orientation.Vertical) { if (!_isRunning) { diff --git a/src/Shared/HandyControl_Shared/Data/ValueBoxes.cs b/src/Shared/HandyControl_Shared/Data/ValueBoxes.cs index 42ad020b..8c25b761 100644 --- a/src/Shared/HandyControl_Shared/Data/ValueBoxes.cs +++ b/src/Shared/HandyControl_Shared/Data/ValueBoxes.cs @@ -1,4 +1,6 @@ -namespace HandyControl.Data +using System.Windows.Controls; + +namespace HandyControl.Data { /// /// 装箱后的值类型(用于提高效率) @@ -9,6 +11,10 @@ internal static object FalseBox = false; + internal static object VerticalBox = Orientation.Vertical; + + internal static object HorizontalBox = Orientation.Horizontal; + internal static object Double0Box = .0; internal static object Double01Box = .1; @@ -38,5 +44,8 @@ internal static object Int99Box = 99; internal static object BooleanBox(bool value) => value ? TrueBox : FalseBox; + + internal static object OrientationBox(Orientation value) => + value == Orientation.Horizontal ? HorizontalBox : VerticalBox; } } diff --git a/src/Shared/HandyControl_Shared/Themes/Styles/Base/TabControlBaseStyle.xaml b/src/Shared/HandyControl_Shared/Themes/Styles/Base/TabControlBaseStyle.xaml index 295213ee..a7db8ae9 100644 --- a/src/Shared/HandyControl_Shared/Themes/Styles/Base/TabControlBaseStyle.xaml +++ b/src/Shared/HandyControl_Shared/Themes/Styles/Base/TabControlBaseStyle.xaml @@ -291,7 +291,7 @@ - + diff --git a/src/Shared/HandyControl_Shared/Themes/Theme.xaml b/src/Shared/HandyControl_Shared/Themes/Theme.xaml index cf31f075..2945a9a8 100644 --- a/src/Shared/HandyControl_Shared/Themes/Theme.xaml +++ b/src/Shared/HandyControl_Shared/Themes/Theme.xaml @@ -10992,7 +10992,7 @@ - + From e5add880666d32cb4bc9bcb42089d089bc7e6035 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Tue, 21 Dec 2021 23:20:19 +0800 Subject: [PATCH 05/51] Update push-to-myget.yml --- .github/workflows/push-to-myget.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push-to-myget.yml b/.github/workflows/push-to-myget.yml index 825c0df3..f88696e9 100644 --- a/.github/workflows/push-to-myget.yml +++ b/.github/workflows/push-to-myget.yml @@ -15,7 +15,7 @@ jobs: run: | cd build .\build.ps1 - nuget pack build-for-myget.nuspec -version 3.3.0-beta.${env:GITHUB_RUN_NUMBER} + nuget pack build-for-myget.nuspec -version 3.4.0-beta.${env:GITHUB_SHA::7} $nupkgName = @(gci *.nupkg)[0].Name $mygetSourcePath = "https://www.myget.org/F/handycontrol" nuget setApiKey ${{ secrets.MYGETTOKEN }} -Source $mygetSourcePath From aaae79f651acffbabece9ab2c94fd2946c85a573 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Tue, 21 Dec 2021 23:35:47 +0800 Subject: [PATCH 06/51] Update push-to-myget.yml --- .github/workflows/push-to-myget.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push-to-myget.yml b/.github/workflows/push-to-myget.yml index f88696e9..4d8efec4 100644 --- a/.github/workflows/push-to-myget.yml +++ b/.github/workflows/push-to-myget.yml @@ -7,6 +7,12 @@ jobs: - name: checkout uses: actions/checkout@master + - name: Setup .NET Core SDK + uses: actions/setup-dotnet@master + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: setup nuget.exe uses: NuGet/setup-nuget@v1.0.5 @@ -15,7 +21,7 @@ jobs: run: | cd build .\build.ps1 - nuget pack build-for-myget.nuspec -version 3.4.0-beta.${env:GITHUB_SHA::7} + nuget pack build-for-myget.nuspec -version 3.4.0-beta.${env:GITHUB_RUN_NUMBER} $nupkgName = @(gci *.nupkg)[0].Name $mygetSourcePath = "https://www.myget.org/F/handycontrol" nuget setApiKey ${{ secrets.MYGETTOKEN }} -Source $mygetSourcePath From 97c43d625b72873ee9ec54d39aa4aa03dae5c2c7 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Wed, 22 Dec 2021 10:02:05 +0800 Subject: [PATCH 07/51] Update build-for-myget.nuspec --- build/build-for-myget.nuspec | 44 ++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/build/build-for-myget.nuspec b/build/build-for-myget.nuspec index a6377e91..936c6843 100644 --- a/build/build-for-myget.nuspec +++ b/build/build-for-myget.nuspec @@ -163,20 +163,34 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -218,4 +232,4 @@ - \ No newline at end of file + From d1e135816596bfc7d760c552c30eeda1827ead63 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Wed, 22 Dec 2021 10:02:48 +0800 Subject: [PATCH 08/51] Update build-for-myget.nuspec --- build/build-for-myget.nuspec | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/build/build-for-myget.nuspec b/build/build-for-myget.nuspec index 936c6843..e8ceffef 100644 --- a/build/build-for-myget.nuspec +++ b/build/build-for-myget.nuspec @@ -191,20 +191,6 @@ - - - - - - - - - - - - - - From 64b26cfc9dababa34940b93808a8e80cac87ba9d Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Wed, 22 Dec 2021 10:23:02 +0800 Subject: [PATCH 09/51] Update build-for-myget.nuspec --- build/build-for-myget.nuspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/build-for-myget.nuspec b/build/build-for-myget.nuspec index e8ceffef..1730accc 100644 --- a/build/build-for-myget.nuspec +++ b/build/build-for-myget.nuspec @@ -26,8 +26,8 @@ - - + + From b1f91cf9b25e05c76c7e2e1f94158dd84732ee9e Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Wed, 22 Dec 2021 21:52:40 +0800 Subject: [PATCH 10/51] chore: Update build-for-myget.nuspec --- build/build-for-myget.nuspec | 60 ++++++++++++++++++------------------ build/build.cake | 2 +- build/build.nuspec | 20 ++++++------ 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/build/build-for-myget.nuspec b/build/build-for-myget.nuspec index 1730accc..546faaef 100644 --- a/build/build-for-myget.nuspec +++ b/build/build-for-myget.nuspec @@ -26,8 +26,8 @@ - - + + @@ -163,34 +163,34 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/build.cake b/build/build.cake index f0e90dc2..1dbec896 100644 --- a/build/build.cake +++ b/build/build.cake @@ -42,7 +42,7 @@ Task("Build") { Configuration = "Release", Framework = framework, - OutputDirectory = $"lib/{framework}" + OutputDirectory = $"lib/{framework.Split('-')[0]}" }; DotNetCoreBuild("../src/Net_GE45/HandyControl_Net_GE45/HandyControl_Net_GE45.csproj", settings); diff --git a/build/build.nuspec b/build/build.nuspec index 497f7e3b..2c8b28f2 100644 --- a/build/build.nuspec +++ b/build/build.nuspec @@ -67,16 +67,16 @@ - - - - - - - - - - + + + + + + + + + + From 12c6c39424026d2536de0ee58ae7223e096f5531 Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Thu, 23 Dec 2021 22:07:01 +0800 Subject: [PATCH 11/51] chore: add new backer --- README-cn.md | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README-cn.md b/README-cn.md index 7cd2546a..693ef052 100644 --- a/README-cn.md +++ b/README-cn.md @@ -130,6 +130,7 @@ HandyControl 项目已加入 [dotNET China](https://gitee.com/dotnetchina) 组 icarousfff qiang9420d zhongwcool +zhongwcool ## 鸣谢 diff --git a/README.md b/README.md index 30a2f535..b0395975 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,7 @@ icarousfff qiang9420d zhongwcool +zhongwcool ## Special thanks to From 534f1ef961eada29a4edf913ff1fa112588c755c Mon Sep 17 00:00:00 2001 From: NaBian <836904362@qq.com> Date: Thu, 23 Dec 2021 23:06:23 +0800 Subject: [PATCH 12/51] enhance: add HighlightTextBrush prop for HighlightTextBlock --- .../HandyControl_Net_40/Themes/Theme.xaml | 5 +++++ .../UserControl/Main/LeftMainContent.xaml | 2 +- .../Controls/Text/HighlightTextBlock.cs | 22 ++++++++++++------- .../Styles/Base/TextBlockBaseStyle.xaml | 6 +++++ .../Themes/Styles/TextBlock.xaml | 3 +++ .../HandyControl_Shared/Themes/Theme.xaml | 5 +++++ 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml b/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml index a04e38c2..f925481d 100644 --- a/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml +++ b/src/Net_40/HandyControl_Net_40/Themes/Theme.xaml @@ -1306,6 +1306,10 @@ + + + diff --git a/src/Shared/HandyControl_Shared/Themes/Styles/TextBlock.xaml b/src/Shared/HandyControl_Shared/Themes/Styles/TextBlock.xaml index a1351cca..6dd2e4ed 100644 --- a/src/Shared/HandyControl_Shared/Themes/Styles/TextBlock.xaml +++ b/src/Shared/HandyControl_Shared/Themes/Styles/TextBlock.xaml @@ -1,4 +1,5 @@  @@ -49,4 +50,6 @@ + + + + diff --git a/src/Shared/HandyControl_Shared/Themes/Theme.xaml b/src/Shared/HandyControl_Shared/Themes/Theme.xaml index 3e3c1a5a..5949ea9c 100644 --- a/src/Shared/HandyControl_Shared/Themes/Theme.xaml +++ b/src/Shared/HandyControl_Shared/Themes/Theme.xaml @@ -4778,7 +4778,7 @@ - + @@ -4939,7 +4939,7 @@ - + @@ -5163,7 +5163,7 @@ - + @@ -5279,6 +5279,7 @@ + @@ -5362,7 +5363,7 @@ - - - + + - - + + - -