WPF 自定义控件

当现有控件不满足业务需要时,WPF也支持自定义控件。以下提供一个评分控件以供学习

<!--StarRating.xaml-->
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ctrls="clr-namespace:CustomControlDemo.Controls">
    <!--没有 x:Key:默认样式,自动应用到所有 StarRating-->
    <Style TargetType="{x:Type ctrls:StarRating}">
            <Setter Property="HorizontalAlignment" Value="Left"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate TargetType="{x:Type ctrls:StarRating}">
						<!--①ItemsControl ItemsSource="{Binding Stars, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"-->
						<!--②ItemsControl ItemsSource="{TemplateBinding Stars}"-->
						<!--①②与下面这行等价,不过用TemplateBinding和TemplatedParent时要求父节点为ControlTemplate-->
                        <ItemsControl ItemsSource="{Binding Stars, RelativeSource={RelativeSource TemplatedParent}}">
						<!--ItemsSource要求Stars是一个集合,比如List,Dictionary,ObservableCollection等-->
                            <ItemsControl.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <StackPanel Orientation="Horizontal"/>
                                </ItemsPanelTemplate>
                            </ItemsControl.ItemsPanel>
                            <ItemsControl.ItemTemplate>
                                <DataTemplate>
                                <!--Path data用于描述控件形状,本例中11个点坐标围成五角星-->
                                    <Path Data="M12,17.27L18.18,21L16.54,13.97L22,9.24L14.81,8.62L12,2L9.19,8.62L2,9.24L7.45,13.97L5.82,21L12,17.27Z"
                                      Width="{Binding StarSize, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"
                                      Height="{Binding StarSize, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"
                                      Margin="2"
                                      Cursor="Hand">
                                        <Path.Style>
                                            <Style TargetType="Path">
                                                <Setter Property="Fill" Value="{Binding EmptyStarBrush, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"/>
                                                <Style.Triggers>
                                                    <DataTrigger Binding="{Binding}" Value="True">
                                                        <Setter Property="Fill" Value="{Binding FilledStarBrush, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"/>
                                                    </DataTrigger>
                                                </Style.Triggers>
                                            </Style>
                                        </Path.Style>
                                    </Path>
                                </DataTemplate>
                            </ItemsControl.ItemTemplate>
                        </ItemsControl>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
 
        <Style x:Key="BlueTheme" TargetType="{x:Type ctrls:StarRating}" BasedOn="{StaticResource {x:Type ctrls:StarRating}}">
            <Setter Property="FilledStarBrush" Value="#2196F3"/>
            <Setter Property="EmptyStarBrush" Value="#BBDEFB"/>
        </Style>
 
        <Style x:Key="RedLargeTheme" TargetType="{x:Type ctrls:StarRating}" BasedOn="{StaticResource {x:Type ctrls:StarRating}}">
            <Setter Property="FilledStarBrush" Value="#F44336"/>
            <Setter Property="EmptyStarBrush" Value="#FFCDD2"/>
            <Setter Property="StarSize" Value="50"/>
        </Style>
 
    <Style x:Key="CircleTheme" TargetType="{x:Type ctrls:StarRating}">
        <Setter Property="HorizontalAlignment" Value="Left"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type ctrls:StarRating}">
                    <ItemsControl ItemsSource="{Binding Stars, RelativeSource={RelativeSource TemplatedParent}}">
                        <ItemsControl.ItemsPanel>
                            <ItemsPanelTemplate>
                                <StackPanel Orientation="Horizontal"/>
                            </ItemsPanelTemplate>
                        </ItemsControl.ItemsPanel>
                        <ItemsControl.ItemTemplate>
                            <DataTemplate>
                                <Ellipse Width="{Binding StarSize, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"
                                         Height="{Binding StarSize, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"
                                         Margin="3"
                                         Cursor="Hand">
                                    <Ellipse.Style>
                                        <Style TargetType="Ellipse">
                                            <Setter Property="Fill" Value="{Binding EmptyStarBrush, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"/>
                                            <Setter Property="Stroke" Value="Gray"/>
                                            <Setter Property="StrokeThickness" Value="2"/>
                                            <Style.Triggers>
                                                <!--Binding="{Binding}"意思是绑定到当前默认的DataContext,在此例中就是Stars中的每一个Bool元素-->
                                                <DataTrigger Binding="{Binding}" Value="True">
                                                    <Setter Property="Fill" Value="{Binding FilledStarBrush, RelativeSource={RelativeSource AncestorType=ctrls:StarRating}}"/>
                                                </DataTrigger>
                                            </Style.Triggers>
                                        </Style>
                                    </Ellipse.Style>
                                </Ellipse>
                            </DataTemplate>
                        </ItemsControl.ItemTemplate>
                    </ItemsControl>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
 
</ResourceDictionary>



//StarRating.cs
using System;
using System.Collections.ObjectModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace CustomControlDemo.Controls
{
    public class StarRating : Control
    {
        public static readonly DependencyProperty RatingProperty =
            DependencyProperty.Register(
                "Rating",
                typeof(int),
                typeof(StarRating),
                new FrameworkPropertyMetadata(0, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnRatingChanged));

        public static readonly DependencyProperty MaxRatingProperty =
            DependencyProperty.Register(
                "MaxRating",
                typeof(int),
                typeof(StarRating),
                new PropertyMetadata(1, OnRatingChanged));

        public static readonly DependencyProperty FilledStarBrushProperty =
            DependencyProperty.Register(
                "FilledStarBrush",
                typeof(Brush),
                typeof(StarRating),
                new PropertyMetadata(Brushes.Gold));

        public static readonly DependencyProperty EmptyStarBrushProperty =
            DependencyProperty.Register(
                "EmptyStarBrush",
                typeof(Brush),
                typeof(StarRating),
                new PropertyMetadata(Brushes.LightGray));

        public static readonly DependencyProperty StarSizeProperty =
            DependencyProperty.Register(
                "StarSize",
                typeof(double),
                typeof(StarRating),
                new PropertyMetadata(30.0));

        // 星星列表,直接供 ItemsControl 绑定
        public ObservableCollection<bool> Stars { get; } = new ObservableCollection<bool>();

        public event EventHandler<int> RatingChanged;

        static StarRating()
        {
            //注册StarRating的默认Style和ControlTemplate, WPF将会去Generic.xaml中寻找
            //要求StarRating必须提供默认的Style和ControlTemplate。
            //如果StarRating没有提供,则不用注册,WPF将从StarRating父类查询匹配的Style
            DefaultStyleKeyProperty.OverrideMetadata(
                typeof(StarRating),
                new FrameworkPropertyMetadata(typeof(StarRating)));
        }

        public StarRating()
        {
            UpdateStars();
        }

        public int Rating
        {
            get => (int)GetValue(RatingProperty);
            set => SetValue(RatingProperty, value);
        }

        public int MaxRating
        {
            get => (int)GetValue(MaxRatingProperty);
            set => SetValue(MaxRatingProperty, value);
        }

        public Brush FilledStarBrush
        {
            get => (Brush)GetValue(FilledStarBrushProperty);
            set => SetValue(FilledStarBrushProperty, value);
        }

        public Brush EmptyStarBrush
        {
            get => (Brush)GetValue(EmptyStarBrushProperty);
            set => SetValue(EmptyStarBrushProperty, value);
        }

        public double StarSize
        {
            get => (double)GetValue(StarSizeProperty);
            set => SetValue(StarSizeProperty, value);
        }

        private static void OnRatingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var control = (StarRating)d;
            control.UpdateStars();
        }

        // Rating 或 MaxRating 变化时,更新星星列表
        private void UpdateStars()
        {
            Stars.Clear();
            for (int i = 1; i <= MaxRating; i++)
            {
                Stars.Add(i <= Rating);
            }
        }

        protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
        {
            base.OnMouseLeftButtonDown(e);

            var position = e.GetPosition(this);
            var starWidth = ActualWidth / MaxRating;
            var clickedStar = (int)(position.X / starWidth) + 1;

            if (clickedStar >= 1 && clickedStar <= MaxRating)
            {
                Rating = clickedStar;
                RatingChanged?.Invoke(this, clickedStar);
            }
        }
    }
}



<!--Mainwindow.xaml-->
<Window x:Class="CustomControlDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:ctrls="clr-namespace:CustomControlDemo.Controls"
        xmlns:local="clr-namespace:CustomControlDemo"
        Title="CustomControl 示例" Height="400" Width="500">
    <StackPanel Margin="20">
        <TextBlock Text="CustomControl 演示 - 可以换肤" FontSize="20" FontWeight="Bold" Margin="0,0,0,20"/>

        <TextBlock Text="默认金色星星:" Margin="0,0,0,5"/>
        <ctrls:StarRating Rating="{Binding Rating1, Mode=TwoWay}"
                          MaxRating="{Binding MaxRating}"
                          Margin="0,0,0,15"/>

        <TextBlock Text="蓝色主题:" Margin="0,0,0,5"/>
        <ctrls:StarRating Rating="{Binding Rating2, Mode=TwoWay}"
                          MaxRating="{Binding MaxRating}"
                          Style="{StaticResource BlueTheme}"
                          Margin="0,0,0,15"/>

        <TextBlock Text="红色大尺寸主题:" Margin="0,0,0,5"/>
        <ctrls:StarRating Rating="{Binding Rating3, Mode=TwoWay}"
                          MaxRating="{Binding MaxRating}"
                          Style="{StaticResource RedLargeTheme}"
                          Margin="0,0,0,15"/>

        <TextBlock Text="圆形主题:" Margin="0,0,0,5"/>
        <ctrls:StarRating Rating="{Binding Rating4, Mode=TwoWay}"
                          MaxRating="{Binding MaxRating}"
                          Style="{StaticResource CircleTheme}"
                          Margin="0,0,0,15"/>

        <TextBlock Text="{Binding ResultText}" FontSize="14" Foreground="Blue" Margin="0,10,0,0"/>
    </StackPanel>
</Window>


<!--App.xaml-->
<Application x:Class="CustomControlDemo.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/CustomControlDemo;Component/Themes/Generic.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>


//App.xaml.cs
using System;
using System.Windows;
namespace CustomControlDemo
{
    public partial class App : Application
    {
        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);
            var window = new MainWindow();//创建UI窗口
            window.DataContext = new MainViewModel();//注入ViewModel
            window.Show();//显示窗口
        }
    }
}


<!--Generic.xaml-->
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="pack://application:,,,/CustomControlDemo;Component/Controls/Styles/StarRating.xaml"/>
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>


//MainViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace CustomControlDemo
{
    public class MainViewModel : INotifyPropertyChanged
    {
        private int _rating1 = 3;
        private int _rating2 = 4;
        private int _rating3 = 2;
        private int _rating4 = 3;
        private int _maxRating = 6;

        public int Rating1
        {
            get => _rating1;
            set
            {
                if (_rating1 != value)
                {
                    _rating1 = value;
                    OnPropertyChanged();
                    OnPropertyChanged(nameof(ResultText));
                }
            }
        }

        public int Rating2
        {
            get => _rating2;
            set
            {
                if (_rating2 != value)
                {
                    _rating2 = value;
                    OnPropertyChanged();
                }
            }
        }

        public int Rating3
        {
            get => _rating3;
            set
            {
                if (_rating3 != value)
                {
                    _rating3 = value;
                    OnPropertyChanged();
                }
            }
        }

        public int Rating4
        {
            get => _rating4;
            set
            {
                if (_rating4 != value)
                {
                    _rating4 = value;
                    OnPropertyChanged();
                }
            }
        }

        public int MaxRating
        {
            get => _maxRating;
            set
            {
                if (_maxRating != value)
                {
                    _maxRating = value;
                    OnPropertyChanged();
                    OnPropertyChanged(nameof(ResultText));
                }
            }
        }

        public string ResultText => $"当前评分: {Rating1} / {MaxRating}";

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}


说明:

1.ControlTemplate用法

所有继承Control的控件有一个ControlTemplate类型的属性:Template, 可用于挂载ControlTemplate模板,定义控件整体的外观和布局。(整体外观由ControlTemplate控制)

2. Datatemplate用法

xaml中写的Datatemplate,被用于赋值给ContentControl控件的ContentTemplate或ItemsControl控件的ItemTemplate(二者都是Datatemplate类型的依赖属性), 当wpf要去渲染控件的Content或ItemsSource时,会按照ContentTemplate/ItemTemplate 中定义的样子对content/ItemsSource数据进行绘制(内部个体外观由DataTemplate控制)。这样拥有Content/ItemsSource属性的控件被称为数据呈现控件,可以设置DataTemplate让它显示复杂的业务对象。而像布局类控件(Grid, StackPanel,Border等)和纯视觉控件(TextBlock等)没有Content或ItemsSource这样的数据承载属性,因此无法设置Datatemplate。

3. ControlTemplate和Datatemplate 内部都只能放一个根控件,如果想要放多个,则需要先放布局控件作为根,再在布局控件里面放置多个。

4. 上述StarRating控件继承自Control,是利用ControlTemplate完全重绘的一个自包含型控件,它默认不支持DataTemplate,但它在ControlTemplate内部硬编码实例化了ItemsControl, 并在ItemsControl内部使用DataTemplate,这是合法的(通常Datatemplate 和Style 一样,并列放在Resource 里面,并在引用控件的时候,由用户按需注入,本例直接写死,不支持用户修改)。

5. 继承ContentControl或ItemsControl时,控件制作方只需负责边框、阴影、标题栏、布局等整体设计,控件内部的具体内容由用户确定,用户可以在该控件内放任意UI。如果不需要将内部UI开放给用户,则应该直接继承Control,由控件的提供者确定控件内外的全部形态。

6. 在继承ContentControl或ItemsControl实现自定义控件时,需要在Controltemplate模板中使用ContentPresenter或ItemsPresenter占位,引导Content+ContentTemplate或ItemsPanel在控件指定位置展开。ContentPresenter可显式绑定Content和ContentTemplate(默认自动从父控件找),而ItemsPresenter不能显示绑定ItemsPanel。用户使用时,可提供Content, ContentTemplate绑定,或者ItemsPanel设计。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值