Wpf 自定义窗体

      用Wpf做客户端界面也有一段时间了,一直都直接使用的Window显示窗体,这几天闲来没事情,整理了下,自己做了一个自定义窗体。我自定义的窗体需要达到的细节效果包括:

      1、自定义边框粗细、颜色,窗体顶端不要有边框线,也就是说只有窗体左、右和底有边框,顶部是标题栏;

      2、实现圆角窗体,当具有圆角时,关闭按钮离窗体右侧边距为圆角值;

      3、标题栏有logo图标和标题栏文字,右侧有最小化、最大化和关闭按钮,需使用fontawesome字体图标,最大化按钮有切换图标效果;

      4、窗体最大化后不遮挡系统任务栏;

      网上度娘的文章基本都只针对某一个方面来说,我总结下做为我学习研究的一个小结,最终实现的效果如下图所示:

1

      资源字典

      我们先来看一下窗体的自定义资源xaml文件的代码,注意我是使用“自定义控件”创建这个自定义窗体,如下图所示,而不是“用户控件”,2者之间的差异是,“自定义控件”将xaml和cs代码分离,xaml文件名称为Generic.xaml,该文件被自动存放在一个叫做”Themes”的文件夹中,如下面第2张图所示。而通过“用户控件”选项创建的控件xaml和cs代码是归并在一起的,cs是后台代码。

1 2

      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="&#xf2d1;" Style="{StaticResource WindowBaseButton}" Padding="0,0,0,7"/>
                                    <Button x:Name="btnMax" Width="32" Content="&#xf2d0;" Style="{StaticResource WindowBaseButton}"/>
                                    <Button Content="&#xf00d;" 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>
posted @ 2018-11-16 10:58  alexywt  阅读(2996)  评论(12编辑  收藏  举报