65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (58投票s)

2006年6月4日

CPOL

3分钟阅读

viewsIcon

196795

downloadIcon

3730

在 WPF 中,自定义控件应该是无外观的。这意味着业务逻辑和 UI 应该强分离。当您创建一个控件时,您可以为该控件提供一个默认模板,但是任何使用该控件的人都可以覆盖它,而无需触及业务逻辑。

引言

在 WPF 中,自定义控件应该是无外观的。这意味着业务逻辑和 UI 应该强分离。当您创建一个控件时,您可以为该控件提供一个默认模板,但是任何使用该控件的人都可以覆盖它,而无需触及业务逻辑。

在我的示例中,我将创建一个简单的时钟的自定义控件,并为数字时钟提供一个默认模板。在使用该控件的应用程序中,我将覆盖该时钟的模板,以便它显示为模拟时钟。

此示例是使用 2 月份的 CTP 创建的。

Sample screenshot

Sample screenshot

默认模板

自定义模板

创建项目

  • 创建一个“WinFX Windows 应用程序”。此应用程序用于测试自定义控件。
  • 创建一个“WinFX 自定义控件库”。此库将包含自定义时钟控件。
  • 删除控件库中的文件“UserControl1.xaml”。
  • 向控件库添加一个新的自定义控件。
  • 在 Windows 应用程序项目中添加对控件库的引用。

创建自定义控件

  • 向自定义控件添加一个 DependencyPropertyDateTime”。此属性将始终包含实际的日期和时间。此控件的模板可以绑定到此属性。需要 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>

现在,自定义控件正在工作。

Sample screenshot

覆盖默认模板

通过覆盖默认模板,可以完全更改控件的外观。

  • 向“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}" />

现在,自定义控件已自定义

Sample screenshot

© . All rights reserved.