用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>