6.1 从代码了解事件:信号灯应用 6.2 Flex的“事件之旅” 6.3 Event对象 6.4 EventDispatcher类 6.5 5个步骤创建自定义事件 6.6 自定义事件代码样例 对于任何涉及图形用户界面的开发来说,事件控制都是基本技术。操作系统要不断监视环境中发生的各种事件,比如鼠标点击或键盘输入等,然后负责把这些事件报告给正在运行的应用程序,由应用程序决定对这些事件做出什么样的响应。Flex当然也不例外。 相对于B/S时代浏览器应用的开发者来说,那些从传统的C/S架构时代走过来的程序员能够更深切地体会到Flex应用开发的真谛:基于组件的事件驱动模型。如今这个“概念为先”的时代,层出不穷的新名词令人眼花缭乱,然而拨开迷雾看本质,我们会发现那些十几年前就提出的基础概念具有强悍的生命力,许多新技术只不过是为它们披上了一些华丽的外衣。以Flex来说,MVC、松散耦合、事件驱动、组件开发这些基本的概念构成了Flex强壮的架构。得益于Flex这件华丽的外衣,这些概念又重新回到了互联网舞台的中心。 Flex开发者离不开两件事:组件和事件。可以说,Flex开发就是基于组件的事件驱动编程。事件不仅能够帮助开发者响应用户操作、完成应用任务,更重要的是,充分利用事件可以使开发者设计出松散耦合的Flex应用架构。从这个角度来说,Flex并不比10年前的Visual Basic、Visual C++等C/S时代的编程语言更特别,事件驱动模型仍然是Flex开发的核心。 我们将在本章深入解析Flex的事件模型,从介绍最基本的概念开始,最终引导你定制自己的事件,打下成为Flex大师的基础。 6.1 从代码了解事件:信号灯应用 6.1.1 事件代码范例:创建信号灯应用 让我们直接从代码开始。 我们要创建一个简单的信号灯应用,如图6-1所示。当用户点击“绿色”、“红色”或“蓝色”按钮,将会显示相应信息。比如点击红色按钮后,会显示 “交通信号灯:红色”。 图6-1 事件代码样例:信号灯应用 1. 创建新的Flex项目TrafficLight,设置应用的layout为“absolute”。 2. 在设计模式中,拖曳Panel容器到应用中,设置属性: width="250" height="220" layout="absolute" horizontalAlign="center" title="事件样例" fontsize=”16” 3. 拖曳Lable(标签)到HBox容器中,属性为: x="80" y="45" text="关闭" id="lblLightInfo" horizontalCenter="0" 4. 在设计模式中,拖曳HBox容器到Panel,设置属性: x="10" y="104" id="ctnButtons" 5. 在设计模式中,拖曳三个按钮放置在HBox中,按钮的id和lable属性分别设为: id="btnGreen" label="绿色"; id="btnRed" label="红色"; id="btnBlue" label="蓝色"。 6. 切换回代码模式,现在你的代码应该看起来如代码6-1。组件的位置属性无关紧要,你可以按照你喜欢的方式进行设置。 代码6-1:信号灯应用的布局 <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute"> <mx:Panel x="372" y="56" width="250" height="220" layout="absolute" id="traficLight" title="事件样例" fontSize="16"> <mx:Label x="80" y="45" text="关闭" id="lblLightInfo"/> <mx:HBox x="10" y="104" width="100%" id=”ctnButtons”> <mx:Button label="绿色" id="btnGreen"/> <mx:Button label="红色" id="btnRed"/> <mx:Button label="蓝色" id="btnBlue"/> </mx:HBox> </mx:Panel> </mx:Application> 7. 在Application中加入系统事件creationComplete="initApp();",这样当应用构造完毕后,会自动调用initApp方法执行我们需要的操作,如代码6-2所示。 代码6-2:信号灯应用creationComplete事件 <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="initApp();"> 8. initApp()方法将为应用增加一些关键代码。当用户点击按钮时,这些代码能够引导指定的“工作人员”去处理用户的操作。在这个例子中,myEventHandler方法就是这个“工作人员”。我们在<mx:Application>下加入如下ActionScript代码段6-3。 代码6-3:信号灯应用initApp事件 <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="initApp();" layout="absolute"> <mx:Script><![CDATA[ private function initApp():void { ctnButtons.addEventListener(MouseEvent.CLICK, myEventHandler); } ]]></mx:Script> initApp方法中使用了addEventListener方法,告诉Flash Player,一旦发生了MouseEvent.CLICK事件(也就是某个人点击了按钮),就召唤“工作人员”myEventHandler来进行处理。 9. 接下来,我们要在Script代码段中定义myEventHandler()方法,如代码6-4所示。myEventHandler要更新标签lblLightInfo的显示,说明哪个“交通信号灯”被“打开”了。 代码6-4:信号灯应用myEventHandler事件 private function myEventHandler(event:Event):void { if(event.target is Button){ lblLightInfo.text="交通信号灯 :" + Button(event.target).label ; } } 10. 好了,我们看看最终的代码全貌,如代码6-5所示。 代码6-5:信号灯应用代码 <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="initApp()"> <mx:Script> <![CDATA[ private function initApp():void { ctnButtons.addEventListener(MouseEvent.CLICK,myEventHandler); } private function myEventHandler(event:Event):void { if(event.target is Button){ lblLightInfo.text="交通信号灯 :" + Button(event.target).label ; } } ]]> </mx:Script> <mx:Panel x="372" y="56" width="250" height="220" layout="absolute" id="traficLight" title="事件样例" fontSize="16"> <mx:Label x="80" y="45" text="关闭" id="lblLightInfo" horizontalCenter="0"/> <mx:HBox x="10" y="104" width="100%" id="ctnButtons"> <mx:Button label="绿色" id="btnGreen"/> <mx:Button label="红色" id="btnRed"/> <mx:Button label="蓝色" id="btnBlue"/> </mx:HBox> </mx:Panel> </mx:Application> 6.1.2 分析“信号灯应用”中的事件 我们可以通过“信号灯应用”初步了解Flex的事件模型。这个简单的例子包含了两个事件:creationComplete和MouseEvent.CLICK。 讨论MouseEvent.CLICK 这里没有什么高科技,只是某人点击了按钮,然后应用做出响应,更新了标签的显示信息。 从程序员的视角看,这个过程涉及了三个主要元素,也正是事件的三个要素: ・ 事件源:事件源指的是发生事件的对象。本例中即三个按钮; ・ 侦听器:侦听器是一个具体的方法,负责处理事件响应。侦听器要被注册到某个可以接收到事件通知的对象上。本例中,侦听器是myEventHandler方法,被注册到按钮的容器ctnButtons对象上; ・ 事件:事件本身就是一个对象。侦听器在处理事件的时候,有时候需要知道事件的来龙去脉,比如事件源是谁?发生了什么事情?事件对象封装了这些信息。本例中,侦听器myEventHandler方法的参数即事件对象。 从Flex程序编写的角度看,全过程如下: 首先放置了“事件源”:三个按钮btnGreen、btnRed、btnBlue。三个按钮被置于一个容器ctnButtons中。 创建事件侦听器myEventHandler方法。侦听器根据事件对象内封装的信息,决定如何响应事件。这里的情况有一点复杂,侦听器myEventHandler还须要解决两个问题。 1. 用户点击的不是按钮怎么办? 在注册事件侦听器的时候,我们只告诉Flash Player在发生MouseEvent.CLICK事件时调用myEventHandler。这就意味着,用户点击任何容器中的对象,都会触发该事件。举个例子来说,如果容器中除了按钮外,还包含了一个文本输入框组件,用户点击了该组件,会发生什么?FlashPlayer会即刻调用侦听器myEventHandler,但是我们的侦听器很聪明,他会检测事件源是否为“按钮”,如果不是按钮,那么什么都不会发生。秘诀在于传递给myEventHandler的Event类型参数:event。Event是在事件发生时,由Flash Player隐式创建的,因此你看不到该对象的声明和创建代码。Event描述了所发生的事件,这里event.target告诉myEventHandler事件源是谁。 代码event.target is Button检查事件源是否为按钮对象。 2. 用户按了哪个按钮? 在我们的例子中,myEventHandler知道用户点击了哪个按钮,继而相应地更新lblLightInfo标签信息。这同样是通过事件对象来实现的。Event对象实例event通过event.target告诉了myEventHandler谁是事件源,继而通过强制类型转换Button(event.target),得到了发生事件源按钮对象,从而获取了它的label属性。 之后,addEventListener方法把“事件侦听器”myEventHandler注册在容器ctnButtons上: MouseEvent.CLICK事件发生之后(民间说法即:有人点击了按钮),Flash Player立即向“全世界”广播该事件消息。当发现容器ctnButtons注册了针对MouseEvent.CLICK事件的侦听器myEventHandler时,就调用该方法,并把事件对象发送过去。 讨论creationComplete 与MouseEvent.CLICK事件不同,事件creationComplete并不是由用户的操作触发的。当信号灯应用初始化完毕,Flash Player就会自动触发该事件。 有意思的是,在这里我们并没有看到像MouseEvent.CLICK事件那样,通过addEventListener注册creationComplete事件的侦听器,那么,Flash Player如何知道谁来处理该事件呢?实际上,我们仍然为该事件注册了事件侦听器initApp()。只不过使用了另外一种方式:在MXML标签中定义事件侦听器:<mx:Application … creationComplete="initApp()">。在事件发生后,Flash Player会自动调用initApp()方法。 在6.3.3节“Flex应用启动的事件序列”(见第108页)将会深入讨论creationComplete及与它类似的一些事件。 通过这个例子,我们看到了Flex是如何利用事件处理用户操作的。然而,这个示例远远没有覆盖Flex事件的所有方面。现在,我们回到概念本身,更系统地学习Flex事件模型。 6.2 Flex的“事件之旅” 信号灯的应用中,为什么在点击按钮后,就会自动调用myEventHandler方法呢?看官答道:“我们把myEventHandler作为事件侦听器注册到了容器ctnButtons上了!”没错,但是为什么在容器上注册了侦听器,Flash Player就能够调用myEventHandler方法呢?Flash Player如何发现有这样一个侦听器?如果我们在其他的容器,比如ctnButtons的父容器traficLight上为同样的事件注册了侦听器,会发生什么?Flash Player维护了一个侦听器队列吗?如果是这样的话,那队列中成员的顺序又如何呢? 其实并没有一个所谓的侦听器队列。但Flash Player确实维护了一个树状列表,即显示列表(详见第7章的7.1.1节“显示列表”(见第126页))。Flash Player创建事件对象,并调度该对象到事件流。事件流就是事件对象在显示列表中的“旅程”。 当事件发生时(比如用户按下了按钮),Flash Player即创建Event对象,事件之旅也由此刻开始了。然而,事件之旅的起源地并不在“此地”,而是从显示列表的根节点Stage(flash.display.Stage,一个特别的显示对象容器,显示列表的根节点)开始,然后沿着列表逐级向下,直到发生事件的对象。之后,又按相反的方向逐级向上回到根节点。没错,事件之旅是个往返旅程。而且,需要强调的是,事件旅程并不包括从根节点Stage对象到发生事件的对象之间的所有节点,而只涉及发生事件的对象本身和它的父容器。比如,信号灯应用中,Flash Player就不会检查lblLightInfo对象,因为它并不是三个按钮对象的父容器。在这段旅程中,Flash Player逐一检查这些对象是否针对发生的事件注册了侦听器,为事件对象赋值,并调用侦听器。 概念上,Flex的事件旅程分为三个阶段:捕获阶段、目标阶段和冒泡阶段,如图6-2所示。既然事件之旅同显示列表密切相关,在旅程开始之前,我列出了信号灯应用的树状显示列表以供参考。同时标出了当用户按下“红色”按钮时事件旅程的三个阶段。 图6-2 信号灯应用的显示列表和事件流 6.2.1 target和currentTarget 如我们刚才提到的,在事件被触发后,Flash Player就会创建事件对象,并逐一检查“事件旅程”上的节点是否针对发生的事件注册了侦听器,为事件对象赋值,并调用侦听器。事件对象源自flash.events.Event类。事件对象currentTarget属性的值会在事件流中改变,而target属性则不会变化。(关于事件对象,我们将在6.3节(见第106页)深入讨论)由此,开发者能够通过currentTarget属性获知事件旅程现在停在了哪个节点上。 ・ currentTarget属性:事件旅程中,currentTarget属性代表了Flash Player正在检查的节点对象。比如,当Flash Player遍历到ctnButtons对象,那么event.currentTarget就是ctnButtons对象。 ・ target属性:target属性就是发生事件的对象。在信号灯应用中,event.target就是用户所点击的按钮对象,在事件旅程中,该属性的值始终不变。信号灯应用中,侦听器myEventHandler利用event.target来获取被点击按钮上的标签数据。此时赋给event.currentTarget属性的则是HBox容器对象ctnButtons。 6.2.2 捕获阶段 在事件发生之后,Flash Player将沿着显示列表,从根节点Stage开始遍历事件发生对象的所有父对象,这个阶段称之为“捕获阶段”。捕获阶段截止到发生事件对象的直接父对象。在遍历的过程中,Flash Player会检查是否有节点注册了事件侦听器,并对生成的事件对象赋值,调用事件侦听器方法。 在信号灯应用中,在事件发生后,Flash Player将顺序检测Stage、SystemManager、Application、traficLight、ctnButtons,至此为捕获阶段。 需要指出的是,在默认情况下,容器们并不在捕获阶段“侦听”事件。信号灯的应用中,侦听器myEventHandler实际上是在冒泡阶段被调用的。当我们在ctnButtons上注册侦听器时,默认设置了addEventListener方法的use_capture属性为“false”。如果你希望在捕获阶段调用侦听器,那么要使用如下代码注册侦听器,见代码6-6: 代码6-6:在捕获阶段响应按钮事件 ctnButtons.addEventListener(MouseEvent.CLICK,myEventHandler,true); 通常情况下,开发者更习惯于利用事件旅程的目标阶段和冒泡阶段处理事件响应。 6.2.3 目标阶段 目标阶段只涉及一个对象,即发生事件的对象。在目标阶段,事件对象的target和currentTarget对象被赋予相同的值,也就是发生事件的对象。 在信号灯应用中,如果用户按下了红色按钮,那么在目标阶段,Flash Player将只检测btnRed对象。如果我们在btnRed按钮上注册侦听器方法(假设为redEventHandler(event Event)),则在目标阶段,系统会调用redEventHandler方法。在该方法中,如果检测event.target和event.currentTarget属性,将会发现两者均指向btnRed按钮。 6.2.4 冒泡阶段 冒泡阶段与捕获阶段涉及的节点完全相同,但遍历顺序却恰好相反。在Flash Player对发生事件对象检查完毕后,将从其直接父容器开始,沿着显示列表向上,最终返回到Stage根节点。 在信号灯的应用中,目标阶段结束后,Flash Player将依次检测ctnButtons、traficLight、Application、SystemManager,最终返回到Stage,此为冒泡阶段,而事件之旅也告一段落。 6.2.5 信号灯应用的事件之旅 最后,通过代码再一次体验Flex的“事件之旅”。依然以信号灯的应用为基础,经过改造的应用代码见代码6-7,新的项目名为EventJourney。 代码6-7: EventJourney项目代码 <mx:Script> <![CDATA[ import mx.controls.Alert; private function initApp():void { btnRed.addEventListener(MouseEvent.CLICK,myEventHandler); traficLight.addEventListener(MouseEvent.CLICK,myEventHandler); ctnButtons.addEventListener(MouseEvent.CLICK,myEventHandler); lblLightInfo.addEventListener(MouseEvent.CLICK,myEventHandler); this.addEventListener(MouseEvent.CLICK,myEventHandler); //在捕获阶段调用EventListener btnRed.addEventListener(MouseEvent.CLICK,myEventHandler,true); traficLight.addEventListener(MouseEvent.CLICK,myEventHandler,true); ctnButtons.addEventListener(MouseEvent.CLICK,myEventHandler,true); lblLightInfo.addEventListener(MouseEvent.CLICK,myEventHandler,true); this.addEventListener(MouseEvent.CLICK,myEventHandler,true); } private function myEventHandler(event:Event):void { trace(event.currentTarget.toString() + " 在事件阶段" + event.eventPhase); } ]]> </mx:Script> 在initApp()方法中,我们为btnRed、traficLight、ctnButtons、lblLightInfo和Application本身(通过this)分两次注册了同一个侦听器myEventHandler。第二次注册中,我们设置use_capture参数为true。 在myEventHandler中使用trace方法输出了侦听器被调用时的当前节点对象和事件所在阶段。事件属性event.eventPhase标志着当前所处事件旅程的阶段。返回值为unit类型,包含了代表着三个阶段的numeric值。分别为: ・ 捕获阶段:EventPhase.CAPTURING_PHASE=1; ・ 目标阶段:EventPhase.AT_TARGET=2; ・ 冒泡阶段:EventPhase.BUBBLING_PHASE=3。 在按下红色按钮后,输出结果如代码6-8所示。 代码6-8: EventJourney项目代码输出结果 EventJourney0 在事件阶段1 EventJourney0.traficLight 在事件阶段1 EventJourney0.traficLight.ctnButtons 在事件阶段1 EventJourney0.traficLight.ctnButtons.btnRed 在事件阶段2 EventJourney0.traficLight.ctnButtons 在事件阶段3 EventJourney0.traficLight 在事件阶段3 EventJourney0 在事件阶段3 关于结果的说明: ・ 尽管我们为btnRed按钮注册了两次侦听器,但是由于Flash Player只在目标阶段检查该按钮,因此侦听器只在目标阶段被调用一次; ・ 尽管我们为lblLightInfo标签也注册了侦听器,但由于该对象并不在事件旅程覆盖的节点中,因此不会产生任何输出。 6.3 Event对象 如我们之前所说,在Flex的事件之旅中,一旦事件发生,Flash Player就会隐式地创建事件对象。这些事件对象在Flex的事件模型中发挥了重要作用。 所有的事件对象都源自同一个基类flash.events.Event。Event对象的属性包含了所发生事件的具体信息,同时,开发者能够利用Event对象的一系列方法来操纵事件,甚至影响系统对事件的处理。 6.3.1 事件对象重要的属性和方法 flash.event.Event类的全部6个属性都是只读属性,这些属性提供了发生事件的基本信息。我们已经讨论过target和currentTarget属性(见6.2.1节的事件属性:target和currentTarget(见第103页)),并在“EventJourney的事件侦听器myEventhandler中使用eventPhase属性来获取事件旅程的阶段。Event类也提供了许多方法。我们只介绍事件对象常用的属性和方法,更详细信息请参考“Adobe FlexTM3语言参考”(http://livedocs.adobe.com/flex/3/langref/)。 1. type属性(String类型) 当用户进行鼠标操作时,Flash Player会自动调度鼠标事件MouseEvent。操作可能是鼠标点击、移动、滑动滚轮等。这些不同的操作都是通过事件类型来标记的。 type属性返回的字符串表明事件的类型。代表事件类型的字符串是大小写敏感的。Flex以常量的形式内置了许多事件类型,如Event.unloaded和MouseEvent.CLICK。 2. cancellable属性(String类型)和preventDefault()方法 默认情况下,许多事件都有 Flash Player 执行的关联行为。比如,如果用户在文本字段中键入一个字符,则默认行为就是在文本字段中显示该字符。如果需要取消TextEvent.TEXT_INPUT事件的默认行为,你可以使用preventDefault()方法来阻止显示所键入的字符。 不可取消行为的一个示例是与Event.REMOVED事件关联的默认行为,只要Flash Player从显示列表中删除显示对象,就会生成该事件。由于无法取消默认行为(删除元素),因此preventDefault()方法对此默认行为无效。 开发者可以使用Event.cancelable属性来检查是否可以阻止与特定事件关联的默认行为。如果属性Event.cancelable的值为true,则可以使用preventDefault()来取消默认行为,否则,preventDefault()无效。 6.3.2 flash.event.Event的子类 用户的操作或系统本身会触发各种各样的事件。对于特定事件,Event对象提供的通用属性和方法不能满足需要。以键盘事件为例,开发者须要通过事件对象获知用户敲下哪个键,或者Ctrl键是否处于活动状态。这些信息都须要特定的事件类来处理。Flex提供了大量内置的事件类,比如鼠标事件MouseEvent、键盘事件KeyboardEvent、网络活动相关的事件NetStatusEvent等。这些事件都是flash.event.Event的子类。 Flex内置的大部分事件类来自于两个ActionScript类包:flash.events和mx.events。 flash.events包内置了大量支持DOM事件模型的事件类,反映了Flash Player本身的活动,比如网络事件、摄像机、上下文菜单、鼠标事件等,因此这些事件类不仅仅局限于Flex应用。 mx.events包则包含了大量只同Flex组件相关的事件类,比如DataGridEvent、ScrollEvent等。 无论来自哪个ActionScript包,这些内置的事件类都源自flash.events.event基类,也因此获取到基类提供的通用公共属性及方法。同时这些事件类也提供了一系列特有的属性和方法,开发者可以据此更好的响应事件。 除了Flex内置的事件类,开发者经常须要继承flash.event.Event自定义事件,我们在本章稍后会详细介绍。 6.3.3 Flex应用启动的事件序列 Flex的事件可以分为系统事件和用户事件。 由用户操作触发的事件,就是用户事件。Flex的用户事件遵循了W3C DOM Level3(W3C文档对象模型Level3)事件规范。具体可参见http://www.w3.org/TR/DOM-Level-3-Events/。用户事件是应用中最常见的事件类型,比如鼠标点击、键盘输入等都属于用户事件。除了用户直接触发的事件,在Flex应用运行过程中,Flex架构本身也会自动触发一些事件,也就是所谓的系统事件。在信号灯应用中,creationComplete事件是系统事件,而MouseEvent.CLICK事件则是用户事件。 所有的Flex可视化组件都继承自基类mx.core.UIComponent,同时也继承了UIComponent的系统事件。当Flex应用启动时,会触发一系列事件,这些事件表明了组件在启动过程中的不同时间点,比如被创建、布局完成或者在屏幕上完成绘制等。这些启动序列事件帮助开发者更好的监控并控制应用运行生命周期的各个环节。实际上,这“一系列”事件都由一个事件类mx.events. FlexEvent表示。FlexEvent类的不同类型反映了启动过程中不同阶段发生的事件。 在Flex应用中,容器和组件启动时的事件序列不同。 在组件所属的容器内,组件被实例化、加入到父容器的显示列表中,然后设置外观尺寸,最终完成容器中的布局,并被绘制出来。图6-3显示了组件的启动事件序列: 图6-3 组件的启动序列 由于容器包含了子组件,因此容器启动时的序列不同于单一组件,图6-4显示了包含子组件的容器的启动过程。不仅容器本身要完成创建的全过程,所包含的子组件也要依次序地完成自身的创建。当然如果容器中嵌入了子容器,其启动过程与此类似。 图6-4 容器的启动序列 在所有组件和容器被创建并最终绘制在屏幕上之后,Application对象将触发applicationComplete事件,该事件类型标志着应用启动完毕,是应用启动时触发的最后一个事件。 图6-4中描述的部分启动序列事件解释如下: ・ preinitialize:在初始化之前触发,对于容器来说,所有子组件尚未定义。一般来说,不会在该事件触发时配置组件; ・ initialize:当组件或容器完成构造,并设置初始化属性后触发。在这个阶段,对于容器,其所有的子组件的preinitialize事件已经触发,但是这些子组件还没有完成布局; ・ creationComplete:当应用或组件(包括所有子组件)完成构造、布局和绘制可见时触发。当creationComplete事件发生时,组件已经被创建出来,因此开发者也能够访问同组件相关的属性,比如height、width等属性。开发者通常使用这个事件来对应用进行初始化配置。比如为某些应用组件添加侦听器,调用Web服务等; ・ applicationComplete:所有的组件初始化完成并显示。 我们对信号灯应用做一些小的修改,以此为例体会Flex应用启动过程中的事件序列,修改后的Flex项目名称为StartupSequence,改造的代码如代码6-9所示。 代码6-9:信号灯应用StartupSequence代码 在StartupSequence应用中,为每个组件都注册了针对preinitialize、initialize和creationComplete的事件侦听器showEventSequence(Event,String)。此外,我们为Application注册了针对applicationComplete的事件侦听器showEventSequence和showLog。showEventSequence把事件发生的序列信息记录在字符串out中,而在应用启动完毕后,我们利用侦听器showLog输出记录。 输出结果如图6-5所示,为了便于理解,已经过格式化编辑: 图6-5 启动序列事件的输出结果说明 6.4 EventDispatcher类 事件当然要发生在事件目标上,比如点击按钮时,事件目标就是按钮。在Flex应用中,任何显示列表上的对象都可能成为事件目标,换句话说,都能够触发事件。而这种特性全拜flash.events.EventDispatcher类所赐。 EventDispatcher类实现IEventDispatcher接口,并且是DisplayObject类的基类(DisplayObject 类是可放在显示列表中的所有对象类的基类)。EventDispatcher类允许显示列表上的任何对象都是一个事件目标,而同时,又使得这些对象能够侦听事件。 对于EventDispatcher类来说,“dispatcher(调度)”不是一个好名字,因为EventDispatcher类不仅仅能“调度”事件。EventDispatcher实现了复杂的DOM事件模型,提供了关键的事件方法,通常会使用到的方法包括: ・ addEventListener方法:为EventDispatcher对象注册事件侦听器; ・ dispatchEvent方法:触发事件,也可以称为“调度事件”; ・ hasEventListener方法:检查对象是否为特定事件注册了事件侦听器; ・ removeEventListener:从EventDispatcher对象中删除事件侦听器。 6.4.1 注册事件侦听器 开发者可以通过两种方式注册事件侦听器:使用addEventListener方法和通过MXML标签设置。 1. 使用addEventListener方法 addEventListener方法的签名如下: public function addEventListener (type:String, listener:Function, useCapture: Boolean = false, priority:int = 0, useWeakReference:Boolean = false):void 使用addEventListener方法,可以为EventDispatcher对象注册事件侦听器,以使侦听器能够接收事件通知。如我们之前所介绍的,可以为显示列表中任何节点对象注册事件侦听器,通过参数,可以为特定类型的事件注册侦听器,设置侦听器运行的事件阶段和优先级。 ・ type:String:必需的参数,说明了侦听器所侦听的事件类型。事件类型可以是简单的字符串,也可以使用特定的事件对象常量。 ・ listener:Function:必需的参数,也就是所谓的“侦听器”,负责在事件发生后响应事件。在Flex中,侦听器是一个接受Event对象作为唯一参数,并且不返回任何结果的函数,即: function(evt:Event):void 在这一点上,Flex没有完全遵循W3C DOM Level 3事件规范。在该规范中,侦听器对象属于一个类的实例,而这个类实现了一个特殊的接口,名为“侦听器接口”。而在Flex中,把这个类“简化”为一个函数。 ・ useCapture:Boolean:可选参数。说明侦听器是运行于捕获阶段、目标阶段还是冒泡阶段。useCapture设为true意味着侦听器只在捕获阶段处理事件。要在所有三个阶段都侦听事件,则须要分两次调用addEventListener:一次将useCapture设置为true,一次将useCapture设置为false,如我们在StartupSequence样例项目中所示。 ・ priority:int:可选参数。设置事件侦听器的优先级,数字越大,优先级越高,优先级为n的侦听器会在优先级为n1的侦听器之前得到处理。如果两个或更多个侦听器共享相同的优先级,那么按照侦听器添加的顺序进行处理。默认优先级为0。如果希望修改侦听器的优先级,必须通过removeEventListener删除原有的事件侦听器,然后重新注册带有新的优先级参数的侦听器。 须要特别说明的是,事件侦听器的优先级保证了事件侦听器运行的顺序,但是,Flash Player并不会等高优先级的侦听器运行完毕后才去调用低优先级的侦听器。也就是说,低优先级侦听器的运行不能依赖于高优先级侦听器的处理,比如不能在低优先级侦听器运行时调用高优先级侦听器的处理结果。 2. 通过MXML标签设置事件侦听器: 使用MXML标签,也可以利用对象的事件属性来设置事件侦听器,如下所示,eventName为对象的事件属性,而其值可以直接为处理该事件的ActionScript代码段,或者ActionScript函数。 <mx:TagName eventName="[ActionScript代码段或事件侦听器函数]" /> 例如,代码6-10中当用户点击按钮时,将会更新标签显示为“红色”。 代码6-10:使用MXML标签设置事件侦听器样例――内置ActionScript代码段 <mx:Button label=”控制按钮” click=”lblLight.text='红色'”/> <mx:Label id="lblLight" text=”关闭”/> 对于复杂的事件侦听器,当然无法通过在标签中内置代码段实现。此时我们可以设置事件属性值为ActionScript函数。 例如,上述代码可以修改为如下方式,如代码6-11所示。 代码6-11: 使用MXML标签设置事件侦听器样例――ActionScript函数 <mx:Script> <![CDATA[ private function myEventHandler():void{ lblLight.text="红色"; } ]]> </mx:Script> <mx:Button label="控制按钮" click="myEventHandler()"/> <mx:Label id="lblLight" /> MXML标签注册事件侦听器看起来要比通过addEventListener方法容易得多,但是也同时带来了许多限制。MXML标签的方式远不如addEventListener方式灵活,使实现业务逻辑的ActionScript代码与展示外观的MXML代码过于紧密地耦合在一起。此外,MXML标签的方式也不能像addEventListener那样改变侦听事件的属性,如设置侦听器优先级等。 6.4.2 触发事件:dispatchEvent 所谓触发事件,就是通知Flash Player把事件对象调度到从显示列表根节点开始的事件流中。 dispatchEvent方法的签名如下: public function dispatchEvent(event:Event):Boolean 在本章之前的代码样例中,你并不能看到dispatchEvent的身影。因为事件都由Flex自动触发了。然而,开发者有时须要“手工”触发事件,尤其在处理自定义事件时。我们会在本章余下章节看到如何使用dispatchEvent方法。 关于EventDispatcher更详细的信息,请参考“ActionScript 3.0语言参考”。我们在本章的6.5.1节“使用Action Script创建自定义事件”(见本页)将会再次看到EventDispatcher类。 6.4.3 使用内置事件的基本步骤 至此为止,我们讨论了Flex事件的基本概念:事件对象和事件流,初步了解了EventDispatcher类。回顾本章开始的信号灯应用可以看到,开发者要使用Flex内置事件,须要完成两个步骤: 1. 注册事件侦听器:如前文所述,可以通过addEventListener或MXML标签实现; 2. 实现侦听器函数:侦听器函数接收了事件对象,由此获知事件的相关信息,并响应事件。 6.5 5个步骤创建自定义事件 通过自定义事件和自定义组件,Flex开发者能够设计出松散耦合的应用架构。以信号灯应用为例,当用户按下按钮时,对于应用来说发生了“信号灯切换”事件。我们以信号灯应用为例,学习如何创建自定义事件,并体会由此带来的益处。 创建自定义事件可以分为5个步骤。 6.5.1 使用ActionScript创建自定义事件 自定义事件只是一个“特别”的ActionScript类。开发者创建自定义事件无外乎完成如下几个任务:继承flash.events.Event、定义事件属性、编写类构造器、重载clone方法。容我一一道来。 继承flash.events.Event 如同Flex内置的事件,比如MouseEvent,自定义事件继承于flash.events.Event类。但事实上,你可以扩展其他事件类来创建自己的自定义事件,比如扩展MouseEvent。使用ActionScript创建事件的代码如下: package events{ import flash.events.Event; public class CustomEventClass extends Event{ } } 定义属性 从某种程度看,事件就是系统传递的“消息”。Flash Player利用事件对象从事件目标向事件侦听器传递数据。flash.events.Event提供了“消息”所应有的基本信息,比如事件类型event.type等。然而,开发者通常希望“消息”能够携带更多的信息,提供更多的数据。在自定义事件中,通过定义属性,可以让事件携带更多的信息。 public var custEvProperty:String; 编写构造器 类当然离不开构造器。事件类的构造器要完成两项任务。 1. 调用Super() 通过Super()调用父类的构造器,以初始化从父类继承的属性等。通常情况下,子类构造器应该首先调用Super()方法。当然,如果没有为子类编写构造器(不添加构造器方法),编译器会自动添加一个构造器,并且也会调用Super()。但是,我们仍然推荐编写构造器,并且显式地调用Super()方法。 2. 设置属性 事件类型是最常用到的事件属性。Super()可以接受String类型的参数,由此设置父类事件中继承来的事件类型属性(event.type)。 除了事件类型外,事件的其他自定义属性可以作为构造器的参数传入,在构造器中完成初始化。更灵活的是,这些属性类型不仅仅局限于String、Number等基本类型,还可以是任何自定义类。这样,自定义事件就成为真正的“运输大队”,可以携带复杂的数据,成为更称职的“信使”。 通常的事件类构造器如下: public function CustomEventClass(type:String,custEvPropertyParameter:String){ super(type) this.custEvProperty=custEvPropertyParameter; } 重载clone方法 创建自定义事件的最后一步是重载父类的clone方法,返回新的Event对象。当触发事件时,即dispatchEvent(event)时,EventDispatcher会自动调用clone方法获取新的Event对象。 重载clone方法将复制自定义类的所有属性。如果你没有对自定义事件类中添加的所有属性进行赋值的话,那么当侦听器处理触发的自定义事件时,就不会获得正确的属性值。 override public function clone():Event { return new CustomEventClass(type,custEvProperty); } 6.5.2 使用元数据[Event]定义事件 上一节,我们创建了自定义事件,那么如何使用自定义事件呢?还记得我们说过可以使用MXML标签注册事件侦听器吗?在下面代码中,click是Button组件的事件,我们只须为该事件设置侦听器方法或ActionScript处理代码。 <mx:Button label=”控制按钮” click=”lblLight.text='红色'”/> 组件能够获知有哪些内置事件可用,但是,如何让组件“认识”我们自定义的事件呢? 通过[Event]元数据,开发者可以为组件定义事件。编译器能够把这些自定义的事件识别为MXML标签属性。使用[Event]既可以为ActionScript组件定义事件,也可以为MXML组件定义事件,如图6-6所示。 [Event]元数据的签名如下: Event(name="eventName",type="package.eventType")] name说明了事件的名称,而type表明了该名称对应的事件类型。事件侦听器使用name进行注册。 为ActionScript组件定义事件 [Event]元数据必须置于包(package)定义之内,类(class)定义之上。如下: package events{ [Event(name="sampleEvent", type="myEvents.SampleEvent")] public class MyComponent extends UIComponent{ ... } } 为MXML组件定义事件 在下面的例子中,我们为自定义组件LightConsole(组件为LightConsole.mxml,位于views目录下)定义了事件switchLightEvent,并在用户点击按钮时触发该事件。在CustomTraficLight.mxml中使用该组件时,编译器会把switchLightEvent识别为LightConsole的事件属性。在本章最后一个代码范例CustomTraficLight中,我们可以看到更全面的介绍。 图6-6 元数据[Event]定义事件 6.5.3 触发事件 我们使用dispatchEvent()方法触发事件。就像已经谈到的,由于我们的自定义组件继承自(或间接继承自)EventDispatcher类,因此获得了触发事件的能力。dispatchEvent接受Event事件对象作为参数,也意味着在触发事件前,我们要创建事件对象的实例,而dispatchEvent方法会把该事件对象调度到事件流中。 public dispatchEvent(event:Event):Boolean 回顾上一节的图6-6,可以看到,在LightConsole.mxml中我们通过下面的代码创建了SwitchLightEvent事件对象的实例,然后通过dispatchEvent方法触发了该事件。 var evtObj:SwitchLightEvent = new SwitchLightEvent("switchLightEvent“,varSelectedLight); dispatchEvent(evtObj); 6.5.4 创建事件侦听器 通过dispatchEvent方法触发事件后,Flash Player会在事件流中检查显示列表中的每个节点对象是否注册了事件侦听器。事件侦听器最终将完成针对事件的响应处理。依然回到图6-6,其中,我们通过创建事件侦听器方法switchEventHandler(),并通过赋予事件属性switchLightEvent值的方式注册了这个侦听器。事件侦听器方法switchEventHandler接受event事件对象为参数。 6.6 自定义事件代码样例 新的信号灯应用将帮助我们更好地理解如何创建自定义事件,以及自定义事件带来的好处。 新的信号灯应用实现出的效果同本章第一节例子一样,但在方法上完全不同。在旧的TrafficLight项目中,我们只有一个MXML应用文件,所有的代码全都集中在该文件中。幸运的是,信号灯应用非常简单,一个文件实现一个项目的模式还不至于使开发者陷入疯狂。然而,你能够想象,当应用复杂的时候,一个项目一个文件当然行不通。新的信号灯应用将遵循OOP的理念,你能够看到我们如何把一个简单的应用拆分成不同的对象组件,然后通过事件把它们联系起来。 按照OOP的理念,信号灯应用包含了两个对象:控制板和信号灯,以及一个事件:切换信号灯事件。 新的信号灯应用项目为CustomTraficLight。主应用为CustomTraficLight.mxml。我们将创建两个自定义组件Lights.mxml(代表信号灯对象)和LightConsole.mxml(代表控制板对象),还将创建一个自定义事件SwitchLightEvent.as。请参考目录结构图6-7。 图6-7 CustomTraficLight的源码目录结构 图6-8解释了信号灯应用中,用户操作触发事件、系统创建事件对象实例、侦听事件并响应的过程。不同组件在其中扮演了不同的角色。 图6-8 CustomTraficLight中自定义事件的创建、触发和侦听 1. 用户按下按钮触发事件。 2. 系统创建SwitchLightEvent事件的实例。事件对象携带了从事件目标取得的事件上下文信息。 3. 主应用处理SwitchLightEvent事件,通过访问事件对象获取所需信息。 4. 主应用通过事件对象把数据传递给Lights.mxml,更新信号灯。 6.6.1 创建工程和自定义UI组件 1. 创建新的Flex Web项目CustomTraficLight。在源代码目录下,新增两个目录,分别为views和events。 2. 在项目导航面板中,选中views目录,通过菜单File→New→MXML Component创建两个组件:Lights.mxml和LightConsole.mxml。Lights.mxml的基类为Canvas,而LightConsole的基类为HBox。如图6-9所示。 图6-9 创建CustomTraficLight的自定义组件 3. CustomTraficLight中创建自定义组件。 在LightConsole中分别加入三个按钮btnGreen、btnRed和btnBlue,完成后如代码6-12所示。 代码6-12: LightConsole组件的布局 <?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="300" height="30" horizontalAlign="center" fontSize="16"> <mx:Button label="绿色" id="btnGreen"/> <mx:Button label="红色" id="btnRed"/> <mx:Button label="蓝色" id="btnBlue"/> </mx:HBox> 在Lights中加入Label,设定id为lblLightInfo。定义String类型属性currentLight,同时绑定lblLightInfo的text属性。完成后如代码6-13所示: 代码6-13:Lights组件的布局 <?xml version="1.0" encoding="utf-8"?> <mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="300" height="30" horizontalCenter="center" creationComplete="init()"> <mx:Script> <![CDATA[ [Bindable] public var currentLight:String; private function init():void{ currentLight="关闭"; } ]]> </mx:Script> <mx:Label id="lblLightInfo" text="{currentLight}" horizontalCenter="0" textAlign="center" fontSize="16"/> </mx:Canvas> 4. 在主应用CustomTraficLight.mxml中加入LightConsole和Lights组件,完成后如代码6-14所示: 代码6-14:CustomTraficLight主应用的布局 <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" horizontalAlign="center" xmlns:comp="views.*"> <comp:Lights/> <comp:LightConsole/> </mx:Application> 接下来,我们开始通过4个步骤创建并使用自定义事件,使CustomTraficLight项目的主应用CustomTraficLight和组件LightConsole、Lights实现协作和数据交互。 6.6.2 步骤一:创建自定义事件类 1. 选中events目录,通过菜单File→New→ActionScript Class,在events目录下新建ActionScript类SwitchLightEvent。以Event作为其基类。 2. 定义新的String类型私有属性selectedLight。该属性表示被选中的信号灯颜色。此外,为该属性添加返回String类型值的get方法getSelectedLight()。 3. 修改构造器方法:新的构造器方法接受type和varLight两个String类型参数,调用Super(type)方法,并且用varLight初始化selectedLight属性。 4. 重载clone方法,返回新的SwitchLightEvent事件对象。 完成后,SwitchLightEvent.as代码全貌如代码6-15所示: 代码6-15:自定义事件类SwitchLightEvent.as package events { import flash.events.Event; public class SwitchLightEvent extends Event { private var selectedLight:String; public function SwitchLightEvent(varType:String, varLight:String){ super(varType); this.selectedLight=varLight; } override public function clone():Event{ return new SwitchLightEvent(type,selectedLight); } public function getSelectedLight():String{ return selectedLight; } } } 6.6.3 步骤二:利用[Event]定义事件 由于“切换信号灯”事件是在用户按下对应按钮后发生,也就是说由自定义组件LightConsole(信号灯控制台)触发,因此,我们将为这个事件定义为SwitchLightEvent事件。 在LightConsole.mxml中添加[Event]元数据定义:name=“switchLightEvent”,type=“events.SwitchLightEvent”。 6.6.4 步骤三:触发事件 在用户按下LightConsole的按钮后将触发SwitchLightEvent事件,因此我们为LightConsole的三个按钮的click事件属性添加事件侦听器:clickEventHandler(varSelectedLight:String):void。clickEventHandler一方面在创建SwitchLightEvent事件对象时,通过varSelectedLight参数初始化了其自定义的参数(说明选中信号灯的颜色),另一方面调用dispatchEvent触发了SwitchLightEvent事件。 完成后的LightConsole.mxml代码如代码6-16所示: 代码6-16:自定义组件LightConsole.mxml <?xml version="1.0" encoding="utf-8"?> <mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml" width="300" height="30" horizontalAlign="center" fontSize="16"> <mx:Metadata> [Event(name="switchLightEvent", type="events.SwitchLightEvent")] </mx:Metadata> <mx:Script> <![CDATA[ import events.SwitchLightEvent; private function clickEventHandler(varSelectedLight:String):void{ var evtObj:SwitchLightEvent = new SwitchLightEvent("switchLightEvent", varSelectedLight); dispatchEvent(evtObj); } ]]> </mx:Script> <mx:Button label="绿色" id="btnGreen" click="clickEventHandler('绿色')"/> <mx:Button label="红色" id="btnRed" click="clickEventHandler('红色')"/> <mx:Button label="蓝色" id="btnBlue" click="clickEventHandler('蓝色')"/> </mx:HBox> 6.6.5 步骤四:注册SwitchLightEvent的事件侦听器 我们须要注册针对SwitchLightEvent的事件侦听器,该侦听器也将完成事件处理:即更新信号灯显示lblLightInfo。由于我们已经为LightConsole.mxml定义了事件switchLightEvent,因此在主应用CustomTraficLight中,我们就能够为LightConsole的实例设定该事件属性,如代码6-17所示。 1. 我们先设定LightConsole实例的事件属性switchLightEvent值为函数switchEventHandler(event); 2. 接下来,我们定义switchEventHandler(event)方法,我们将从Event对象的getSelectedLight获取“被选中的信号灯颜色”值,并将它赋予selectedLight变量,而该变量则绑定在Lights属性实例的currentLight属性上。在Lights.mxml中,该属性被绑定在lblLightInfo的text属性上。至此,我们最终更新了标签的显示。 代码6-17:主应用CustomTraficLight.mxml <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" horizontalAlign="center" xmlns:comp="views.*"> <mx:Script> <![CDATA[ import events.SwitchLightEvent; [Bindable] private var selectedLight:String; private function switchEventHandler(event:SwitchLightEvent):void { selectedLight = String(event.getSelectedLight()); } ]]> </mx:Script> <comp:Lights currentLight="{selectedLight}"/> <comp:LightConsole switchLightEvent="switchEventHandler(event)" /> </mx:Application> 6.6.6 分析 同最初的TrafficLight项目相比,CustomTraficLight无疑前进了一大步。CustomTraficLight.mxml利用自定义事件和自定义组件将应用按对象化进行分割,信号灯对象(Lights.mxml)和信号灯控制台对象(LightConsole.mxml)通过事件对象来传递“选中信号灯颜色”(selectedLight)。在这个例子中,你或多或少可以体会Flex开发的精髓:基于组件的事件驱动开发。按照OOP方法,把应用拆分成不同的对象,实现或展示为不同的组件,并使用事件在其中传递信息。 CustomTraficLight.mxml仍然有缺陷。主应用CustomTraficLight.mxml被赋予了太多的逻辑处理职责,因此信号灯(Light.mxml)不得不暴露过多的信息(currentLight)。而且,在信号灯控制台(LightConsole.mxml)触发了SwitchLightEvent事件后,我们没有直接“通知”信号灯(Lights.mxml)更新显示,这也让应用看上去有点“怪异”。 但总体来讲,新的CustomTraficLight已经“看上去很美”了。 在第14章“表单、数据校验和格式化”(见第297页),我们采用改进的方式实现的bookManage样例,可以帮助你更好地理解事件在Flex开发中发挥的重要作用。
Adobe Flex 大师之路——第6章 事件驱动编程
书名: Adobe Flex 大师之路
作者: 董龙飞 | 肖娜
出版社: 电子工业出版社
出版年: 2009年
页数: 558
定价: 69.80元
装帧: 平装
ISBN: 9787121085918