WPF -- 自定义控件 (UserControl)(二 上)

在这里我们将将打造一个UserControl(用户控件)来逐步讲解如何在WPF中自定义控件,并将WPF的一些新特性引入到自定义控件中来.
我们制作了一个带语音报时功能的钟表控件, 效果如下:


在VS中右键单击你的项目,点击"添加新项目",在出现的选择列表中选择"UserControl",VS会自动为你生成一个*.xaml文件以及其对应的后台代码文件(*.cs或其它).
值得注意的是,自动生成的代码中,你的控件是继承于System.Windows.Controls.UserControl类的,这对应你的控件而言并不一定是最恰当的基类,你可以修改它,但注意你应该同时修改*.cs文件和*.xaml文件中的基类,而不只是修改*.cs文件,否则当生成项目时会报错"不是继承于同一基类".修改*.xaml文件的方法是:将该文件的第一行和最后一行的"UserControl"改成与你认为恰当的基类名称.

1,为控件添加属性(依赖属性,DependencyProperty)
正如下面的代码所示:
public static readonly DependencyProperty TimeProperty = 
            DependencyProperty.Register(
"Time"typeof(DateTime), typeof(ClockUserCtrl), 
           
new FrameworkPropertyMetadata(DateTime.Now,new PropertyChangedCallback(TimePropertyChangedCallback)));
我们为控件(或者任何一个WPF类)添加的依赖属性都是"公开的","静态的","只读的",其命名方式是"属性名+Property",这是依赖属性一成不变的书写方式.对于依赖属性的注册可以在声明该属性时就调用DependencyProperty.Register()方法注册,也可以在其静态构造方法中注册.上面的DependencyProperty.Register方法的几个参数分别是:属性名(该属性名与声明的依赖属性名称"XXXProperty"相比仅仅是少了"Property"后缀,其它完全一样,否则在运行时会报异常),属性的数据类型,属性的拥有者的类型,元数据.
关于参数中传递的元数据:如果是普通的类则应该传递PropertyMetadata,如果是FrameworkElement则可以传递FrameworkPropertyMetadata,其中FrameworkPropertyMetadata中可以制定一些标记表明该属性发生变化时控件应该做出什么反应,比如某属性的变化会影响到该控件的绘制,那么就应该像这样书写该属性的元数据: new FrameworkPropertyMetadata(defauleValue, FrameworkPropertyMetadataOptions.AffectsRender);这样当该属性发生变化时系统会考虑重绘该控件.另外元数据中还保护很多内容,比如默认值,数据验证,数据变化时的回调函数,是否参与属性"继承"等.
然后,我们将该依赖属性包装成普通属性:
        [Description("获取或设置当前日期和时间")]
        [Category(
"Common Properties")]
       
public DateTime Time
       
{
           
get
           
{
               
return (DateTime)this.GetValue(TimeProperty);
            }

           
set
           
{
               
this.SetValue(TimeProperty, value);
            }

        }
GetValue和SetValue方法来自于DependencyObject类,其用于获取或设置类的某属性值.
注意:在将依赖属性包装成普通属性时,在get和set块中除了按部就班的调用GetValue和SetValue方法外,不要进行任何其它的操作.下面的代码是不恰当的:
        [Description("获取或设置当前日期和时间")]
        [Category(
"Common Properties")]
       
public DateTime Time
       
{
           
get
           
{
               
return (DateTime)this.GetValue(TimeProperty);
            }

           
set
           
{
               
this.SetValue(TimeProperty, value);
               
this.OnTimeUpdated(value);//Error
            }

        }
在以前这或许是很多人的惯用写法,但在WPF中,这样的写法存在潜在的错误,原因如下:我们知道继承于DependencyObject的类拥有GetValue和SetValue方法来获取或设置属性值,那为什么我们不直接使用该方法来获取或设置属性值,而要将其包装成普通的.NET属性呢,事实上在这里两种方式都是可以的,只不过包装成普通的.NET属性更符合.NET开发人员的习惯,使用GetValue和SetValue更像JAVA开发人员的习惯,但XAML在执行时似乎于JAVA开发人员一样,其不会调用.NET属性而是直接使用GetValue或SetValue方法,这样一来,我们写在get块和set块中的其它代码根本不会被XAML执行到.所以说,就上面的Time属性而言,C#(或其它)对该属性的调用不会出现任何问题,但该属性被用在XAML中时(比如在XAML对该属性进行数据绑定等),其set块中的this.OnTimeUpdated(value);语句不会被执行到.
那么,当Time属性发生变化时的确需要调用this.OnTimeUpdated(value);语句(因为该语句会引发时间被更新了的事件),还是在传递的依赖属性元数据做文章:
new FrameworkPropertyMetadata(DateTime.Now,new PropertyChangedCallback(TimePropertyChangedCallback)),我们为属性的变化指定了一个回调函数,当该属性变化时该回调函数就会被执行:
        private static void TimePropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
       
{
           
if (sender != null && sender is ClockUserCtrl)
           
{
                ClockUserCtrl clock 
= sender as ClockUserCtrl;
                clock.OnTimeUpdated((DateTime)arg.OldValue, (DateTime)arg.NewValue);                
            }

        }



2,为控件添加事件(传阅事件,RoutedEvent)
添加传阅事件的方法与添加依赖属性的方法很类似:

        public static readonly RoutedEvent TimeUpdatedEvent = 
            EventManager.RegisterRoutedEvent(
"TimeUpdated",
             RoutingStrategy.Bubble, 
typeof(RoutedPropertyChangedEventHandler<DateTime>), typeof(ClockUserCtrl));

其支持方法EventManager.RegisterRoutedEvent()对应的几个参数分别为:事件名称,事件传阅的方式(向上传阅,向下传阅或不传阅),事件对应的EventHandler的类型,事件拥有者的类型)
然后将事件包装成普通的.NET事件:
        [Description("日期或时间被更新后发生")]
       
public event RoutedPropertyChangedEventHandler<DateTime> TimeUpdated
       
{
            add
           
{
               
this.AddHandler(TimeUpdatedEvent, value);
            }

            remove
           
{
               
this.RemoveHandler(TimeUpdatedEvent, value);
            }

        }
注意,与依赖属性一样,不要在add与remove块中添加除AddHandler与RemoveHandler以外的代码.
题外话,事件参数中的e.Handled=true并不是终止事件的传阅,这只是为事件做一个标记而已,以便在默认情况下的让那些事件处理函数在该标记为true的情况下不被调用,要为该标记为true的事件注册处理方法并让该方法得到执行,请使用AddHandler方法,并把最后一个参数handlerEventsToo设置为true,如下:
this.myInkCanvas.AddHandler(
      InkCanvas.MouseLeftButtonDownEvent,
     
new MouseButtonEventHandler(
          myInkCanvas_MouseLeftButtonDown),
     
true);
private void myInkCanvas_MouseLeftButtonDown(
      
object sender, MouseButtonEventArgs e)
{
      
//do something
}


然后编写惯用的OnXXX方法:
        protected virtual void OnTimeUpdated(DateTime oldValue, DateTime newValue)
       
{
            RoutedPropertyChangedEventArgs
<DateTime> arg = 
               
new RoutedPropertyChangedEventArgs<DateTime>(oldValue, newValue,TimeUpdatedEvent);
           
this.RaiseEvent(arg);            
        }


3,为控件添加命令(Commands)
能为自定义控件添加如WPF内置控件一样的命令是一件很不错的事情(事实上这也是在CustomControl中降低界面和后台逻辑耦合度的一种方法,本系列随笔中的下一篇中将会具体谈谈).
WPF中内置的命令有两大类型:RoutedCommand以及RoutedUICommand,后者比前者多了一个Text属性用于在界面上自动本地化地显示该命令对应的文本,更多的可以参考WPF中的命令与命令绑定(一)以及WPF中的命令与命令绑定(二).
这里我们来定义一个命令,其功能是控件的语音报时.首先我们定义一个命令:

        public static readonly RoutedUICommand SpeakCommand = new RoutedUICommand("Speak""Speak"typeof(ClockUserCtrl));
参数分别为命名的显示名称,命令的名称,命令的拥有者类型.
然后在控件的静态函数中定义一个命令绑定,该命令绑定定义了命令的具体细节:对应的命令是什么?其完成什么样的功能,当前环境下其能执行吗?
            CommandBinding commandBinding =
               
new CommandBinding(SpeakCommand, new ExecutedRoutedEventHandler(ExecuteSpeak),
               
new CanExecuteRoutedEventHandler(CanExecuteSpeak));
        private static void ExecuteSpeak(object sender, ExecutedRoutedEventArgs arg)
       
{
            ClockUserCtrl clock 
= sender as ClockUserCtrl;
           
if (clock != null)
           
{
                clock.SpeakTheTime();
            }

        }

       
private static void CanExecuteSpeak(object sender, CanExecuteRoutedEventArgs arg)
       
{
            ClockUserCtrl clock 
= sender as ClockUserCtrl;
            arg.CanExecute 
= (clock != null);
        }
CanExecuteRoutedEventArgsCanExecute属性用于指示当前命令是否可用,也就是说系统会不断地检视该命令与该命令的作用对象,并根据你所提供的条件来判断当前命令是否可用,比如文本框状态变为"只读"后,其"粘贴"命令将不可用,作用于该文本框的粘贴按钮会自动被禁用,反之则启用.
new ExecutedRoutedEventHandler(ExecuteSpeak)
委托指定了当该命令被执行时所要完成的任务,这通过回调ExcuteSpeak函数来实现.
        private static void ExecuteSpeak(object sender, ExecutedRoutedEventArgs arg)
       
{
            ClockUserCtrl clock 
= sender as ClockUserCtrl;
           
if (clock != null)
           
{
                clock.SpeakTheTime();
            }

        }
        private void SpeakTheTime()
       
{
            DateTime localTime 
= this.Time.ToLocalTime();
           
string textToSpeak = "现在时刻," + 
                localTime.ToShortDateString() 
+","+
                localTime.ToShortTimeString()  
+ 
               
",星期" + (int)localTime.DayOfWeek;
           
this.speecher.SpeakAsync(textToSpeak);
        }
我们也可以为命令添加快捷键,这是通过InputBinding来实现的,其将命令与命令的快捷键关联起来,比如:
            InputBinding inputBinding = new InputBinding(SpeakCommand, new MouseGesture(MouseAction.LeftClick));
            CommandManager.RegisterClassInputBinding(
typeof(ClockUserCtrl), inputBinding);
这样,当我们鼠标点击控件时就会引发控件的Speak命令,从而调用SpeakTheTime函数进行语音播报.
快捷键可以通过MouseGesture或KeyGesture来定义.

4,优点与缺点:
UserControl能比较快速的打造自定义控件,但其对模板样式等缺乏很好的支持,打造出来的控件不如WPF内置控件一样灵活。

代码:
前端
<UserControl x:Class="YangUI.View.ClockUserCtrl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:YangUI.View"
             mc:Ignorable="d"  
             Width="200" Height="100">
    <Viewbox>
        <Grid Width="Auto" Height="Auto">
            <Grid.RowDefinitions>
                <RowDefinition Height="0.74*"/>
                <RowDefinition Height="0.26*"/>
            </Grid.RowDefinitions>
            <TextBlock Width="Auto" Height="Auto" Text="00:00" TextWrapping="Wrap" FontSize="48" Foreground="#FFFFD700" x:Name="textBlock_Time" Grid.RowSpan="1" FontFamily="Broadway">
                <TextBlock.BitmapEffect>
                    <OuterGlowBitmapEffect Opacity="0.6"/>
                </TextBlock.BitmapEffect>
            </TextBlock>
            <TextBlock HorizontalAlignment="Center" Margin="0,0,0,0" VerticalAlignment="Center" Grid.Row="1" Foreground="#FF26F1FB" Text="Mondy" TextWrapping="Wrap" FontFamily="Bradley Hand ITC" x:Name="textBlock_Date">
                <TextBlock.BitmapEffect>
                    <OuterGlowBitmapEffect GlowColor="#FF26F1FB" Opacity="0.7"/>
                </TextBlock.BitmapEffect>
            </TextBlock>
        </Grid>
    </Viewbox>

</UserControl>

后端:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Speech.Synthesis;
using System.Windows.Shapes;
using System.Windows.Threading;

namespace YangUI.View
{
    /// <summary>
    /// ClockUserCtrl.xaml 的交互逻辑
    /// </summary>
    public partial class ClockUserCtrl : UserControl
    {
        public ClockUserCtrl()
        {
            InitializeComponent();
        }

        static ClockUserCtrl()
        {
            SetCommands();
        }

        private static void SetCommands()
        {
            CommandBinding commandBinding =
                           new CommandBinding(SpeakCommand, new ExecutedRoutedEventHandler(ExecuteSpeak),
                           new CanExecuteRoutedEventHandler(CanExecuteSpeak));

            CommandManager.RegisterClassCommandBinding(typeof(ClockUserCtrl), commandBinding);

            InputBinding inputBinding = new InputBinding(SpeakCommand, new MouseGesture(MouseAction.LeftClick));
            CommandManager.RegisterClassInputBinding(typeof(ClockUserCtrl), inputBinding);
        }

        private DispatcherTimer innerTimer;

        private SpeechSynthesizer speecher = new SpeechSynthesizer();

        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);

            this.SetTimer();
            this.SetBinding();
            this.SpeakTheTime();
        }

        private static void ExecuteSpeak(object sender, ExecutedRoutedEventArgs arg)
        {
            ClockUserCtrl clock = sender as ClockUserCtrl;
            if (clock != null)
            {
                clock.SpeakTheTime();
            }
        }

        private static void CanExecuteSpeak(object sender, CanExecuteRoutedEventArgs arg)
        {
            ClockUserCtrl clock = sender as ClockUserCtrl;
            arg.CanExecute = (clock != null);
        }

        private void SpeakTheTime()
        {
            DateTime localTime = this.Time.ToLocalTime();
            string textToSpeak = "现在时刻," +
                localTime.ToShortDateString() + "," +
                localTime.ToShortTimeString() +
                ",星期" + (int)localTime.DayOfWeek;

            this.speecher.SpeakAsync(textToSpeak);
        }

        private void SetTimer()
        {
            this.innerTimer = new DispatcherTimer(TimeSpan.FromSeconds(1.0),
                DispatcherPriority.Loaded, new EventHandler(this.InnerTimerCallback), this.Dispatcher);
            this.innerTimer.Start();
        }

        private void InnerTimerCallback(object obj, EventArgs arg)
        {
            this.Time = DateTime.Now;
        }

        private void SetBinding()
        {
            Binding timeBinding = new Binding();
            timeBinding.Source = this;
            timeBinding.Path = new PropertyPath(ClockUserCtrl.TimeProperty);
            timeBinding.Converter = new TimeConverter();
            this.textBlock_Time.SetBinding(TextBlock.TextProperty, timeBinding);

            Binding dateBinding = new Binding();
            dateBinding.Source = this;
            dateBinding.Path = new PropertyPath(ClockUserCtrl.TimeProperty);
            dateBinding.Converter = new DateConverter();
            this.textBlock_Date.SetBinding(TextBlock.TextProperty, dateBinding);

        }

        #region DP

        public static readonly DependencyProperty TimeProperty =
            DependencyProperty.Register("Time", typeof(DateTime), typeof(ClockUserCtrl),
            new FrameworkPropertyMetadata(DateTime.Now, new PropertyChangedCallback(TimePropertyChangedCallback)));

        [Description("获取或设置当前日期和时间")]
        [Category("Common Properties")]
        public DateTime Time
        {
            get
            {
                return (DateTime)this.GetValue(TimeProperty);
            }
            set
            {
                this.SetValue(TimeProperty, value);
            }
        }

        private static void TimePropertyChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs arg)
        {
            if (sender != null && sender is ClockUserCtrl)
            {
                ClockUserCtrl clock = sender as ClockUserCtrl;
                clock.OnTimeUpdated((DateTime)arg.OldValue, (DateTime)arg.NewValue);
            }
        }

        #endregion

        #region Event

        public static readonly RoutedEvent TimeUpdatedEvent =
            EventManager.RegisterRoutedEvent("TimeUpdated",
             RoutingStrategy.Bubble, typeof(RoutedPropertyChangedEventHandler<DateTime>), typeof(ClockUserCtrl));

        [Description("日期或时间被更新后发生")]
        public event RoutedPropertyChangedEventHandler<DateTime> TimeUpdated
        {
            add
            {
                this.AddHandler(TimeUpdatedEvent, value);
            }
            remove
            {
                this.RemoveHandler(TimeUpdatedEvent, value);
            }
        }

        protected virtual void OnTimeUpdated(DateTime oldValue, DateTime newValue)
        {
            RoutedPropertyChangedEventArgs<DateTime> arg =
                new RoutedPropertyChangedEventArgs<DateTime>(oldValue, newValue, TimeUpdatedEvent);
            this.RaiseEvent(arg);
        }

        #endregion

        #region Commands

        public static readonly RoutedUICommand SpeakCommand = new RoutedUICommand("Speak", "Speak", typeof(ClockUserCtrl));

        #endregion
    }

    #region Converter

    [ValueConversion(typeof(DateTime), typeof(string))]
    public class TimeConverter : IValueConverter
    {
        #region IValueConverter 成员

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            DateTime date = (DateTime)value;

            return date.ToLocalTime().ToLongTimeString();
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return null;
        }

        #endregion
    }

    [ValueConversion(typeof(DateTime), typeof(string))]
    public class DateConverter : IValueConverter
    {
        #region IValueConverter 成员

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            DateTime date = ((DateTime)value).ToLocalTime();

            return date.ToShortDateString() + " " + date.DayOfWeek;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return null;
        }

        #endregion
    }

    #endregion

}

阅读更多

没有更多推荐了,返回首页