原文:在WPF中自定义控件(2) UserControl

在WPF中自定义控件(2) 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);

        }

CanExecuteRoutedEventArgs的CanExecute属性用于指示当前命令是否可用,也就是说系统会不断地检视该命令与该命令的作用对象,并根据你所提供的条件来判断当前命令是否可用,比如文本框状态变为"只读"后,其"粘贴"命令将不可用,作用于该文本框的粘贴按钮会自动被禁用,反之则启用.

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,优点与缺点:

正如在在WPF中自定义控件(1) 中谈到的一样,UserControl能比较快速的打造自定义控件,但其对模板样式等缺乏很好的支持,打造出来的控件不如WPF内置控件一样灵活,在本系列随笔的下一篇中,我们将介绍如何打造能对WPF新特性提供完全支持的CustomControl.

DEMO

在WPF中自定义控件(2) UserControl的更多相关文章

  1. 在WPF中自定义控件

    一, 不一定需要自定义控件在使用WPF以前,动辄使用自定义控件几乎成了惯性思维,比如需要一个带图片的按钮,但在WPF中此类任务却不需要如此大费周章,因为控件可以嵌套使用以及可以为控件外观打造一套新的样 ...

  2. 在WPF中自定义控件(3) CustomControl (上)

    原文:在WPF中自定义控件(3) CustomControl (上) 在WPF中自定义控件(3) CustomControl (上)                              周银辉 ...

  3. [转]在WPF中自定义控件 UserControl

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

  4. 在WPF中自定义控件(3) CustomControl (下)

    原文:在WPF中自定义控件(3) CustomControl (下)   在WPF中自定义控件(3) CustomControl (下)                                 ...

  5. 在WPF中自定义控件(1)

    原文:在WPF中自定义控件(1)    在WPF中自定义控件(1):概述                                                   周银辉一, 不一定需要自定 ...

  6. wpf 中自定义控件及其使用

    主要有3个步骤: 1. 首先创建一个自定义的控件,该控件继承 TextBox namespace EzIntePark.Presentation.Common { /// <summary> ...

  7. 在WPF中UserControl

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

  8. 在WPF中减少逻辑与UI元素的耦合

    原文:在WPF中减少逻辑与UI元素的耦合             在WPF中减少逻辑与UI元素的耦合 周银辉 1,    避免在逻辑中引用界面元素,别把后台数据强加给UI  一个糟糕的案例 比如说主界 ...

  9. WPF中UserControl和DataTemplate

    最新更新: http://denghejun.github.io 前言 前言总是留给我说一些无关主题的言论,WPF作为全新Microsoft桌面或web应用程序显示技术框架, 从08年开始,一直到现在 ...

随机推荐

  1. May 12th 2017 Week 19th Friday

    Love asks faith, and faith asks firmness. 爱情要求忠诚,而忠诚要求坚贞. Love, as well as many other relations amon ...

  2. ABAP正则表达式 vs SPLIT INTO

    需求: 把如下通过"/"连接起来的三个字符串分别解析出来. 传统的做法见下图第98行的function module SKWF_UTIL_IO_FIND_BY_KEY: 这个fun ...

  3. 面条代码 vs. 馄沌代码

    转载自:https://blog.csdn.net/godsme_yuan/article/details/6594013

  4. 安卓extends和implements

    extends是继承类 implements是实现接口

  5. mssql数据库迁移到mysql

    使用mysql migration toolkit工具来进行迁移.(需要安装jdk6 java的安装包) 发现数据量大的表却没能迁过来.软件使用比较容易,配置下源数据库信息,和目标数据库信息就可以进行 ...

  6. Unity 游戏框架搭建 (九) 减少加班利器-QConsole

    为毛要实现这个工具? 在我小时候,每当游戏在真机运行时,我们看到的日志是这样的. 没高亮啊,还有乱七八糟的堆栈信息,好干扰日志查看,好影响心情. 还有就是必须始终连着usb线啊,我想要想躺着测试... ...

  7. onblur事件和click事件冲突

    在js中onblur事件的优先级click事件,所以同一个元素上绑定两个事件的时候,onblur事件会冲掉click事件. 解决方案:将click事件改成mousedown事件

  8. ABAP术语-Connection Type

    Connection Type 原文:http://www.cnblogs.com/qiangsheng/archive/2008/01/17/1042479.html A connection ty ...

  9. Graylog2日志服务安装配置

    软件版本: mongodb-org-3.2.10 jdk-1.8.0 (推荐rpm包,不然要修改Graylog启动脚本定义的JAVA命令路径) elasticsearch-2.4.1 (Graylog ...

  10. CentOS 手动配置本地yum源(参考CentOS7 制作 CentOS6本地yum源)

    将原有/etc/yum.repos.d/目录下的文件名全部改为(*.bak),如(红色标记) [root@localhost ~]# cd /etc/yum.repos.d/ [root@localh ...