用Wpf做客户端界面也有一段时间了,一直都直接使用的Window显示窗体,这几天闲来没事情,整理了下,自己做了一个自定义窗体。我自定义的窗体需要达到的细节效果包括:
1、自定义边框粗细、颜色,窗体顶端不要有边框线,也就是说只有窗体左、右和底有边框,顶部是标题栏;
2、实现圆角窗体,当具有圆角时,关闭按钮离窗体右侧边距为圆角值;
3、标题栏有logo图标和标题栏文字,右侧有最小化、最大化和关闭按钮,需使用fontawesome字体图标,最大化按钮有切换图标效果;
4、窗体最大化后不遮挡系统任务栏;
网上度娘的文章基本都只针对某一个方面来说,我总结下做为我学习研究的一个小结,最终实现的效果如下图所示:
资源字典
我们先来看一下窗体的自定义资源xaml文件的代码,注意我是使用“自定义控件”创建这个自定义窗体,如下图所示,而不是“用户控件”,2者之间的差异是,“自定义控件”将xaml和cs代码分离,xaml文件名称为Generic.xaml,该文件被自动存放在一个叫做”Themes”的文件夹中,如下面第2张图所示。而通过“用户控件”选项创建的控件xaml和cs代码是归并在一起的,cs是后台代码。
Generic.xaml代码
代码首先通过xmlns:local="clr-namespace:youplus.OA.WpfApp"引入名称空间,该空间下我们定义了WindowBase.cs的代码;通过xmlns:converter="clr-namespace:youplus.OA.WpfApp.Converter"引入值转换器。
然后定义了3个值转换器用于转换边框粗细、圆角半径、关闭按钮右侧边距的值。然后引入了FontAwesome字体,最小化、最大化、关闭按钮是使用的该字体里的对应项。然后定义了这几个按钮所使用的样式。最后是WindowBase窗体的自定义模板。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:youplus.OA.WpfApp" xmlns:converter="clr-namespace:youplus.OA.WpfApp.Converter"> <converter:WindowBaseBorderThicknessConverter x:Key="BorderThicknessConverter"/> <converter:WindowBaseCornerRadiusConverter x:Key="CornerRadiusConverter"/> <converter:WindowBaseCloseMarginRightConverter x:Key="CloseMarginRightConverter"/> <Style x:Key="FontAwesome" > <Setter Property="TextElement.FontFamily" Value="pack://application:,,,/Resources/#FontAwesome" /> <Setter Property="TextElement.FontSize" Value="11" /> </Style> <Style x:Key="WindowBaseButton" TargetType="{x:Type Button}"> <Setter Property="Background" Value="Transparent"/> <Setter Property="Foreground" Value="Black"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Button}"> <Border BorderThickness="{TemplateBinding BorderThickness}" Background="{TemplateBinding Background}" > <ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" Margin="{TemplateBinding Padding}" /> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="true"> <Setter Property="Background" Value="#c75050"/> <Setter Property="Foreground" Value="White"/> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="{x:Type local:WindowBase}"> <Setter Property="AllowsTransparency" Value="True" /> <Setter Property="WindowStyle" Value="None"/> <Setter Property="ResizeMode" Value="CanMinimize"/> <Setter Property="BorderBrush" Value="#6fbdd1" /> <Setter Property="CornerRadius" Value="2" /> <Setter Property="BorderThickness" Value="4"/> <Setter Property="Background" Value="White"/> <Setter Property="HeaderHeight" Value="40"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:WindowBase}"> <Grid Name="root" Style="{StaticResource FontAwesome}"> <Grid.RowDefinitions> <RowDefinition Height="{Binding RelativeSource={RelativeSource TemplatedParent},Path=HeaderHeight}"/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition/> </Grid.ColumnDefinitions> <Border Name="header" Background="{TemplateBinding BorderBrush}" CornerRadius="{Binding Path=CornerRadius, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource CornerRadiusConverter}, ConverterParameter=header}" BorderThickness="0"> <DockPanel Height="Auto"> <StackPanel VerticalAlignment="Center" Orientation="Horizontal" DockPanel.Dock="Left"> <Image Source="{TemplateBinding Icon}" MaxHeight="20" MaxWidth="20" Margin="10,0,0,0"/> <TextBlock Text="{TemplateBinding Title}" FontSize="14" FontFamily="Microsoft Yihi" VerticalAlignment="Center" Margin="6,0,0,0"></TextBlock> </StackPanel> <StackPanel DockPanel.Dock="Right" Height="32" HorizontalAlignment="Right" VerticalAlignment="Top" Orientation="Horizontal"> <Button x:Name="btnMin" Width="32" Content="" Style="{StaticResource WindowBaseButton}" Padding="0,0,0,7"/> <Button x:Name="btnMax" Width="32" Content="" Style="{StaticResource WindowBaseButton}"/> <Button Content="" x:Name="btnClose" Width="32" Margin="{Binding Path=CornerRadius,RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource CloseMarginRightConverter}}" Style="{StaticResource WindowBaseButton}"/> </StackPanel> </DockPanel> </Border> <Border Grid.Row="1" CornerRadius="{Binding Path=CornerRadius,RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource CornerRadiusConverter}, ConverterParameter=content}" BorderThickness="{TemplateBinding BorderThickness,Converter={StaticResource BorderThicknessConverter}}" BorderBrush="{TemplateBinding BorderBrush}" Background="{TemplateBinding Background}" DockPanel.Dock="Top" Height="Auto"> <AdornerDecorator> <ContentPresenter /> </AdornerDecorator> </Border> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
WindowBase.cs
接下来我们看看WindowBase的代码,它继承自Window,自定义了HeaderHeight和CornerRadius两个依赖项属性,从而可以在以上的xaml代码中配置2个属性。在静态WindowBase构造函数中我们要完成依赖项属性的注册,在实例WindowBase构造函数中我们监听SystemParameters.StaticPropertyChanged事件,从而可以使窗体最大化时不覆盖系统任务栏。最后通过覆盖父类的OnApplyTemplate事件代码,来为几个按钮配置状态和事件。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; namespace youplus.OA.WpfApp { public class WindowBase : Window { private static DependencyProperty HeaderHeightProperty; public int HeaderHeight { get => (int)GetValue(HeaderHeightProperty); set => SetValue(HeaderHeightProperty, value); } private static int maxCornerRadius = 10; public static DependencyProperty CornerRadiusProperty; public int CornerRadius { get => (int)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } static WindowBase() { DefaultStyleKeyProperty.OverrideMetadata(typeof(WindowBase), new FrameworkPropertyMetadata(typeof(WindowBase))); FrameworkPropertyMetadata metadata = new FrameworkPropertyMetadata(); metadata.Inherits = true; metadata.DefaultValue = 2; metadata.AffectsMeasure = true; metadata.PropertyChangedCallback += (d,e)=> { }; CornerRadiusProperty = DependencyProperty.Register("CornerRadius", typeof(int), typeof(WindowBase), metadata, o => { int radius = (int)o; if (radius >= 0 && radius <= maxCornerRadius) return true; return false; }); metadata = new FrameworkPropertyMetadata(); metadata.Inherits = true; metadata.DefaultValue = 40; metadata.AffectsMeasure = true; metadata.PropertyChangedCallback += (d, e) => { }; HeaderHeightProperty = DependencyProperty.Register("HeaderHeight", typeof(int), typeof(WindowBase), metadata, o => { int radius = (int)o; if (radius >= 0 && radius <= 1000) return true; return false; }); } public WindowBase() : base() { SystemParameters.StaticPropertyChanged -= SystemParameters_StaticPropertyChanged; SystemParameters.StaticPropertyChanged += SystemParameters_StaticPropertyChanged; } private void SystemParameters_StaticPropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName == "WorkArea") { if (this.WindowState == WindowState.Maximized) { double top = SystemParameters.WorkArea.Top; double left = SystemParameters.WorkArea.Left; double right = SystemParameters.PrimaryScreenWidth - SystemParameters.WorkArea.Right; double bottom = SystemParameters.PrimaryScreenHeight - SystemParameters.WorkArea.Bottom; root.Margin = new Thickness(left, top, right, bottom); } } } private double normaltop; private double normalleft; private double normalwidth; private double normalheight; private Grid root; private Button minBtn; private Button maxBtn; private Button closeBtn; private Border header; public override void OnApplyTemplate() { base.OnApplyTemplate(); minBtn = (Button)Template.FindName("btnMin", this); minBtn.Click += (o, e) => WindowState = WindowState.Minimized; maxBtn = (Button)Template.FindName("btnMax", this); root = (Grid)Template.FindName("root",this); maxBtn.Click += (o, e) => { if (WindowState == WindowState.Normal) { normaltop = this.Top; normalleft = this.Left; normalwidth = this.Width; normalheight = this.Height; double top = SystemParameters.WorkArea.Top; double left = SystemParameters.WorkArea.Left; double right = SystemParameters.PrimaryScreenWidth - SystemParameters.WorkArea.Right; double bottom = SystemParameters.PrimaryScreenHeight - SystemParameters.WorkArea.Bottom; root.Margin = new Thickness(left, top, right, bottom); WindowState = WindowState.Maximized; maxBtn.Content = "xf2d2"; } else { WindowState = WindowState.Normal; maxBtn.Content = "xf2d0"; Top = 0; Left = 0; Width = 0; Height = 0; this.Top = normaltop; this.Left = normalleft; this.Width = normalwidth; this.Height = normalheight; root.Margin = new Thickness(0); } }; closeBtn = (Button)Template.FindName("btnClose", this); closeBtn.Click += (o, e) => Close(); header = (Border)Template.FindName("header", this); header.MouseMove += (o, e) => { if (e.LeftButton == MouseButtonState.Pressed) { this.DragMove(); } }; header.MouseLeftButtonDown += (o, e) => { if (e.ClickCount >= 2) { maxBtn.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); } }; } } }
值转换器
接下来我们看看值转换器的代码,值转换器有三个,1、窗体的边框只有左、下、右三面有,我们需要将配置给窗体的边框设置去掉顶部的边框设置后,配置给WindowBase内部的Border元素,该转换操作通过WindowBaseBorderThicknessConverter完成。2、窗体可能具有圆角,关闭按钮需要与窗体右边缘保持圆角指定值的边距,此时需要从Int型的圆角值转换为Thickness类型的边距,这是通过WindowBaseCloseMarginRightConverter实现的。3、最后一个转换器将Int型的圆角值转换为各个内部Border控件的CornerRadius。
using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Data; namespace youplus.OA.WpfApp.Converter { [ValueConversion(typeof(Thickness),typeof(Thickness))] public class WindowBaseBorderThicknessConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { Thickness t = (Thickness)value; return new Thickness(t.Left,0,t.Right,t.Bottom); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } [ValueConversion(typeof(int), typeof(Thickness))] public class WindowBaseCloseMarginRightConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int v = (int)value; return new Thickness(0, 0, v, 0); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } [ValueConversion(typeof(int), typeof(CornerRadius))] public class WindowBaseCornerRadiusConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int v = (int)value; string p = parameter.ToString().Trim().ToLower(); if (p == "header") return new CornerRadius(v, v, 0, 0); else if(p== "btnclose") return new CornerRadius(0, v, 0, 0); else return new CornerRadius(0, 0, v, v); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); } } }
WindowBase的使用
接下来我们就需要将以上的自定义窗体应用到我们的MainWindow窗体上了,实例xaml代码如下所示
<local:WindowBase x:Class="youplus.OA.WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:youplus.OA.WpfApp" mc:Ignorable="d" Title="自定义窗体测试" CornerRadius="10" Height="311" Width="493" Icon="Resources/logo.ico" WindowStartupLocation="CenterScreen"> <Grid> </Grid> </local:WindowBase>