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>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM