WPF UserControl

一, 不一定需要自定义控件
在使用WPF以前,动辄使用自定义控件几乎成了惯性思维,比如需要一个带图片的按 钮,但在WPF中此类任务却不需要如此大费周章,因为控件可以嵌套使用以及可以为控件外观打造一套新的样式就可以了.是否需要我们来自定义控件,这需要你 考虑目前已有控件的真正逻辑功能而不要局限于外观,如果目前的控件都不能直觉地表达你的想法,那么你可以自己来打造一个控件,否则,也许我们仅仅改变一下 目前控件的模板等就可以完成任务.很多人在自定义控件上经常犯的错误是:重复撰写已有的逻辑

二,UserControl还是CustomControl?
要在WPF中自定义一个控件,使用UserControl与CustomControl都是不错的选择(除此之外,还有更多选择,比如打造一个自定义的面板,但这不在本文的讨论范围),他们的区别在于:
UserControl, 其更像WinForm中自定义控件的开发风格,在开发上更简单快速,几乎可以简单地理解为:利用设计器来将多个已有控件作为子元素来拼凑成一个 UserControl并修改其外观,然后后台逻辑代码直接访问这些子元素.其最大的弊端在于:其对模板样式等支持度不好,其重复使用的范围有限.
CustomControl, 其开发出来的控件才真正具有WPF风格,其对模板样式有着很好的支持,这是因为打造CustomControl时做到了逻辑代码与外观相分离,即使换上一套完全不同的视觉树其同样能很好的工作,就像WPF内置的控件一样.
在 使用Visual Studio打造控件时,UserControl与CustomControl的差别就更加明显,在项目中添加一个UserControl时,我们会发现 设计器为我们添加了一个XAML文件以及一个对应的.CS文件(或.VB等),然后你就可以像设计普通窗体一样设计该UserControl; 如果我们是在项目中添加一个CustomControl,情况却不是这样,设计器会为我们生成一个.CS文件(或.VB等),该文件用于编写控件的后台逻 辑,而控件的外观却定义在了软件的应用主题(Theme)中了(如果你没有为软件定义通用主题,其会自动生成一个通用主题 themes/generic.xaml, 然后主题中会自动为你的控件生成一个Style),并将通用主题与该控件关联了起来.这也就是CustomControl对样式的支持度比 UserControl好的原因.

三,继承于UserContorl,Control还是其它?
如 果你准备打造一个控件,并使用像Visual Studio这样的工具来开发的话,打造UserControl时其会自动为你从 System.Windows.Controls.UserControl继承,打造CustomControl时其会为从 System.Windows.Controls.Control继承.但实际情况下,也许我们从他们的衍生类别开始继承会得到更多的好处(更好的重用已 有的逻辑),比如你的控件拥有更多的类似于Button的某些特性,那么从Button开始继承就比从Control继承少写很多代码.

在接下来的几节中,我们会逐步讨论如何打造UserControl与CustomControl以及让它们更好支持WPF新特性.

 

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

DEMO

 

原帖:

http://www.cnblogs.com/zhouyinhui/archive/2007/10/16/926355.html

http://www.cnblogs.com/zhouyinhui/archive/2007/10/27/939920.html

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值