1 概述
WPF提供了创建控件的3种通用模式,提供了 不同的特性和灵活性。在开发一个新控件之前,理解这些模式是非常重要的。这将有助于你使用适当的模式来开发控件。
1.1 何时需要开发一个控件
当你需要一个表现丰富的的控件,而已有控件又不能满足需要,这个时候就需要开发一个定制控件了。
1.2 控件特性
WPF的控件支持 Rich Content,Styles, Triggers 以及templates,在大多数情况下你将不需要编写一个新的控件,而仅仅通过这些特性来定制表现丰富的控件
Rich Content:它支持任意的内容显示
Styles: values的集合,通过它你可以改变已有控件界面UI,实现定制界面显示
Trigger.: 通过它你可以改变已有控件的显示和行为。
Templates: 通过它可以不需要编写代码就实现扩充和定制界面显示。
2 控件开发
2.1 创建一个UserControl控件
这是WPF中创建控件最简单的方法,通过继承UserControl类实现
2.1.1 UserControl特性
持有特性:Rich Content,styles,Triggers。
非持有特性:Templates
2.1.2 创建UserControl的好处
● 可以像创建Application一样来创建控件
● 控件仅仅由已有组件(控件)组成
● 不需要支持复杂的定制
2.2 创建一个继承自Control的控件
继承自Control类的控件开发模式是大部分WPF中的控件采用的开发模式。自定义控件通过模板实现运行逻辑和界面显示的分离。比起UserControl它提供了更多的灵活性
2.2.1 Control特性
持有特性:Rich Content, Styles, Triggers, templates
非持有特性:无
2.2.2 创建Control的好处
● 通过controltemplate定制控件的显示
● 支持theme
2.3 创建一个继承自FrameworkElement的控件
无论是继承自UserControl还是Control的控件都将依赖已有的元素(Elements),如果构成它们的简单元素不能满足当前需要,那么就需要通过重载OnRender并由DrawingContext操作来重绘界面
2.3.1 创建继承自FrameworkElement的好处
● 实现当前控件元素不具有的特定显示
● 通过定义自己的Render逻辑来显示界面
● 以新奇的方式组合现有控件元素
3 创建自定义控件
由于UserControl相对简单,这里仅介绍如何创建自定义控件,本节假定.NET3.0环境已经搭建完毕,采用Feb 2006 CTP版。
以创建一个时钟控件为例
3.1 打开VS2005,在左边的项目类型中选择Visual C# -- Net Framework3.0,在右边Vs已经安装的模板中选择CustomControlLibrary(WPF),并在下面项目名称处填入项目名称Clock,选择项目要生成到的目录,点击确定。过程如下图所示
3.2工程创建完毕之后,在解决方案中将VS自动生成的UserControl1.xaml删除。然后给工程MyClock加入新建项Custom Control(WPF),如下图所示
3.3 此时,工程已经创建完毕,开始编写代码。基本框架如下
public class MyClock : System.Windows.Controls.Control
{
static MyClock()
{
//This OverrideMetadata call tells the system that this element wants to provide a style that is different than its base class.
//This style is defined in themes/generic.xaml
DefaultStyleKeyProperty.OverrideMetadata(typeof(MyClock), new FrameworkPropertyMetadata(typeof(MyClock)));
}
}
3.3.1 定义一个计时器
private DispatcherTimer timer; // NameSpace:System.Windows.Threading
//重载初始化函数OnInitialized,加入初始化计时器代码
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
UpdateDateTime();
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(1000);
timer.Tick += new EventHandler(timer_Tick);
timer.Start();
}
//计时器事件函数
private void timer_Tick(Object sender, EventArgs e)
{
UpdateDateTime();
}
//编写UpdateDateTime函数,用于更新时间
private void UpdateDateTime()
{
Time = DateTime.Now;
}
3.3.2 添加依赖属性Time
public DateTime Time
{
get{ return (DateTime)GetValue(TimeProperty);}
private set SetValue(TimeProperty, value);}
}
public static DependencyProperty TimeProperty = DependencyProperty.Register(
"Time",peof(DateTime),
typeof(MyClock),
new PropertyMetadata(DateTime.Now,new PropertyChangedCallback(OnDateTimeInvalidated)));
//添加依赖属性值改变后触发的事件代码
public static readonly RoutedEvent DateTimeChangedEvent =
EventManager.RegisterRoutedEvent("DateTimeChanged", RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<DateTime>),
typeof(MyClock));
private static void OnDateTimeInvalidated(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DateTime oldValue = (DateTime)e.OldValue;
DateTime newValue = (DateTime)e.NewValue;
clock.OnDateTimeChanged(oldValue, newValue);
}
protected virtual void OnDateTimeChanged(DateTime oldValue, DateTime newValue)
{
RoutedPropertyChangedEventArgs<DateTime> args = new RoutedPropertyChangedEventArgs<DateTime>(oldValue, newValue);
args.RoutedEvent = MyClock.DateTimeChangedEvent;
RaiseEvent(args);
}
至此代码编写基本完毕
3.4 打开解决方案theme文件夹中的generic.xaml,内容如下
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyClock"
>
<Style TargetType="{x:Type local:MyClock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyClock}">
<!--在这里加入特定显示代码-->
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
打开Express Blend,新建一个windosw app工程,将TextBox拖放到窗体中,位置大小设置好之后,打开对应的xaml文件,内容如下:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="test.Window1"
x:Name="Window"
Title="Window1"
Width="640" Height="480">
<Grid x:Name="LayoutRoot">
<TextBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Text="TextBox" TextWrapping="Wrap"/>
</Window>
将<Grid></Grid>以及它们包含的内容copy到generic.xmal的注释处,并加入绑定信息,最终generic.xaml文件如下所示:
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyClock"
>
<Style TargetType="{x:Type local:MyClock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyClock}">
<!--在这里加入特定显示代码-->
<Grid x:Name="LayoutRoot">
<TextBox HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Text="{Binding Path=Time, RelativeSource={RelativeSource TemplatedParent}}"
TextWrapping="Wrap"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
至此一个简单的时钟控件已经创建完毕,打开Blend,创建一工程,将MyClock拖放到窗体,结果如下:
4 为自定义控件创建控件模板
以MyClock为例,如果希望时间以模拟时钟的方式显示,就需要重写ControlTemplate。
4.1打开Blend,绘制希望显示的界面。如下图
4.2 在theme文件 夹中新建一字典资源文件Classic.xaml,将<Grid></Grid>(Window下一级)以及它所包含的内容拷贝到<ControlTemplate></ControlTemplate>中。
4.3 因为模拟时钟需要角度数据,所在在这里需要定义4个转换类,分别用于转换时分秒和星期,如下所示:
#region Converter
[ValueConversion(typeof(DateTime), typeof(int))]
public class SecondsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime date = (DateTime)value;
return date.Second * 6;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
[ValueConversion(typeof(DateTime), typeof(int))]
public class MinutesConverter : IValueConverter
{
public object Convert(Object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime date = (DateTime)value;
return date.Minute * 6;
}
public object ConvertBack(Object value, Type targetType, Object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
[ValueConversion(typeof(DateTime), typeof(int))]
public class HoursConverter : IValueConverter
{
public object Convert(Object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime date = (DateTime)value;
return (date.Hour * 30) + (date.Minute / 2);
}
public object ConvertBack(Object value, Type targetType, Object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
[ValueConversion(typeof(DateTime), typeof(string))]
public class WeekDayConverter : IValueConverter
{
public object Convert(Object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
DateTime date = (DateTime)value;
return date.DayOfWeek.ToString();
}
public object ConvertBack(Object value, Type targetType, Object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
#endregion
4.4 修改classic.xmal文件,结果如下
通过添加数据绑定到表指针的角度,实现模拟时钟
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MyClock"
>
<local:SecondsConverter x:Key="SecondsConverter"/>
<local:MinutesConverter x:Key="MinutesConverter"/>
<local:HoursConverter x:Key="HoursConverter"/>
<local:WeekDayConverter x:Key="WeekDayConverter"/>
<Style TargetType="{x:Type local:MyClock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:MyClock}">
<!--在这里加入特定显示代码-->
<Grid x:Name="LayoutRoot">
<Ellipse Fill="#FF3121B3" Stroke="#FF000000"/>
<Ellipse RenderTransformOrigin="0.507,0.5" Fill="#FF 6A 76C 6" Stroke="#FF000000" Margin="24,24,24,22"/>
<Path Fill="#FF3121B3" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Right" Margin="0,24,311,0" VerticalAlignment="Top" Width="1" Height="57" Data="M312, 24 L 312,80"/>
<Path Fill="#FF3121B3" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Right" Margin="0,0,311,22" VerticalAlignment="Bottom" Width="1" Height="57" Data="M 320,360 L 320,416"/>
<Path Fill="#FF3121B3" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Right" Margin="0,0,24,221.5" VerticalAlignment="Bottom" Width="48.5" Height="1" Data="M 600,224 L 560,224"/>
<Path Fill="#FF3121B3" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Left" Margin="24,0,0,221.5" VerticalAlignment="Bottom" Width="48.5" Height="1" Data="M 32,224 L 64,224"/>
<Label HorizontalAlignment="Left" Margin="288,0,280,118" VerticalAlignment="Bottom" Height="24" Width="50"
Content="{Binding Path=Time, Converter={StaticResource WeekDayConverter}, RelativeSource={RelativeSource TemplatedParent}}"/>
<Path RenderTransformOrigin="-0.062,1.004" Fill="#FF 6A 76C 6" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Right" Margin="0,120,310.635,222.793" Width="0.361" Data="M 344,192 L 472,112">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="{Binding Path=Time, Converter={StaticResource MinutesConverter}, RelativeSource={RelativeSource TemplatedParent}}"/>
<TranslateTransform X="0.914" Y="0.296"/>
</TransformGroup>
</Path.RenderTransform>
</Path>
<Path RenderTransformOrigin="0,1.008" Fill="#FF 6A 76C 6" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Right" Margin="0,160.5,310,222" Width="2" Data="M 320,192 L 272,136">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="-1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="{Binding Path=Time, Converter={StaticResource HoursConverter}, RelativeSource={RelativeSource TemplatedParent}}"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Path.RenderTransform>
</Path>
<Path RenderTransformOrigin="0.5,1" Fill="#FF 6A 76C 6" Stretch="Fill" Stroke="#FF000000" HorizontalAlignment="Right" Margin="0,94.5,311,222.5" Width="1" Data="M 344,224 L 344,96">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="{Binding Path=Time, Converter={StaticResource SecondsConverter}, RelativeSource={RelativeSource TemplatedParent}}"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Path.RenderTransform>
</Path>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>