在WPF中自定义控件

周银辉的开发博客(WPF)

在WPF中自定义控件(1)


一, 不必定须要自定义控件
在使用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新特性.html

在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中的命令与命令绑定(二).
这里咱们来定义一个命令,其功能是控件的语音报时.首先咱们定义一个命令:
ide

         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

WPF中的命令与命令绑定(一)

说到用户输入,可能咱们更多地会联想到键盘、鼠标、手写笔,其实还用一种高级别的输入——命令(Commands),从WPF类库角度讲他们分别对于Keyboard,Mouse,Ink与ICommand。命令是一种语义级别的输入而不是设备级别的,好比“复制”与“粘贴”,但实现一个命令能够有不少中方式,好比“粘贴”,咱们可使用CTRL-V,也可使用主菜单或右键菜单(上下文菜单)等等。在以往的.net版本中,要在软件界面上添加一个“粘贴”按钮,是很是麻烦的事情,你得监视剪切板中是否有可用的文本以及对应的文本框是否得到了焦点以便启用或禁用该按钮,当粘贴时你还得从剪切板中取得相应的文本并插入到文本框的合理位置,等等。

在WPF中提供的命令机制能很是简单地实现这些任务,下面的Demo演示了如何简单到不用手动编写一行后台逻辑代码便解决上面的难题的,你能够粘贴下面的代码到XamlPad:
< Window
    
xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x
="http://schemas.microsoft.com/winfx/2006/xaml"
    x:Name
="Window"
    Title
="Window1"
    Width
="640"  Height ="480" >

    
< DockPanel  LastChildFill ="True" >
        
< Menu  Width ="Auto"  Height ="20"  DockPanel.Dock ="Top" >
            
< MenuItem  Command ="ApplicationCommands.Copy"  Header ="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}" />
            
< MenuItem  Command ="ApplicationCommands.Paste"  Header ="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}" />
            
< MenuItem  Command ="ApplicationCommands.Cut"  Header ="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}" />
            
< MenuItem  Command ="ApplicationCommands.Redo"  Header ="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}" />
            
< MenuItem  Command ="ApplicationCommands.Undo"  Header ="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}" />
        
</ Menu >
        
< RichTextBox >
            
< FlowDocument >
                
< Paragraph />
            
</ FlowDocument >
        
</ RichTextBox >
    
</ DockPanel >
</ Window >

commands1-1.png
Demo中菜单栏的菜单项不只仅能完美地完成任务并且能根据文本框的状态和剪切板自动的启用与禁用,而咱们却没有为这些复杂的逻辑编写任何的后台代码。这就是WPF中的命令机制为咱们提供了方便。

注意这一行代码:工具

< MenuItem  Command ="ApplicationCommands.Copy"  Header ="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}" />

咱们将“复制”命令(ApplicationCommands.Copy)赋值给了菜单项的Command属性,实现了ICommandSource接口的元素都拥有该属性,这表示该元素能够做为一个“命令源”来引起某个命令,其Command属性就指示了其将引起的命令。

其实一个命令系统是被分为四个部分的:
Command(命令):一个语义级别的输入,好比“复制”,“左对齐”,“播放”等
CommandSource(命令源):引起某命令的元素,好比按钮,菜单项,键盘(Ctrl-C,F1等),鼠标等。
CommandTarget(命令目标):命令被做用的目标,好比文本框,播放器等。
CommandBinding(命令绑定):用于将命令和命令的处理逻辑连接起来,好比一样的"粘贴",但粘贴文本和粘贴图片的处理逻辑是不同的,命令绑定负责将“粘贴”命令与合理的处理逻辑链接起来。
关于命令系统将在本文章的后续部分中讲解,不过值得一提的是,在上面的Demo中咱们只指定了命令和命令源,并未指定命令目标,但它会以获取键盘焦点的元素(这里是咱们的RichTextBox)做为默认值,而命令绑定以及命令的后台执行逻辑被隐藏到了RichTextBox内部,那些编写RichTextBox控件的开发人员会为咱们编写该部分代码。

另外,你可能已经发现,在Demo中咱们并无为菜单项标题直接设置“复制”“粘贴”这样的文本,而是使用了以下的一个绑定:post

Header="{Binding Path=Command.Text, RelativeSource={RelativeSource Self}}"/>

咱们将菜单文本绑定到了命令的Text属性,这是由于,若是一个命令为RoutedUICommand类型,那么该命令将有一个Text属性来讲明该命令对应到的文本名称,该Text属性会自动本地化的,也就是说若是你的计算机使用语言是简体中文的话该菜单项显示的是“复制”,若是你的计算机使用的语言是英语的话该菜单项显示的将是“Copy”。


WPF为咱们提供了大量内置命令,包括ApplicationCommands,NavigationCommands,,MediaCommands,EditingCommands与ComponentCommands,以及控件开发人员为它们的控件也提供了不少特有的命令(好比Slider.DecreaseLarge 与 Slider.DecreaseSmall),这些足以应付平时的大多数应用,若是还不够的话,你能够为本身的应用自定义更多的命令。学习

在本随笔的后续部分咱们将更加深刻的探讨WPF的命令系统,敬请关注,谢谢。ui

WPF中的命令与命令绑定(二)

在WPF中,命令(Commanding)被分割成了四个部分,分别是ICommand,ICommandSource,CommandTarget和CommandBinding。下面咱们来分别探讨这四个部分。

1,ICommand
Command也就是咱们的“命令”自己,好比“复制”“粘贴”。在WPF中,全部的命令都必须实现ICommand接口,它为全部的命令提供一个抽象,这个抽象对于咱们实现Undo、Redo操做很是重要,若是你学习一下设计模式中的“命令”模式,你会更加深入的理解。
ICommand接口中拥有Execute()方法,该方法用于命令的执行(不过,注意:命令的执行逻辑——好比将剪切板中的文本去出来放到文本框的合适位置——并无被编写到该方法中,稍后咱们会讲到这其中的奥妙),另外的一个方法是CanExecute()用于指示当前命令在目标元素上是否可用,当这种可用性发生改变时其便会引起该接口的尾页一个事件CanExecuteChanged。
在目前的WPF类库中,你能看到惟一一个实现了ICommand接口的类型RoutedCommand(其实还有一个名为SecureUICommand的类也实现了该接口,不过该类未被公开),“Routed”是一个不太容易被翻译的修饰词(有人将它翻译为“路由”),但这意味着该类型的命令能够向WPF中的RoutedEvent同样在元素树中上下传递。
RoutedCommand的子类RoutedUICommand是咱们常用的类型,它与RoutedCommand的不一样之处仅仅在与它多了一个Text属性来描述该命令,不过大多数WPF内置命令的Text属性有一个很不错的特色:其支持自动本地化。这至少会为咱们的软件的本地化减小工做量。

在本系列随笔的后续部分将介绍如何自定义一个命令。

2,ICommandSource与CommandTarget
命令源,用来触发咱们的命令,好比用一个菜单项来触发“复制”命令,那么该菜单项就是命令源。要使一个元素成为命令源,其必须实现ICommandSource接口。命令源决定了它所要触发的命令、该命令所做用的对象以及命令参数(若是须要的话),这分别对应于它的三个属性:Command、CommandTarget以及CommandParameter。其中须要注意的是CommandTarget,由于在WPF中若是你不为命令源指定其命令对象,那么其将会把界面上得到键盘焦点的元素做为默认的命令对象,这为咱们提供了方便,好比界面上有两个文本框,咱们没必要担忧主菜单项上的“粘贴”操做是针对哪一个文本框的,谁得到焦点便针对谁,这符合你们的习惯。但引入的问题是,若是命令目标不具有获取键盘焦点的能力(好比Label)或命令源会抢占焦点(好比用Button来代替菜单项,点击按钮时焦点由文本框转移到了按钮上),你的命令将会无效,这时你就必须为命令源指定命令目标。

在本系列随笔的后续部分将介绍如何让你的自定义控件成为命令源和命令目标。

3,CommandBinding
前面已经提到咱们并无将命令的执行逻辑编写到其Excute()方法中,这是有道理的,好比"粘贴"命令(ApplicationCommands.Paste),粘贴一段文本到文本框和粘贴一个图片到绘图板的执行逻辑确定是不同的,负责开发该“粘贴”命令的开发人员不可能知道全部的粘贴操做的具体逻辑,使用“粘贴”命令的客户也不该该为该执行逻辑负责,编写该执行逻辑的任务应该被分发给那些支持“粘贴”操做的控件的开发人员以及那些但愿为本身的控件添加“粘贴”操做的客户。也就是说咱们须要将“行为的请求者(命令)”和“行为的执行者(命令的执行逻辑)”分开而实现一种松耦合,而CommandBinding(命令绑定)即是命令和命令执行逻辑的桥接器。
咱们使用CommandBinding将命令与其合适的执行逻辑绑定在一块儿:this

 CommandBinding CloseCommandBinding  =   new  CommandBinding(
    ApplicationCommands.Close, CloseCommandHandler, CanExecuteHandler);

CommandBinding构造方法的最后两个参数分别是ExecutedRoutedEventHandler 与 CanExecuteRoutedEventHandler 类型的委托,用于指示如何执行命令和如何判断命令可否被执行。
与CommandBinding同样扮演着中间角色的还有CommandManager类,它为命令绑定(以及输入绑定)提供了不少实用方法。

在本系列随笔的后续部分将介绍WPF的命令系统与“命令模式”(设计模式之一)之间的关系。url