创建无外观的 WPF 自定义控件






4.86/5 (58投票s)
在 WPF 中,自定义控件应该是无外观的。这意味着业务逻辑和 UI 应该强分离。当您创建一个控件时,您可以为该控件提供一个默认模板,但是任何使用该控件的人都可以覆盖它,而无需触及业务逻辑。
引言
在 WPF 中,自定义控件应该是无外观的。这意味着业务逻辑和 UI 应该强分离。当您创建一个控件时,您可以为该控件提供一个默认模板,但是任何使用该控件的人都可以覆盖它,而无需触及业务逻辑。
在我的示例中,我将创建一个简单的时钟的自定义控件,并为数字时钟提供一个默认模板。在使用该控件的应用程序中,我将覆盖该时钟的模板,以便它显示为模拟时钟。
此示例是使用 2 月份的 CTP 创建的。
|
|
默认模板 |
自定义模板 |
创建项目
- 创建一个“WinFX Windows 应用程序”。此应用程序用于测试自定义控件。
- 创建一个“WinFX 自定义控件库”。此库将包含自定义时钟控件。
- 删除控件库中的文件“UserControl1.xaml”。
- 向控件库添加一个新的自定义控件。
- 在 Windows 应用程序项目中添加对控件库的引用。
创建自定义控件
- 向自定义控件添加一个
DependencyProperty
“DateTime
”。此属性将始终包含实际的日期和时间。此控件的模板可以绑定到此属性。需要PropertyChangedCallback
来通知 UI 元素该属性已更改。如果没有此回调,绑定控件将不会更新。
...
public DateTime DateTime
{
get
{
return (DateTime)GetValue(DateTimeProperty);
}
private set
{
SetValue(DateTimeProperty, value);
}
}
public static DependencyProperty DateTimeProperty =
DependencyProperty.Register(
"DateTime",
typeof(DateTime),
typeof(Clock),
new PropertyMetadata(
DateTime.Now,
new PropertyChangedCallback(OnDateTimeInvalidated)));
public static readonly RoutedEvent DateTimeChangedEvent =
EventManager.RegisterRoutedEvent(
"DateTimeChanged",
RoutingStrategy.Bubble,
typeof(RoutedPropertyChangedEventHandler<DateTime>),
typeof(Clock));
protected virtual void OnDateTimeChanged(DateTime oldValue,
DateTime newValue)
{
RoutedPropertyChangedEventArgs<DateTime> args = new
RoutedPropertyChangedEventArgs<DateTime>(oldValue, newValue);
args.RoutedEvent = Clock.DateTimeChangedEvent;
RaiseEvent(args);
}
private static void OnDateTimeInvalidated(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
Clock clock = (Clock)d;
DateTime oldValue = (DateTime)e.OldValue;
DateTime newValue = (DateTime)e.NewValue;
clock.OnDateTimeChanged(oldValue, newValue);
}
...
- 向控件添加一个
Timer
以保持“DateTime
”属性的更新。如果您希望能够在
TimerElapsedEvent
中更新 UI 元素,则应使用System.Windows.Threading
命名空间中的DispatcherTimer
类。另一个选择是从Control.Dispatcher
对象调用委托 (this.Dispatcher.Invoke(...);
)。
...
using System.Windows.Threading;
namespace CustomControlLibrary
{
public class Clock : Control
{
private DispatcherTimer timer;
static Clock()
{
DefaultStyleKeyProperty.OverrideMetadata(
typeof(Clock),
new FrameworkPropertyMetadata(typeof(Clock)));
}
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
UpdateDateTime();
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromMilliseconds(1000 -
DateTime.Now.Millisecond);
timer.Tick += new EventHandler(Timer_Tick);
timer.Start();
}
private void Timer_Tick(object sender, EventArgs e)
{
UpdateDateTime();
timer.Interval = TimeSpan.FromMilliseconds(1000 -
DateTime.Now.Millisecond);
timer.Start();
}
private void UpdateDateTime()
{
this.DateTime = System.DateTime.Now;
}
...
为自定义控件创建默认模板
- 打开“themes”文件夹中的文件“generic.xaml”。此文件可以包含库中每个自定义控件的模板。
- 插入时钟的模板。在我的示例中,当前的
DateTime
显示在TextBlock
控件中。可选地,您可以向绑定添加一个转换器来格式化DateTime
属性。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:CustomControlLibrary"
>
<Style TargetType="{x:Type local:Clock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:Clock}">
<TextBlock Name="tblClock" Text="{Binding Path=DateTime,
RelativeSource={RelativeSource TemplatedParent}}" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
在应用程序中使用自定义控件
<Window x:Class="WindowsApplication.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:customControl="clr-namespace:CustomControlLibrary;
assembly=CustomControlLibrary"
Title="WindowsApplication" Height="487" Width="412"
>
<StackPanel HorizontalAlignment="Center">
<customControl:Clock Name="customControlClock" />
</StackPanel>
</Window>
现在,自定义控件正在工作。
覆盖默认模板
通过覆盖默认模板,可以完全更改控件的外观。
- 向“Window1.xaml”中
StackPanel
的资源添加一个模拟时钟的模板。此外,您需要一些转换器来将实际日期的秒、分和小时转换为表针的角度。
<StackPanel.Resources>
<local:SecondsConverter x:Key="secondsConverter"/>
<local:MinutesConverter x:Key="minutesConverter"/>
<local:HoursConverter x:Key="hoursConverter"/>
<local:WeekdayConverter x:Key="weekdayConverter"/>
<Style x:Key="AnalogClock" TargetType="{x:Type customControl:Clock}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type customControl:Clock}">
...
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>
- 在模板中绘制时钟。该模板包含一些椭圆和矩形。
- 将表针绑定到自定义控件的
DateTime
DependencyProperty
。由于RotateTransform
的角度不能绑定到DateTime
数据类型,因此您必须使用转换器对象。
<Rectangle x:Name="SecondHand" Canvas.Top="4"
Canvas.Left="49" Fill="Red" Width="1" Height="47">
<Rectangle.RenderTransform>
<RotateTransform Angle="{Binding Path=DateTime,
RelativeSource={RelativeSource TemplatedParent},
Converter={StaticResource secondsConverter}}"
CenterX="0.5" CenterY="47"></RotateTransform>
</Rectangle.RenderTransform>
</Rectangle>
- 在“Window1.xaml”的代码隐藏文件中添加表针的转换器。例如,
SecondsConverter
使用DateTime
属性的秒日期部分,并将其转换为时钟秒针的角度。
[ValueConversion(typeof(DateTime), typeof(int))]
public class SecondsConverter : IValueConverter
{
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
DateTime date = (DateTime)value;
return date.Second * 6;
}
public object ConvertBack(object value, Type targetType,
object parameter, CultureInfo culture)
{
return null;
}
}
- 最后,您必须将新模板应用于您的时钟控件。
<customControl:Clock Name="customControlAnalogClock"
Style="{StaticResource AnalogClock}" />
现在,自定义控件已自定义