当现有控件不满足业务需要时,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设计。

2999

被折叠的 条评论
为什么被折叠?



