From d2698c5efa0b79a392622faceb997bdfdbca0748 Mon Sep 17 00:00:00 2001 From: ilyfairy Date: Thu, 29 Aug 2024 22:46:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20WindowContentAdapt?= =?UTF-8?q?er=20=E4=BB=A5=E5=9C=A8=E4=BD=BF=E7=94=A8=20WindowChrome=20?= =?UTF-8?q?=E6=97=B6=E6=AD=A3=E7=A1=AE=20Padding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/WindowContentAdapter.cs | 185 +++++++++++++++++ EleCho.WpfSuite/Utilities/Dpi.cs | 40 ++++ WpfTest/MainWindow.xaml | 186 +++++++++--------- 3 files changed, 319 insertions(+), 92 deletions(-) create mode 100644 EleCho.WpfSuite/Controls/WindowContentAdapter.cs create mode 100644 EleCho.WpfSuite/Utilities/Dpi.cs diff --git a/EleCho.WpfSuite/Controls/WindowContentAdapter.cs b/EleCho.WpfSuite/Controls/WindowContentAdapter.cs new file mode 100644 index 0000000..8f571f6 --- /dev/null +++ b/EleCho.WpfSuite/Controls/WindowContentAdapter.cs @@ -0,0 +1,185 @@ +// Origin code from: https://github.com/walterlv/Walterlv.Packages/blob/master/src/Themes/Walterlv.Themes.FluentDesign/Controls/ClientAreaBorder.cs +// Changes: +// - Inherit from Decorator instead of Border +// - Make 'Padding' property read-only +// - Remove cache of properties + +using System; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Shell; + +namespace EleCho.WpfSuite +{ + /// + /// If you're using to extend the interface to non-client area, then you can add this container to the content of in order to have the internal content automatically populate the client area section. + /// Using this container eliminates the need for the various margin adaptations done by Setter/Trigger inside the style when the window state changes. + /// + public class WindowContentAdapter : Decorator + { +#pragma warning disable IDE1006 // 命名样式 +#pragma warning disable IDE0052 // 删除未读的私有成员 + private const int SM_CXFRAME = 32; + private const int SM_CYFRAME = 33; + private const int SM_CXPADDEDBORDER = 92; +#pragma warning restore IDE0052 // 删除未读的私有成员 +#pragma warning restore IDE1006 // 命名样式 + + [DllImport("User32", ExactSpelling = true)] + private static extern int GetSystemMetrics(int nIndex); + + private Window? _nowWindow; + + /// + /// Padding of the client area. + /// + public Thickness Padding => (Thickness)GetValue(PaddingPropertyKey.DependencyProperty); + + private static readonly DependencyPropertyKey PaddingPropertyKey = DependencyProperty.RegisterReadOnly( + nameof(Padding), typeof(Thickness), typeof(WindowContentAdapter), + new FrameworkPropertyMetadata(default(Thickness), FrameworkPropertyMetadataOptions.AffectsParentMeasure | FrameworkPropertyMetadataOptions.AffectsParentArrange)); + + /// + /// DependencyProperty of . + /// + public static readonly DependencyProperty PaddingProperty = PaddingPropertyKey.DependencyProperty; + + /// + protected override void OnVisualParentChanged(DependencyObject oldParent) + { + base.OnVisualParentChanged(oldParent); + + if (_nowWindow is { } oldWindow) + { + oldWindow.StateChanged -= Window_StateChanged; + } + + var newWindow = (Window?)Window.GetWindow(this); + if (newWindow is not null) + { + newWindow.StateChanged -= Window_StateChanged; + newWindow.StateChanged += Window_StateChanged; + } + + _nowWindow = newWindow; + + UpdatePadding(_nowWindow); + } + + /// + protected override Size MeasureOverride(Size constraint) + { + var padding = Padding; + + if (Child is { } child) + { + child.Measure(new Size( + constraint.Width - padding.Left - padding.Right, + constraint.Height - padding.Top - padding.Bottom)); + + var finalSize = new Size( + child.DesiredSize.Width + padding.Left + padding.Right, + child.DesiredSize.Height + padding.Top + padding.Bottom); + + finalSize.Width = Math.Min(finalSize.Width, constraint.Width); + finalSize.Height = Math.Min(finalSize.Height, constraint.Height); + + return finalSize; + } + else + { + var finalSize = new Size(padding.Left + padding.Right, padding.Top + padding.Bottom); + + finalSize.Width = Math.Min(finalSize.Width, constraint.Width); + finalSize.Height = Math.Min(finalSize.Height, constraint.Height); + + return finalSize; + } + } + + /// + protected override Size ArrangeOverride(Size arrangeSize) + { + var padding = Padding; + if (Child is { } child) + { + child.Arrange(new Rect( + padding.Left, + padding.Top, + arrangeSize.Width - padding.Left - padding.Right, + arrangeSize.Height - padding.Top - padding.Bottom)); + } + return arrangeSize; + } + + private void UpdatePadding(Window? window) + { + if (window is null || + WindowChrome.GetWindowChrome(window) is null) + { + SetValue(PaddingPropertyKey, default(Thickness)); + return; + } + + var padding = window.WindowState switch + { + WindowState.Maximized => WindowChromeNonClientFrameThickness, + _ => default, + }; + + SetValue(PaddingPropertyKey, padding); + } + + private void Window_StateChanged(object? sender, EventArgs e) + { + if (sender is not Window window) + { + return; + } + + UpdatePadding(window); + } + + /// + /// 获取系统的 作为 WPF 单位的边框数值。 + /// + private Thickness PaddedBorderThickness + { + get + { + var paddedBorder = GetSystemMetrics(SM_CXPADDEDBORDER); + var dpi = GetDpi(); + var frameSize = new Size(paddedBorder, paddedBorder); + var frameSizeInDips = new Size(frameSize.Width / dpi.FactorX, frameSize.Height / dpi.FactorY); + return new Thickness(frameSizeInDips.Width, frameSizeInDips.Height, frameSizeInDips.Width, frameSizeInDips.Height); + } + } + + /// + /// 获取系统的 作为 WPF 单位的边框数值。 + /// + private Thickness ResizeFrameBorderThickness => new Thickness( + SystemParameters.ResizeFrameVerticalBorderWidth, + SystemParameters.ResizeFrameHorizontalBorderHeight, + SystemParameters.ResizeFrameVerticalBorderWidth, + SystemParameters.ResizeFrameHorizontalBorderHeight); + + /// + /// 如果使用了 来制作窗口样式以将窗口客户区覆盖到非客户区,那么就需要自己来处理窗口最大化后非客户区的边缘被裁切的问题。 + /// 使用此属性获取窗口最大化时窗口样式应该内缩的边距数值,这样在窗口最大化时客户区便可以在任何 DPI 下不差任何一个像素地完全覆盖屏幕工作区。 + /// 方法无法直接获得这个数值。 + /// + private Thickness WindowChromeNonClientFrameThickness => new Thickness( + ResizeFrameBorderThickness.Left + PaddedBorderThickness.Left, + ResizeFrameBorderThickness.Top + PaddedBorderThickness.Top, + ResizeFrameBorderThickness.Right + PaddedBorderThickness.Right, + ResizeFrameBorderThickness.Bottom + PaddedBorderThickness.Bottom); + + private Dpi GetDpi() => PresentationSource.FromVisual(this) is { } source + ? new Dpi( + (int)(Dpi.Standard.X * source.CompositionTarget.TransformToDevice.M11), + (int)(Dpi.Standard.Y * source.CompositionTarget.TransformToDevice.M22)) + : Dpi.System; + } +} diff --git a/EleCho.WpfSuite/Utilities/Dpi.cs b/EleCho.WpfSuite/Utilities/Dpi.cs new file mode 100644 index 0000000..a8d5dd0 --- /dev/null +++ b/EleCho.WpfSuite/Utilities/Dpi.cs @@ -0,0 +1,40 @@ +using System.Runtime.InteropServices; + +namespace EleCho.WpfSuite +{ + /// + /// DPI + /// + /// Horizontal value + /// Vertical value + public record struct Dpi(int X, int Y) + { + /// + /// Horizontal factor to standard DPI + /// + public double FactorX => (double)X / Standard.X; + + /// + /// Vertical factor to standard DPI + /// + public double FactorY => (double)Y / Standard.Y; + + /// + /// Standard DPI + /// + public static Dpi Standard => new Dpi(96, 96); + + /// + /// System DPI + /// + public static Dpi System => new Dpi(GetDeviceCaps(default, LOGPIXELSX), GetDeviceCaps(default, LOGPIXELSY)); + + + + const int LOGPIXELSX = 88; + const int LOGPIXELSY = 90; + + [DllImport("Gdi32", ExactSpelling = true)] + private static extern int GetDeviceCaps(nint hDC, int index); + } +} diff --git a/WpfTest/MainWindow.xaml b/WpfTest/MainWindow.xaml index 01292a9..b8ba4a1 100644 --- a/WpfTest/MainWindow.xaml +++ b/WpfTest/MainWindow.xaml @@ -24,28 +24,29 @@ - - + + - - - + - - - + + - - - + + - - - - - - - - - - - - + + + + + + + + + - - + + - - - - - - - + + + + + + - - + - - + + - - - - - + + + + - - - - - + + + + - - - - - + + + + - - - - - + + + + - - - - - - + + + + + - - - - - - - - - + + + + + + + + - - - + + - - - - - - - + + + + + + - - - - + + + +