|
wpf传递事件和传递命令系统 |
|
|
传递事件和传递命令系统WPF中含有一个新的事件子系统----传递事件系统。传递事件是为WPF中的元素树设计的,当事件发生时,事件可以在WPF 中的视觉树和逻辑树元素间用一种简单的方式传递,而不必用户写代码。WPF中的元素树XAML是以XML语言为基础的,XML是建立在DO M之上,DOM是一棵倒挂的树。所以用XAML编写的WPF界面也是一棵倒挂的树。在桌面应用程序中,其树根通常是Window元素;WP F元素树=VisualTree+LogicalTree视觉树=界面上可见的元素(是从Visual或Visual3D中派 生出来的类)逻辑树=描述界面元素的实际结构(由界面上的所有元素构成,但不包括一些附加元素)与视觉树仅由可见元素构成不同,逻辑树 可以包括任何对象。WPF有两个类可以帮助我们遍历视觉树和逻辑树:VisualTreeHelperLogicalTreeHelper LogicalTreeHelper静态方法:GetChidren,GetParent和FindLogicaNodeGet Chidren返回IEnumrable接口,使用这个接口的的方法可以遍历当前逻辑节点上的子节点。publicstaticI EnumerableGetChildren(DependencyObjectcurrent)publicstaticIEn umerableGetChildren(FrameworkContentElementcurrent)publicstat icIEnumerableGetChildren(FrameworkElementcurrent)DependencyOb ject=FrameworkContentElement+FrameworkElement在树中只可能有一个parent所 以GetParent只返回一个DependencyObjectFindLogicalNode可以根据逻辑节点名字,来找到树中的某个 节点,在同一棵树中,各个节点的名字是唯的,所以FindLogicalNode返回指所找到节点的引用,未找到时返回null,需要用户 提供起始节点。VisualTreeHelper类中提供了更多的方法,但对于遍历逻辑树来说,我们更关心的是如何从当前节点获取父节点或 子节点的引用。VisualTreeHelper和LogicalTreeHelper遍历子节点有所不同。可以用下面的代码来遍历某个逻 辑节点下的子节点:Foreach(objectlogicaChildinLoggicalTreeHelper.GetChil dren(current)){…//dosomething}用以下方法来遍历视觉树上某个节点的子节点:IntchildNod eNum=VisualTreeHelper.GetChildrenCount(current);For(intI=0; IGetChild(current,i);…//dosomething}有两种为同地方法遍历逻辑树和视觉树,应该是两个不同的程序员 写的代码。VisualTreelHelper.GetParent(current)返回指向父节点的引用。需要指出的是,我们总是可以 用WPF内容模型来获取某个元素的逻辑子或父节点.如Window的唯一子节点是对象中的Content,而ListBox中的Items 可以加入多个子节点视觉树由于内涵元素(contentelement)的加入面复杂起来,虽然内涵元素通过视觉树在计算机上显示出来, 但内涵元素本身并不在视觉树上,它们不是Visual或Visual3D的派生类。为了让传递事件在视觉树上按照同一种机制传播,WPF通 常把内涵元素用另一个视觉元素在用户界面上展示出来.这个视觉元素叫做内涵元素的宿主元素.可以把宿主元素和内涵元素之间的关系想象为互联 网的浏览器和其中所显示的网页内容间的关系.当存在内涵元素时,视觉树不再是连续的,即视觉树即使在一个窗口中,也不是一棵树,可能会存在 多个视觉树.所以在遍历视觉树时要考虑这一点:PrivateDependencyObjectFindVisualTreeRoot (DenpendencyObejctinital){DependencyObjectcurrent=initial;De pendencyObjectresual=initial;while(current!=null){resual =current;if(currentisVisual||currentisVisual3D){curre nt=VisualTreeHelper.GetParent(current);}else{current=Logi calTreeHelper.GetParent(current);}}returnresual;}在同一个视窗下,WPF可 能会创建多个视觉树和逻辑树。对于某一个控件,往往只有一个视觉树,但即可以有多个逻辑树,这些逻辑树可以互不相关;所以只是用Logic alTreelHelper来遍历逻辑树,并不能保证能找出所有的逻辑树。由于WPF中的控件通过控件模板来显示其中的内容,控件和显示控 件的模板间是相互独立的,从而视觉树的构成会非常复杂。若需要从一个逻辑树跳到另一个逻辑树时,则需要通过下列的FrameeorkEle ment或FrameworkContentElment的TemplatedParent属性privateDependencyOb jectGetTemplatedParent(DependencyObjectclosestVisualAncestor){ FrameworkElementfe=closestVisualAncestorasFrameworkElement; FrameworkContentElementfce=closestVisualAncestorasFramework ContentElement;DependencyObjectresult;if(fe!=null){result =fe.TemplatedParent;}elseif(fce!=null){result=fce.Temp latedParent;}else{result=null;}returnresult;}传递事件(Route dEvent)为了支持元素树,WPF开发了一个传递事件的子系统。该子系统实际上是WPF接管了常规的Windows操作系统事件之后 ,把Windows事件变成WPF事件,并且让WPF事件在WPF元素树的节点上传播。这些事件基本上是在视觉树上传递的,有时候也会跨过 不同的视觉树。和Windows操作系统一样,事件的产生往往是在具有输入焦点的控件上。在事件产生后,事件沿着元素树既可以向其包容类传 播,也可以向其子类传播。WPF事件可以沿着元素树向树根或树的元素传播,但它不会传播到和它并列的元素节点的分枝上。传播事件的这两种方 式有专门的命名,事件向树根传递的叫冒泡(Bubbling),向树枝传递的叫潜入(Tunneling).当用鼠标左键单击一个Butt on时,哪个元素会最先得鼠标事件?过去可以非常明确地知道一定是按键接收到了鼠标事件。但在WPF中,并不能那么明确,因为Button 是一个Content控件,和窗口类Window一样,其中可以放置任意的控件组合比如说在Button中加入面板,在面板中加入各种图形 元素,等等.究竟是哪个元素先得到了鼠标事件?如果WFP中没有传递事件系统,那么Button类还会得到用记单击鼠标的事件吗?当你按下 键盘上的键时,哪个元素得到了键盘的输入事件?过去是具有输入焦点的控件获得键盘输入事件,但在WPF中,由于有输入焦点的元素可能不是 我们真正想要获得键盘输入焦点的元素,比如说,菜单的快捷键,那么当用户输入快捷键时,如果菜单处理程序获得键盘输入岂不是更好?所有这一 切,都需要WPF中有一个新的方式来管理用户的输入事件。RoutedEventArgs处理常规.NET事件的委托函数一般有两个参数. 一个是事件产生的对象,另一个是该事件所带有参数.例如,打开文件的菜单处理程序一般具有下面的形式:voidOnOpenFile(o bjectsender,EventArgsea){//处理该菜单}这里参数sender是object类型,即它可以是任. net类型;EventArgs则是所的事件参数的基类。.NET程序员可以根据事件本身的要求在事件参数中加入自己的参数,通常的做法是 从EventArgs或其派生类中派生出自己的类。例如:privatevoidmybtn_Click(objectsender ,RoutedEventArgse){…}有些传递事件直接用RoutedEventArgs类,有些事件则要使用它的派生类 ,RoutedEventArgs中的属性:属性意义Handle表示事件是否已经被处理,可以用来控制是否进一步传播事件Origina lSource最初产生该事件的对象RoutedEvent传递事件的实例Source发出该事件的对象WPF根据传递事件的种类,从Ro utedEventArgs中大概派生出了50个类,而且这些类的数目还在快增长。例如处理键盘事件有KeyBoardEventArgs ,处理鼠标事件有MouseEventArgs等,也可以定义自己的RoutedEventArgs。终止事件传播传递事件在元素树中传播 ,使得所有元素树中的元素都有机会对传递事件进行响应,然而,这种做法是以实时性能为代价的。有时候在对传递事件处理了之后,并不希望事件 在元素中继续传播,即终止传播。有时候则希望截获某个传递事件之后,根据自己的情况,产生一个新的传递事件,这时可能没有必要再传递原来的 事件了。典型的例子如按钮,在截获了鼠标的PreviewMouseButtonDown,PerviewMouseButtonUp事件 之后产生了一个新的Click事件,这个时候若再让原来的事件在元素中传播,显然是对计算机资源的一种浪费。RoutedEventArg s中的Handled属性就是为此设计的,如果把该属性设置为true的话,那么WPF中的传递事件系统就自动中止了该事件的传播。pri vatevoidmybtn_Click(objectsender,RoutedEventArgse){e.Handl ed=true;}事件有冒泡和潜入两种传播方式,潜入事件总是在冒泡之前传播;所以,在某个类中处理传传递事件的委托函数通常也成 对出现的,如处理鼠标有MouseDown和PreviewMouseDown。作为命名规则,WPF潜入事件总在前面加上”Previe w”.若我们在处理潜入事件时,把Handled=true,停止潜入事件传播的同时出会停止冒泡事件的传播。处理传递事件WPF程序 员需要对感兴趣的传递事件进行处理,对于键盘,鼠标这些输入设备所产生的输入事件,UIElement类中定义了大部分事件处理程序。WP F调用事件处理程序的方法有两种:一种是利用虚函数的覆盖机制,另一种是利用.net的委托函数把自己的处理函数连接到传递事件上。虚函数 覆盖方法若类是从wpf类中派生出来,就可以通过虚函数覆盖的方法来调用其事件处理方法:protectedoverridevoid OnMouseDown(MouseButtonEventArgse){base.OnMouseDown(e);...} MouseButtonEentArgs是InputeEventArgs的派生类,而InputeEventArgs则是以Routed EventArts类基类。与过去基类的的函数通常带有事件处理代码不同,WPF基类中事件处理程序的虚函数通常并不提供默认移植,即基类 中可以被覆盖的虚函数常常是空的,这时调用base.OnMouseDown()并不重要,但有时,WPF类确实提供了默热移植,若忘了调 用基类的函数,可能会有不良的结果,所以通常还是要调用基类中被覆盖的函数。使用委托函数在前面的例子中基本上都是用这种方法,可以在XA ML中接入事件处理程序:xxxx也可以用C#代码:This.Pr evieMouseLeftButtonDown+=newMouseButtonEventHandler(xxxxx)附加传递 事件(AttachedRoutedEvent)与相关属性可以附加到其它类一样,WPF的附加传递事件也可以附加到其它类上,如:< GridMouse.PreviewMouseDown=“OnPreviewMouseDown”/>Grid类中并没有定义Pr eviewMouseDown事件,但是即可以把Mouse类中定义的附加事件传递事件引入到自己的类中。C#代码中接入附加事件有点不同 ,需要调且UIElement类中的AddHandler:PublicvoidAddHandler(RoutedEventro utedEvent,Delegatehandler)如:AddHander(Mouse.PreviewMouseDownEven t,NewMouseButtonEventHandler(OnPreviewMouseDown))若要去掉:Publicv oidRemoveHandler(RoutedEventroutedEvent,Delegatehandler)考察传递事件 为了考察传递事件在元素树中传播,可以从一个简单的元素树开始,下图示出了所要观察的逻辑树。树根为window,左边的树枝主要有一个B utton;组成Button的元素为一个StackPanel中包含两个椭圆和一个TextBlock。右边的数值用来显示事项的传递结 果.osoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas. microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft .com/expression/blend/2008"xmlns:mc="http://schemas.openxmlform ats.org/markup-compatibility/2006"xmlns:local="clr-namespace:考察 传递事件"mc:Ignorable="d"Title="MainWindow"Height="300"Width="7 00"MouseDown="RoutedEventHandler"PreviewMouseDown="RoutedEven tHandler"MouseUp="RoutedEventHandler"PreviewMouseUp="RoutedEv entHandler">RoutedEventHandler"PreviewMouseDown="RoutedEventHandler"Mouse Up="RoutedEventHandler"PreviewMouseUp="RoutedEventHandler"> er"PreviewMouseDown="RoutedEventHandler"MouseUp="RoutedEventH andler"PreviewMouseUp="RoutedEventHandler">seDown="RoutedEventHandler"PreviewMouseDown="RoutedEventHandler "MouseUp="RoutedEventHandler"PreviewMouseUp="RoutedEventHandl er">="RoutedEventHandler"MouseUp="RoutedEventHandler"PreviewMouse Up="RoutedEventHandler">ndler"PreviewMouseDown="RoutedEventHandler"MouseUp="RoutedEve ntHandler"PreviewMouseUp="RoutedEventHandler">ht="50"Width="100"Fill="Brown"MouseDown="RoutedEventHandler" PreviewMouseDown="RoutedEventHandler"MouseUp="RoutedEventHandl er"PreviewMouseUp="RoutedEventHandler">Height="50"Width="100"Fill="Green"MouseDown="RoutedEventHandl er"PreviewMouseDown="RoutedEventHandler"MouseUp="RoutedEventH andler"PreviewMouseUp="RoutedEventHandler">lockMouseDown="RoutedEventHandler"PreviewMouseDown="RoutedEve ntHandler"MouseUp="RoutedEventHandler"PreviewMouseUp="RoutedE ventHandler"Text="这是一个按钮">ton>20"Height="280">< /Label>ut">indow>usingSystem;usingSystem.Collections.Generic;usingSystem. Linq;usingSystem.Text;usingSystem.Windows;usingSystem.Windows. Controls;usingSystem.Windows.Data;usingSystem.Windows.Documents ;usingSystem.Windows.Input;usingSystem.Windows.Media;usingSyst em.Windows.Media.Imaging;usingSystem.Windows.Navigation;usingSy stem.Windows.Shapes;namespace考察传递事件{//////MainWindo w.xaml的交互逻辑///publicpartialclassMainWindow:Win dow{stringoutputFormat="{0,-40}{1,-30}{2,-30}{3,-30}";publi cMainWindow(){InitializeComponent();EventOutputHeader.Content =string.Format(outputFormat,"RoutedEvent","Sender","Source" ,"OriginalSource");}privatevoidRoutedEventHandler(object sender,MouseButtonEventArgse){TextBlocktext=newTextBlock( );text.Text=string.Format(outputFormat,e.RoutedEvent.Name,Ge tTypeName(sender),GetTypeName(e.Source),GetTypeName(e.OriginalS ource));EventOutPut.Children.Add(text);(EventOutPut.ParentasS crollViewer).ScrollToBottom();while(EventOutPut.Children.Count >20){EventOutPut.Children.RemoveAt(0);}}privateobjectGetT ypeName(objectsender){string[]name=sender.GetType().ToStrin g().Split(''.'');returnname[name.Length-1];}}}自定义传递事件在属性系统中可以 自定义自己的相属性;同样的也可以定义自己的传递事件,下面是定义自己的传递事件的步骤:声明一个传递事件publicstaticr eadonlyRoutedEventYclickEvent;publicstaticreadonlyRoutedEve ntPreviewYclickEvent;两个传递事件被定义为staticreadonly这是必需的。向传递系统注册传递事件Y clickEvent=EventManager.RegisterRoutedEvent("Yclick",RoutingSt rategy.Bubble,typeof(RoutedEventHandler),typeof(MainWindow));Pre viewYclickEvent=EventManager.RegisterRoutedEvent("Yclick",Rout ingStrategy.Tunnel,typeof(RoutedEventHandler),typeof(MainWindow) );需要在构造函数中注册.第三个参数为传递事件处理函数的类型,第四个参数说明传递事件在哪个类中定义。3.定义传递事件的接入属性pu bliceventRoutedEventHandlerYclick{add{AddHandler(YclickEve nt,value);}remove{RemoveHandler(YclickEvent,value);}}publ iceventRoutedEventHandlerPreviewYclick{add{AddHandler(Prev iewYclickEvent,value);}remove{RemoveHandler(PreviewYclickEve nt,value);}}4.产生传递事件需要借助UIElement类中的RaiseEvent方法:RoutedEventAr gsargsEvent=newRoutedEventArgs();argsEvent.RoutedEvent=Mai nWindow.YclickEvent;argsEvent.Source=this;RaiseEvent(argsEven t);完成上面4个步骤之后,就可以使用自定义的事项了。代码下载管理键盘和鼠标输入事件人主要通过三种途径向个人计算机发出指令:键盘 ,鼠标和手写设备。对应于这三种输入设备,WPF有三个类与之对就:Keyboard.Mouse和Stylus.当用户使用这三种输入 设备时,WPF产生相应的传递事件在WPF元素上传播。除了这三种设备类外,输入事件集中在WPF的四大基类:UIElement,Fra meworkElement,ContentElement,FrameworkContentElement中进行处理。键盘输入键盘事 件是一种传递事件,它可以沿着WPF中的元素树向根或枝传递。WPF中的传递事件采用一种开方式的架构,在UIElement和Conte ntElement中都可以附加传递事件,这两个类是WPF大多UI元素的基类,所以传递事件可以附加到任何WPF的控件中。所有与键盘相 关的事件都定义在Keyboard类中;所有与鼠标相关的事件都定义在Mouse类中;所有与手写输入相关的事件都定义在Stylus类 中。这些传递事件都是以附加事件的形式存在,将来一旦有新的输入设备出现,WPF只要创建一个新的类,并在这个类中定义相应的除加事件,然 后就可以附加到所有的控件中,这样可以很容易地支持新的输设备。传递事件名传递方式功能描述GetKeyBaordFocus冒泡当元素获 得输入焦点时KeyDown冒泡当按下键盘时产生KeyUp冒泡当释放键盘时产生LostKeyFocus潜入xxxxxPreviewK eyBoardFocus潜入xxxxxPreviewKeyDown潜入xxxxPreviewKeyUp潜入xxxxPreviewL ostKeyboardFocus潜入xxx当在键盘上按下一个键时,总有某个应用程序中的某个元素获得输入消息,这个消息被WPF翻译成 传递事件,然后在元素树中传播.当按下Tab键,或者用鼠标单击某个图形元素时,键盘的输入焦点有图形元素间移动。当同时按下”Alt” 和“Tab”键时,可以有不同的应用程序间切换。鼠标输入鼠标输入事件被封装在了Mouse类当中。俘获鼠标所谓俘获鼠标是指在应用程 序中设置鼠标的一种状态,这时哪怕光标离开了应用程序的窗口,应用程序仍然可以接收到鼠标的信息。与之相反的操作就是释放鼠标,这种功能对 于开发拖放功能非常有用。有WPF中俘获和释放鼠标非常简单,一种方法是调用UIElement类中的CaptureMouse()和Re leaseCaptureMouse()方法。另一种是调用Mouse类的静态Capture方法。使用第一种方法的前提是UIEleme nt类要在你的类的继承树上,多数需要捕获鼠标的元素都是从UIElement的派生类。Mouse类中所支持的附加事件传递事件名传递方 式功能描述GotMouseCapture冒泡当元素捕获到鼠标时,发出的消息。当元素捕获到鼠标时,所有鼠标事件都发给该元素。即使输入 焦点不在元素所在的应用程序上时,鼠标的事件仍然被发送到该元素上LostMouseCapture冒泡当元素不同捕获鼠标时,产生这一事 件MouseDown冒泡当鼠标在元素上按下时,产生该事件MouseEnter冒泡当鼠标进入元素的几何范围时,产生该事件MouseM ove冒泡当鼠标进入移动时MouseLeave冒泡与MouseEnter相对MouseWheel冒泡当鼠标滚轮移动时产生Previ ewMouseDownPreviewMouseEnterPreviewMouseMovePreviewMouseLeavePrev iewMouseWheelQueryCursor冒泡当询问鼠标位置时产生输入焦点为WPF支持丰富的用户界面某个呈现在用户前面的图形 元素可能是由多个元素组合而成的.当这些元素前后叠加时,究竟哪个鼠标得到的输入焦点?在二维图形元素当中通常是在最上面的元素获得输入焦 点,然而,有时候希望某个图形的下面的元素获得输入焦点.比如,要在界面上展现一个盒子,盒子里放着红色,白色和黑色的小球,希望当用户把 盒子的盖子打开时,用户可以用鼠标操作把其中的一个小球一个一个取出来.在这种情况下,小球在的界面不是处在最上的位置,要用刀标把小球取 出来的第一项工作就是小球工能够获取输入焦点.WPF地解决方案是若在绘制元素时的画刷不是null,那么图形元素就可以获得输入焦点。若 绘制的元素了画刷是null那么这个元素主不能获得输入焦点。面对上面的问题只要把画元素的画刷设置为null即可鼠标经过元素当多个图形 元素叠加在界面上,当鼠标在界面上移地劝的时候,可能多个元素都会产生MouseOver的传递事件。有时候希望知道鼠标是否直接在某个元 素的上面,例如,按钮可能帽多个图形,TextBlock等控件组合起来,当鼠标的光标可能在图形元素上,也可能在TextBlock上, 但都在按钮的几何范围内。这个时候想要知道是否在某个元素上,需要调用DirectlyOver(Mouse类的)方法,该方法返回IIn putElement.获取鼠标状态鼠标键的状态用MouseButtonState来描述,这是一个枚举类型。它有两个值:Press Released.WPF支持5个鼠标键,要获取某个键的状态,直接读取Mouse类中相就的属性即可。获取鼠标位置GetPosi tion设置光标SetCursor传递命令在大型软件工程中,一个软件往往由多个模块组成,通常希望模块和模块之前的相关性越少越好, 用专门的术语叫做decuple,从面把对一个模块的修改对其它模块的影响降到最低。使命令这种设置范例就可以达到目的:发布命令的模块对 于命令如保执行一无所知,接收命令的模块对命令加以解释并具体执行。Invoker是命令的发出者,ConcretoCommand实现了 Command,ConnectoComand知道命令要作用在哪个对象上。WPF不仅支持命令范例,而且实现了命令和传递事件一样在元素 树上的传递,WPF更进一步开发了常用命令的功能。WPF传递命令的作用机制如下图示:通常菜单条目或按钮发出相关命令,WPF的合作仓库 中(CommandRespository)支持一系列命令,然后把命令绑定到用户界面上的UI元素上。ICommand接口WPF中的命令至少要移植ICommand接口,在ICommand接口中,定义了两个方法和一个 事件:CanExecute():turefalse,表示命令是否可以执行Execute(object)执行命令一个事件是CanE xcuteChanged,当命令从不能执行转变到可以执行或从能执行转变到可以执行时,产生这一事件。ICommandSource接口 要成为可以发出命令的对象,需要移植ICommandSource接口。ButtonBaseMenuItemListBoxItemHy perLink这些类都移植了ICommandSource接口ICommandSource有三个属性:Command要发出的命令C ommandParameter发出命令时的参数CommandTarget命令作用的对象CommandTarget是指要执持命 令的元素。比如,粘贴命令要在TextBox上执行,那么TextBox就是粘贴命令的对象元素CommandBinding命令绑定就是 把处理这个命令的事件联系起来的一种机制。mand="ApplicationCommands.Open"Executed="CommandBinding_Execute d"CanExecute="CommandBinding_CanExecute"/>dings>当发出ApplicationCommands.Open命令时,上述传递事件处理程序就被调用。传递命令只要移植了ICom mand的对象就是传递命令,传递命令像传递事项一样可沿关WPF元素树传递。实际上,它与通常的命令不同地昌,当调用CanExecut e方法时,传递命令产生CanExecute(冒泡)和PeviewCancExecuted两个传递事件,所以传递命令实际依然是传递事 件。理解了事件在元素树中传播,理解命令传递就举手之劳了。WPF命令字库(CommandRepository)WPF的命令仓库移植了 五大类命令:ApplicationCommandsNavigationCommandsMediaCommandsEditingCo mmandsComponentCommands例程:ns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"x mlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="h ttp://schemas.microsoft.com/expression/blend/2008"xmlns:mc="htt p://schemas.openxmlformats.org/markup-compatibility/2006"xmlns: local="clr-namespace:WPF命令"mc:Ignorable="d"Title="MainWindow" Height="350"Width="525">ButtonBase移植了ICommandSource接口,所以可以把Button的Command属性连接到Help上Command=“Help”Help是一个传递命令,当按下help时,它就会在视觉树上传播,若不处理这个命令则它什么事也没做。使用命令绑定截获这个命令:在C#中实现命令绑定publicMainWindow(){InitializeComponent();CommandBindingcb=newCommandBinding(myCommand,ExecuteMyCommand,CanExecuteMyCommand);this.CommandBindings.Add(cb);myConnamdButton.Command=myCommand;} |
|
|
|
|
|
|
|
|
|
|