自定义Window在商业桌面软件中十分常见,通常为了让软件界面整体更统一我们都会使用自定义的Window,再者自定义Window的上边缘区域提供了一片非常有用的空间。
这是来自WindowChrome 类 (System.Windows.Shell) | Microsoft Learn中的标准Window,
在WPF中一般有两种方法实现自定义Window,第一种是利用 WindowStyle="None"
和 AllowsTransparency="True
去除原有的 non-client area
(上图中除了Client area 外的区域,即Chrome),然后再在client area中定义Window的外观和行为,这种实现方式自由度很高,但是需要自己去实现Window原有的外观和行为;另一种实现方法则是利用 WindowChrome
来实现,WindowChrome
可以控制用户可操作/不可操作的区域,这种实现方式可以避免上一种方法的后遗症。
最终实现成这个样子(ZlaWindow):
相较标准Window ZlaWindow增加了 non-client area
的高度、背景颜色控制,在Window顶部区域的中间添加控件
先创建一个继承自Window的Control
using System.Windows; using System.Windows.Data; using System.Windows.Input; using System.Windows.Shell; namespace ZlaWindow { public class ZlaWindow : Window { public static readonly DependencyProperty NonClientAreaHeightProperty = DependencyProperty.Register( nameof(NonClientAreaHeight), typeof(double), typeof(ZlaWindow), new PropertyMetadata(35.0)); /// <summary> /// 顶部区域高度 /// </summary> public double NonClientAreaHeight { get => (double)GetValue(NonClientAreaHeightProperty); set => SetValue(NonClientAreaHeightProperty, value); } public static readonly DependencyProperty HeaderBarContentProperty = DependencyProperty.Register( nameof(HeaderBarContent), typeof(FrameworkElement), typeof(ZlaWindow), new PropertyMetadata(default(FrameworkElement))); /// <summary> /// 顶部区域中间的内容 /// </summary> public FrameworkElement HeaderBarContent { get => (FrameworkElement)GetValue(HeaderBarContentProperty); set => SetValue(HeaderBarContentProperty, value); } public ZlaWindow() { DefaultStyleKey = typeof(ZlaWindow); var chrome = new WindowChrome { CornerRadius = new CornerRadius(), //控制边框大小 GlassFrameThickness = new Thickness(5), ResizeBorderThickness = new Thickness(5), UseAeroCaptionButtons = false }; WindowChrome.SetWindowChrome(this, chrome); //CaptionHeight 为顶部区域高度 BindingOperations.SetBinding(chrome, WindowChrome.CaptionHeightProperty, new Binding(NonClientAreaHeightProperty.Name) { Source = this }); } public override void OnApplyTemplate() { base.OnApplyTemplate(); CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, (s, e) => WindowState = WindowState.Minimized)); CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, (s, e) => WindowState = WindowState.Maximized)); CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, (s, e) => WindowState = WindowState.Normal)); CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, (s, e) => Close())); } } }
在构造时设置了 WindowChrome
,通过绑定 NonClientAreaHeightProperty
控制 non-client area
的高度;另外提供 HeaderBarContent
给用户添加自定义控件在 non-client area
中;因为最大化最小化、关闭等按钮我们重新设计定义了,这里需要将这些Command添加到CommandBindings中
自定义Window的布局
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:behaviors="http://schemas.microsoft.com/xaml/behaviors" xmlns:local="clr-namespace:ZlaWindow"> <Style x:Key="CmdPathStyle" TargetType="Path"> <Setter Property="Width" Value="12" /> <Setter Property="Height" Value="12" /> <Setter Property="HorizontalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="Fill" Value="White" /> <Setter Property="UseLayoutRounding" Value="True" /> </Style> <Style x:Key="CmdButtonStyle" TargetType="{x:Type Button}"> <Setter Property="Background" Value="#32FFFFFF" /> <Setter Property="BorderThickness" Value="0" /> <Setter Property="Focusable" Value="False" /> <Setter Property="HorizontalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="WindowChrome.IsHitTestVisibleInChrome" Value="True" /> <Setter Property="IsTabStop" Value="False" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Grid x:Name="LayoutRoot" Background="Transparent"> <Rectangle x:Name="ButtonBackground" Fill="{TemplateBinding Background}" Opacity="0" /> <Border x:Name="ButtonBorder" SnapsToDevicePixels="true"> <ContentPresenter x:Name="TitleBarButtonContentPresenter" Margin="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="{TemplateBinding VerticalContentAlignment}" Focusable="False" RecognizesAccessKey="True" SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" /> </Border> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="true"> <Setter TargetName="ButtonBackground" Property="Opacity" Value="1" /> </Trigger> <Trigger Property="IsPressed" Value="True"> <Setter TargetName="ButtonBackground" Property="Opacity" Value="0.6" /> </Trigger> <Trigger Property="IsEnabled" Value="false"> <Setter TargetName="TitleBarButtonContentPresenter" Property="Opacity" Value=".5" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Path x:Key="RestoreIcon" Data="M1,3 L1,11 L9,11 L9,3 z M3,1 L3,2 L10,2 L10,9 L11,9 L11,1 z M2 ,0 L12,0 L12,10 L10,10 L10,12 L0,12 L0,2 L2 ,2 z" Style="{StaticResource CmdPathStyle}" /> <Path x:Key="CloseIcon" Data="M1,0 L6,5 L11,0 L12,1 L7,6 L12,11 L11,12 L6,7 L1,12 L0,11 L5,6 L0,1 z" Style="{StaticResource CmdPathStyle}" /> <Path x:Key="MaximizeIcon" Data="M1,1 L1 ,11 L11,11 L11,1 z M0,0 L12,0 L12,12 L0,12 z" Style="{StaticResource CmdPathStyle}" /> <Path x:Key="MinimizeIcon" Data="M0,6 L12,6 L12,7 L0,7 z" Style="{StaticResource CmdPathStyle}" /> <Style TargetType="{x:Type local:ZlaWindow}"> <Setter Property="Background" Value="White" /> <Setter Property="SnapsToDevicePixels" Value="True" /> <Setter Property="BorderBrush" Value="#4f4e4f" /> <Setter Property="Icon"> <Setter.Value> <BitmapImage UriSource="pack://application:,,,/ZlaWindow;component/Zla.png" /> </Setter.Value> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:ZlaWindow}"> <Border x:Name="WindowBorder" BorderThickness="0"> <Grid x:Name="LayoutRoot" Background="{TemplateBinding Background}"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <Grid x:Name="WindowTitlePanel" Grid.Row="0" Height="{TemplateBinding NonClientAreaHeight}" Background="{TemplateBinding BorderBrush}"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0" Margin="5,0,0,0" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal"> <Image Width="{x:Static SystemParameters.SmallIconWidth}" Height="{x:Static SystemParameters.SmallIconHeight}" Margin="5,0,0,0" VerticalAlignment="Center" Source="{TemplateBinding Icon}" WindowChrome.IsHitTestVisibleInChrome="True" /> <ContentControl Margin="5,0,0,0" VerticalAlignment="Center" Content="{TemplateBinding Title}" FontSize="16" Foreground="White" /> </StackPanel> <Grid Grid.Column="1" WindowChrome.IsHitTestVisibleInChrome="True"> <ContentPresenter VerticalAlignment="Center" Content="{Binding HeaderBarContent, RelativeSource={RelativeSource TemplatedParent}, Mode=OneWay}" Focusable="False" /> </Grid> <!-- IsHitTestVisibleInChrome设置此元素在chrome中可以交互 --> <StackPanel Grid.Column="2" Background="Transparent" Orientation="Horizontal" WindowChrome.IsHitTestVisibleInChrome="True"> <Button Name="ButtonMinimize" Width="{TemplateBinding NonClientAreaHeight}" Height="{TemplateBinding NonClientAreaHeight}" Command="{Binding Source={x:Static SystemCommands.MinimizeWindowCommand}}" Content="{StaticResource MinimizeIcon}" Style="{StaticResource CmdButtonStyle}" /> <Grid Name="SizeChangeButtons"> <Button Name="ButtonRestore" Width="{TemplateBinding NonClientAreaHeight}" Height="{TemplateBinding NonClientAreaHeight}" Command="{Binding Source={x:Static SystemCommands.RestoreWindowCommand}}" Content="{StaticResource RestoreIcon}" Style="{StaticResource CmdButtonStyle}" Visibility="Collapsed" /> <Button Name="ButtonMax" Width="{TemplateBinding NonClientAreaHeight}" Height="{TemplateBinding NonClientAreaHeight}" Command="{Binding Source={x:Static SystemCommands.MaximizeWindowCommand}}" Content="{StaticResource MaximizeIcon}" Style="{StaticResource CmdButtonStyle}" /> </Grid> <Button Name="ButtonClose" Width="{TemplateBinding NonClientAreaHeight}" Height="{TemplateBinding NonClientAreaHeight}" Background="Red" Command="{Binding Source={x:Static SystemCommands.CloseWindowCommand}}" Content="{StaticResource CloseIcon}" Style="{StaticResource CmdButtonStyle}" /> </StackPanel> </Grid> <AdornerDecorator Grid.Row="1" KeyboardNavigation.IsTabStop="False"> <ContentPresenter ClipToBounds="True" /> </AdornerDecorator> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="WindowState" Value="Maximized"> <Setter TargetName="ButtonMax" Property="Visibility" Value="Collapsed" /> <Setter TargetName="ButtonRestore" Property="Visibility" Value="Visible" /> <Setter TargetName="WindowBorder" Property="Padding" Value="{x:Static SystemParameters.WindowResizeBorderThickness}" /> <Setter TargetName="LayoutRoot" Property="Margin" Value="{x:Static local:WindowParameters.PaddedBorderThickness}" /> </Trigger> <Trigger Property="WindowState" Value="Normal"> <Setter TargetName="ButtonMax" Property="Visibility" Value="Visible" /> <Setter TargetName="ButtonRestore" Property="Visibility" Value="Collapsed" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
另外提供一个WindowParameter来解决最大化存在的问题,关于这个问题可以参考一下这篇文章([WPF 自定义控件]使用WindowChrome的问题 – dino.c – 博客园 (cnblogs.com))
using System; using System.Reflection; using System.Runtime.InteropServices; using System.Security; using System.Windows; using System.Windows.Media; namespace ZlaWindow { public static class WindowParameters { private static Thickness? _paddedBorderThickness; [ThreadStatic] private static Matrix _transformToDip; /// <summary> /// returns the border thickness padding around captioned windows,in pixels. Windows XP/2000: This value is not supported. /// </summary> public static Thickness PaddedBorderThickness { [SecurityCritical] get { if (_paddedBorderThickness == null) { var paddedBorder = NativeMethods.GetSystemMetrics(SM.CXPADDEDBORDER); var dpi = GetDpi(); Size frameSize = new Size(paddedBorder, paddedBorder); Size frameSizeInDips = DeviceSizeToLogical(frameSize, dpi / 96.0, dpi / 96.0); _paddedBorderThickness = new Thickness(frameSizeInDips.Width, frameSizeInDips.Height, frameSizeInDips.Width, frameSizeInDips.Height); } return _paddedBorderThickness.Value; } } /// <summary> /// Get Dpi /// </summary> /// <returns>Return 96,144</returns> public static double GetDpi() { var dpiXProperty = typeof(SystemParameters).GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static); var dpiX = (int)dpiXProperty.GetValue(null, null); return dpiX; } /// <summary> /// Convert a point in system coordinates to a point in device independent pixels (1/96"). /// </summary> /// <param name="devicePoint">A point in the physical coordinate system.</param> /// <param name="dpiScaleX">dpiScaleX</param> /// <param name="dpiScaleY">dpiScaleY</param> /// <returns>Returns the parameter converted to the device independent coordinate system.</returns> public static Point DevicePixelsToLogical(Point devicePoint, double dpiScaleX, double dpiScaleY) { _transformToDip = Matrix.Identity; _transformToDip.Scale(1d / dpiScaleX, 1d / dpiScaleY); return _transformToDip.Transform(devicePoint); } public static Size DeviceSizeToLogical(Size deviceSize, double dpiScaleX, double dpiScaleY) { Point pt = DevicePixelsToLogical(new Point(deviceSize.Width, deviceSize.Height), dpiScaleX, dpiScaleY); return new Size(pt.X, pt.Y); } } /// <summary> /// https://msdn.microsoft.com/en-us/library/windows/desktop/ms724385(v=vs.85).aspx /// Retrieves the specified system metric or system configuration setting. /// Note that all dimensions retrieved by GetSystemMetrics are in pixels. /// </summary> public enum SM { /// <summary> /// The amount of border padding for captioned windows, in pixels. /// Returns the amount of extra border padding around captioned windows /// Windows XP/2000: This value is not supported. /// </summary> CXPADDEDBORDER = 92, } internal static class NativeMethods { [DllImport("user32.dll")] internal static extern int GetSystemMetrics(SM nIndex); } }
最大化问题
第一张是使用了PaddedBorderThickness
的,第二张没有使用,看出差异了吗?注意看边缘