本章简介
前几章介绍了Flex应用开发的主要内容,本章将介绍Flex应用性能优化相关的知识,比如如何减少SWF文件的大小和内存泄漏问题以及改善代码性能的技巧等。很多时候,影响应用性能的主要因素是设计。不好的设计是导致应用性能低下的主要原因,而针对不同特点的应用,采用何种设计方法往往与设计者本身的经验和素质相关。在排除了设计的因素之后在Flex应用开发中还有很多具体细节和技巧可以提高Flex应用的性能,本章将介绍RSL技术以减小SWF文件的体积,和Flex垃圾回收原理,以及预防内存泄露的一些基本技巧。此外还介绍了Flex应用中进行打印机打印的常见方法。
核心技能部分
Flex应用的性能优化除了设计的因素外,主要集中在两个方面
Ø 如何解决SWF文件过大的问题。
Ø 如何解决Flex内存泄漏的问题
本章会从这两个方面着手 讲解如何使用RSL技术降低SWF文件的体积以及Flex内存泄漏的原因、如何避免Flex应用内存泄漏、如何确定是否有内存泄漏、几个Flash提供的能移检查内存使用情况和Flash Player自身相关信息的系统类,以及其他提高性能的技巧。
在Flex1.0的时候,MXML文件和它的资源文件全部编译到一个SWF文件中,导致SWF文件非常大。SWF文件中包含了基础的application模型(MODEL)组件(如Button ,CheckBox
和Panel等组件)、图片资源、嵌人数据和自定义组件。
大SWF文件导致的直接后果就是下载时间较长(虽然在没有修改的情况下只下载一次)。在多数情况下,一个Flex客户端包含多个应用,这些应用包含了很多相同的资源。但是由于每个应用都被编译成SWF文件,相同的资源被编译进不同的SWF,在下载不同应用的同时也下载了重复的资源。
Flex1.5出现了运行期共享库(Runtime Shared Libraries,RSL)的概念,通过RSL将共享资源提取成独立的文件,可以有效地减小应用SWF文件的大小。这些RSL文件可以分开下载并且在客户端缓存,它们能够被大量地应用在运行期共享使用,但是只需要传输到客户端一次
如果客户端有多个应用,并且这些应用共享一个图片文件、组件和其他资源的核心资源包,用户只需要下载一次包含这些资源的RSL,这明显减小了主应用文件的尺寸。如果某个RSL内的资源发生了变化,Flex可以重编译这个RSL,由客户端重新单独下载,这个过程不需要重编译那些引用资源的应用和其他的资源RSL
理解Flex的链接形式有利于我们理解RSL,如何工作。Flex的编译器支持两种连接方式,静态链接和动态链接.所谓静态链接就是将应用所需要的代码以及这些代码所依赖的代码都编译进应用的SWF文件,这样获得的SWF就会非常大,下载很慢,但运行很快,因为所有的代码都己经在swF里了。而动态链接是指Flex应用中引用的一些类放到了应用SWF文件之外的其他文件中,并且在运行器加载,使用RSL就是动态链接的一种形式。
下面我们首先通过配置编译环境,来看看使用系统RSL的效果。
首先我们新建一个空白的Application,文件名为Blank.mxml。
示例9.1
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600">
打开工程属性窗口,查看编译选项,其中的Library path有一个Framework linkage选项,如
图9.1.1所示。
图9.1.1 Application的链接属性设置
默认的选项为Merged into code(合并到代码中 即静态链接),将系统Frame框架库文件内容编译进应用文件代码中。保存设置,系统完成文件编译,查看得知blank.swf文件的大小为362KB。注意:这是一个空白的应用,仅有一个Application组件。造成文件较大的根本原因是文件中很大一部分是Flex基础代码,这部分代码几乎每个Application都会加载使用,如果它们不能够被共享使用,那么空间的浪费是相当巨大的。
下面我们把Framework linkage的选择设置为另外一个选项。Runtime Shared Libraries(RSL)重新构建整个工程,再查看得知blank.swf坟件的大小为142KB ,约减少到原来的1/3,这说明,使用RSL能够有效减少Flex应用SWF文件的大小,当然,减少的部分不是消失了,而是以RSL的形式在运行期动态加载了。
使用Framework RSL使得应用的大小有了明显减少,但是还剩下的一百多KB是些什么东西
呢?让我们再次打开工程属性窗口,查看编译选项。仔细查看Library Path页,如图9.1.2所示。
图9.1.2 工程引用库文件列表
除了Framework之外,工程中还引用了其他的库文件。对于一个空白的应用文件来说rpc.swc文件也可以设为运行期加载。展开rpc.swc属性,选择Link Type项,点击窗口右侧的Edit按钮,弹出库文件修改窗口如图9.1.3所示。
图9.1.3 选择库文件是否采用摘要
选择Digests(摘要 摘要将在下一小节进行详细介绍)选项,点击【添加】按钮,在【部署路径】会增加一条记录,名字默认为rpc_3.0.0.477.swz,这个文件将被生成到发布目录,供客户端下载。文件名中的3.0.0.477是当前编辑环境所使用的RPC库的版本号。点击【OK】后系统将重新构建工程,编译后查看blank.swf文件的大小减到了80KB,这基本是在Flex环境下一个空白应用文件能够达到的最小尺寸了。
对于使用chart等组件通过图形方式展示数据的应用,将datavisualization库设置为动态加载后,SWF文件会明显减小。
我们知道了可以通过修改库文件的编译方式来实现RSL的使用。在配置中,我们使用了Digests(摘要)选项,它到底是做什么的呢?下面将详细介绍Digests(摘要)。
Flash Player缓存是Flash Player 9.0.115.0版本提供的新功能,允许有Adobe签名的文件由Flash Player进行缓存,这些文件以swz为后缀。
与浏览器缓存相比,Flash Player缓存有几个好处。首先,由于缓存的swz文件是Adobe签
名的,因此可以在多个域中共享,而不需要考虑这个文件是从哪个域下载的。或者说,只要文件的版本相同,经过前面的库文件可以为整个客户端的所有应用共享使用。
其次,由于,swz文件存储在Flash Player的缓存中,不会因为浏览器清理缓存内容被清理掉。Flash player会自行管理旧的swz文件,当缓存容量达到限制时,不使用的swz文件会被回收清理。
另外,签名的swz文件可以跨浏览器使用,这意味若如果在IE中已经下载了某个swz文件,
那么在Firefox中也可以共享该文件,而不需要另外下载一份。
如何使用具有摘要的RSL呢?在Flex的编译环境中可以很方便地配置它。我们在Library path属性页中,从SDK库中找到framework.swc,如图9.1.4所示。
图图9.1.4 库文件配置
选择Link Type项,点击【Edit】编辑按钮,进入属性编辑界面,如图9.1.5所示。
图9.1.5 库文件的发布形式
在属性编辑界面中,选择验证的方式为Digests(摘要),可以看到Deployment pahts(发布路径)有两个文件。一个是framework_3.0.0.477.swz,这个文件就是当前编译环境所使用的Framework的swz文件,它将会被放到发布路径中供客户端下载;而第二个文件是在swz文件下载出现异常(如Digests验证失败等)的情况下作为swz的替代文件进行下载,从而保证客户端能够正常运行。
属性设置中的policy File可以指向cross-domain.xml文件,不指定表示该文件可以任意下载。
Flex3是从Flash Player缓存获得好处的第一个版本,前面我们已经讲到了如何在Flex3中设
置编译环境,从而使用Framework RSL。但是,我们注意到,在编译时包含的库文件都是SWC
格式的,这是为什么呢?
在编译期需要SWC格式的库文件有两个原因:第一个就是要从SWC中读取库文件的摘要,这个摘要将被缓冲的framework进行校验,Flex3中使用的摘要是使用SHA-256加密算法创建的。
一个RSL的摘要是从SWC文件中提取出来并且编译到应用中,当运行时应用从网络或者本
地读取RSL时,会校验RSL的摘要与应用中包含的摘要是否一致,如果两个摘要不匹配,系统将显示一个错误,并放弃RSL的加载。
开发者可以使用未签名的RSL,这个RSL以普通的SWF形式缓存在浏览器中,会因为浏览
器缓存的清理而被清空,并且由于没有认证,也不能实现跨域共享。
内存问题一直是程序员比较关心的问题之一,每个程序员都希望自己开发的程序足够健壮,在运行过程中不会因内存泄漏而导致程序运行变慢甚至崩溃。
现在,面向对象语言(比如Java)增强了内存管理机制,能够自动回收不被使用的内存,或者说能够回收垃圾内存,这种内存管理机制通常被称为“garbage collection(垃圾回收)“,简称GC。
Flex开发中所使用的ActionScript语言(简称AS)也是一种支持GC的语言。经过编译后的AS代码运行在AS虚拟机(简称AVM)中,由AVM自动完成垃圾内存回收的工作。Flash Player就是一个AVM,所以有时候大家会将二者混为一谈。
既然AVM能够自动完成垃圾回收的功能,那么是不是Flex程序员就可以认为自己所开发的Flex应用不存在内存泄漏问题呢?答案是否定的。在某些情况下,处理不妥当的代码仍然会导致内存泄漏.如何才能避免内存泄漏?应该说,只有在 AS程序员清楚地了解了Flash Player的垃圾回收的基本原理,并且高度重视内存泄漏这个问题后,才能有效避免内存泄漏情况的发生。
Flash Player垃圾回收工作是由垃圾回收器(garbage collection)完成的。垃圾回收器是运行在后台的一个进程,它释放那些不再被应用所使用的对象所占用的内存。不再被应用所使用的对象是指那些不会再被活动着(正在工作的)的对象所引用的对象。在AS中,对于非基本类型(Boolean;String, Number,Unit,Int)的对象,在对象之间传递的都是对象引用,而不是对象本身。删除一个变量只是删除对象的引用,而不是删除对象本身。一个对象可以被多处引用,通过这些不同的引用所操作的都是同一个对象。
通过示例9.2和示例9.3我们可以了解基本类型和非基本类型对象的差异。
示例9.2 基本类型的值传递
private function testPrimitiverTypes():void
{
var s1:String="abcd";
var s2:String = s1;
s2+="efg";
trace("s1:",s1);
trace("s2:",s2);
var n1:Number=100;
var n2:Number=n1;
n2=n2+100;
trace("n1:",n1);
trace("n1:",n1);
}
示例9.3 非基本类型对象的引用传递
private function testNonPrimitiverTypes():void
{
var a:Object={foo:'bar'};
var b:Object=a;
delete(a);
trace(b.foo);
}
对于非基本类型对象,AS采用两种方法来判定一个对象是否还有活动的引用,从而决定是否可以将其回收。一种方法是引用计数法,另一种方法是标记清除法。
引用计数法是判定对象是否有活动引用的最简单方法,并且从AS1.0就开始在Flash中使用。
当创建一个对对象的引用后,对象的引用计数就加一,当删除一个引用时,对象的引用计数就减一。如果对象的引用计数为0,那么它被标记为可被GC删除,如示例9.4所示。
示例9.4 对象的引用计数示例
var a:Object={foo:'bar'};
//现在对象的引用计数为1(a)
var b:Object=a;
//现在对象的引用计数为2(a和b)
delete(a);
//对象的引用计数又回到了1(b)
delete(b);
//对象的引用计数变成0,现在这个对象可以被GC释放
引用计数法很简单,并且不会增加CPU的开销,可惜的是,当出现对象之间的循环引用时,它就不起作用了。所谓循环引用就是指对象之间直接或间接地彼此引用,尽管应用已经不使用这些对象,但是它们的引用计数仍然大于0,因此,这些对象就不会从内存中移除,如示例9.5所示。
示例9.5 对象的循环引用
//创建第一个对象
var a:Object={};
//创建第二个对象来引用第一个对象
var b:Object={foo:a};
//使第一个对象也引用第二个对象
a.foo= b;
//删除两个活动引用
delete(a);
delete(b):
在上面的例子中,两个活动引用都已被删除,因此在应用程序中再也无法访问这两个对象。
但是它们的引用计数都是1,因为它们彼此相互引用。这种对象间的相互引用可能会更加复杂(a引用b,b引用a,C又引用a,诸如此类),并且难以在代码中通过删除引用来使得引用计数变为0。Flash Player6和Flash Player7中就因为XML对象中的循环引用而痛苦,每个XML节点既引用了该节点的子节点,又引用了该节点的父节点,因此这些XML对象永远不会释放内存。好在Flash Player8之后增加了一种新的GC技术—标记清除。
AS3使用的第二种查找不活动对象的GC策略就是标记清除。Flash Player从应用的根(root )节点开始,遍历所有其上的引用,标记它所发现的每个对象,然后迭代遍历每个被标记的对象,标记它们的子对象。这个过程递归进行,直到Flash Player遍历了应用的整个对象树,并标记了它所发现的每个对象,在这个过程结束的时候,可以认为内存中那些没有被打标记的对象没有任何活动引用,因此可以被安全地释放,通过图9.1.6可以很直观地了解这种机制。
在图9.1.6中,深色(有底纹)的引用在标记清除过程中被遍历,深色对象被打上了标记,白色(无底纹)对象将被释放内存。
图9.1.6 Flex内存对象树
标记清除机制非常准确。但是这种方法需要遍历整个对象结构,从而增加CPU占用率。因此Flash Player9为了减少这种开销,只是在需要的时候偶尔执行标记清除活动。
Flash Player在运行时请求内存的速度受限于浏览器。因此,Flash Player采取小量请求大块内存,而不是大量请求小块内存的内存请求策略。同样,Flasb Player在运行时释放内存的速度也相对较慢,所以Flasb Player会减少释放内存的次数, 只有在必要的时候才释放内存。也就是说,Flasb Player的垃圾回收只有在必要的时候才会执行。
在Flasb Player9和之后的版本中,Flasb Player垃圾回收的时机是在Flasb Player需要请求新的内存之前。这样,Flasb Player可以重新利用垃圾对象所占用的内存资源,并且可以重新评估需要另外请求的内存数量,这也会节省时间。
程序的实际运行中并不是每次应用申请内存时都会导致垃圾回收的执行,只有当Flash占用的内存达到一定程度时,才会执行真止的垃圾回收。如果应用中内存开销增长是匀速的,那么计算机物理内存越大,则垃圾回收触发周期越长。如果计算机有2G的物理内存,直到打开Flash应用的浏览器占用700M物理内存之后才会导致Flash Player回收垃圾内存。
来自Adobe公司的Alex Harui总结了两点:
Ø 何时真正执行垃圾回收不可预知。
Ø 垃圾回收总是在请求内存时触发,而不是在对象删除时发生。
最后,有关Flash Player中垃圾回收的一件非常重要的事情就是:垃圾回收不是立即进行的,而是延迟的。当没有任何对象引用某个对象后,该对象也不会立即从内存中清除,相反地,它们会在将来某个不确定的时候被移出(从开发者的角度来看)。但这些对象在没有被垃圾回收之前,它们仍然在工作(占用内存和CPU }。尽管这不是我们所希望的。
虽然我们不能让Flash Player立即回收内存,但在程序确定不再引用一个正在工作的对象之前,应终止其工作。比如,停止已经启动的Timer ,停止正在播放的视频或声音,以防止其继续占用CPU。
很多程序员都想能够在程序中指定计算机进行垃圾回收。目前,Adobe官方没有公布能够强制执行垃圾回收操作的相关API。不过,可以利用Flash Player的bug来实现强制回收内存,主要是通过人为抛出某种特别的异常,从而让Flash Player回收内存,如示例9.6所示。
示例9.6 强制回收内存的代码
try{
var lc1:LocalConnection = new LocalConnection();
var lc2: LocalConnection = new LocalConnection();
lc1.connect(‘gcConnection’);
lc2.connect(‘gcConnection’);
}
catch(e:Error){
}
这种强制回收内存的方法并不是根据官方API而是利用系统的某些漏洞。因此,在开发
应用时,不要依赖于这种方法来回收内存,只能将其视为辅助方法。
通过上面的讨论我们可以知道,只要对象被其他活动对象(仍在运行的)所引用,那么这个对象就不会被垃圾回收,从而可能造成内存泄漏。
在开发中,如下的一些情形会导致内存泄漏:
(1) 不再使用被全局对象所引用的对象时,开发者忘记从全局对象上清除对它们的引用,这
时就会产生内存泄漏。常见的全局对象有Stage,主Application ,类的静态成员变量以及采用Singleton模式创建的实例等。如果使用第三方框架,比如PureMVC,Cairongorm等,要注意这些框架的实现原理,尤其要注意框架里面采用Singleton模式创建的Controler和Model。
无限次触发的Timer会导致内存泄漏。不管无限次触发的Timer是否为全局对象,它本身
以及注册在Timer中的监听器对象都不会被垃圾回收,如示例9.7所示。
示例9.7 含有无限次触发Timer的内存泄漏测试组件
xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" width="400" height="300" title="测试组件" creationComplete="panel1_creationCompleteHandler(event)" >
import mx.events.FlexEvent; [bindable] private var timeer:Timer = new Timer(1000); private var memoryBlocks:Array = new Array(); protected function panel1_creationCompleteHandler(event:FlexEvent):void { var mBlock:Array = this.allocateMemory(); memoryBlocks.push(mBlock); this.timeer.addEventListener(TimerEvent.TIMER,onTime); this.timeer.start(); } protected function onTime(event:TimerEvent):void { trace(this.toString()); } protected function allocateMemory():Array { var memoryBlock:Array = new Array(25600000); for(var i:uint=1;i<=25600000;i++){ memoryBlock[i-1]=i; } trace('allcate 100M memory!'); return memoryBlock; } ]]>
上面的代码自定义了一个测试内存泄漏的Canvas组件,这个组件在初始化时开辟了100M内存(为了方便查看内存的回收情况),同时创建了一个每隔1秒钟无限次数触发的Timer,并且启动了这个Timer 。
针对上面的组件,下面我们给出一个测试应用,其界面如图9.1.7所示。
图9.1.7 Flex验证内存泄漏组件的应用程序界面
该测试应用上有三个按钮,分别是“强制回收内存’、“创建内存消耗组件”和“移出内存消耗组件”。点击“创建内存消耗组件”按钮就会执行创建一个用于内存泄漏测试的Canvs对象,并将其作为container的子对象显示到界面上,点击“移出内存消耗组件”按钮则会将“创建内存消耗组件”按钮所创建的Canvs对象从containe的子对象列表中删除,并且永远不再使用。
应用运行后,先点击“创建内存消耗组件”按钮,然后再点击“移出内存消耗组件”按钮,重复这样的操作,我们发现,由于Canvs对象上的无限次触发的Timer对象已经启动,导致Canvs对象所占用的内存无法被回收,内存会一直增加,最终导致浏览器崩溃。如果我们将Canvs初始化代码中启动Timer的语句注释掉,重复上述的测试操作,内存会在某个时候减少,这说明占用内存的Canvs对象己经被垃圾回收。
通过这个简单的侧试程序测试了Timer的情况,当然,将其稍加改造也可以用来测试其他情况,在本教材中所列举的内存泄漏的情况都是经过测试程序得到的结论。
上述程序代码如示例9.8所示。
示例9.8 验证内存泄润组件的应用程序
xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600">
private var memoryTester:TimerTest=null; //添加测试组件 private function addTest():void{ memoryTester = new TimerTest(); this.container.addChild(memoryTester); } //删除测试组件 private function removeTest():void{ this.container.removeChild(this.memoryTester); this.memoryTester=null; } //回收内存 private function gc():void{ try{ var lc1:LocalConnection = new LocalConnection(); var lc2:LocalConnection = new LocalConnection(); lc1.connect('gcConnection'); lc2.connect('gcConnection'); } catch(e :Error){ } } ]]>
(2)通过隐式方式建立的对象之间的引用关系更容易被程序员所忽略,从而导致内存泄漏。最常见的以隐式方式建立对象之间的引用就是“绑定”和“为对象添加事件监听器”。通过测试我们发现,“绑定”不会造成内存泄漏,对象可以放心地绑定全局对象,而调用addEventListener()方法“为对象添加事件监听器”则可能产生内存泄漏,大多数内存泄漏都因此而发生。
下面的代码:
a.add.EventListener(Event.EVENT_TYPE,b.listenerFunction);
使得a对象引用了b对象,如果a对象是一个全局对象(全局对象在应用运行期间始终存在),则b对象永远不会被垃圾回收,可能会造成内存泄漏。比如下面的代码就有造成内存泄漏的可能:
this.stage.addEventListener(Event.RESIZE,onResize);
上面代码中的stage是UIComponent的stage属性,表示当前Flex应用运行的“舞台“。不过,通过以下三种方式使用addEventListener方法不会造成内存泄漏。
Ø 用弱引用方式注册监听器。就是调用时将addEventListener的第5个参数设置为true,例如:
SomeObject.addEventlistener(MouserClick.CLICK,otherObject.handlerFunction,false,0,true);
Ø 自引用的方式。即为对象添加的监听处理函数是对象本身的方法。例如:
this.addEventListener(MouseClick,CLICK,this.handlerFunction);
Ø 子对象引用。即为子对象添加的监听处理函数是父对象的方法。例如:
private var childObject:UIComponent=new UIComponent;
addChild(childObject);
childObject.addEventListener(MouseEvent.CLICK,this.clickHandler);
Flash Builder带有一个“剖析(Profiler)”工具,可以用来帮助我们识别内存泄漏。在Flash Builder中选择要被“剖析(Profiler)”的应用后,点击鼠标右键,在右键菜单中选择“Profile As”就可以运行“剖析(Profiler)“工具来分析所选择的应用,如图9.1.8所示。
图9.1.8打开Flex内存泄漏分析工具的菜单
“剖析(Profiler)”工具运行后的界面如图9.1.9所示。
图9.1.9 Flex内存泄漏分析工具界面
在这个工具中,有一个“Force Garbage Collection(强制垃圾回收)”按钮,当应用被剖析时或者以剖析方式运行时,点击这个按钮然后观察“活动对象”列表,可以帮助我们分析内存泄漏。如果确定已经完全移除和清除对对象的引用,那么“活动对象”列表中的“类’就会减少。
通过“累计实例数”栏可以看到有多少个对象曾被创建,而通过“实例数”栏可以看到当前存在的对象实例有多少。如果在创建和移除对象之后运行“Force GC”,“果计实例数”的数量和“实例数”的数量相同,则可能存在内存泄漏。
“内存使用情况”图提供了另一种确定内存泄漏的方法,但只适合小应用。灰线代表最大的内存使用,而黑线则代表当前的内存使用。如果黑线和灰线从不分离,则说明有内存泄漏,
Flash中提供了一些系统级的类,可以帮助开发者获取Flash Player的一些信息,并且可以提供一定的控制能力,这就是flash.system包。
该包中的flash.system.System类提供控制内存剪切板(Clipboard ) ,摄像头、麦克风、共享对象的控制,通过该类的totalMemory属性则可以查看当前Flash Player所占用的内存。
该包中的flash.system.Capabilities类则提供了很多属性用于说明Flash Player的版本、操作系统版本和所具备的能力,比如hasPrinting可以告诉开发者当前系统是否支持打印功能。
在本章并不准备详细介绍这些API,学员可以在应用系统里面可以开发一个应用界面,用来显示这些系统的信息,通过特定的快捷键(比如Alt+Ctrl+M组合键)来调出这个应用界面,为维护者提供一些有用的参考信息。
知道Flash内存管理机制和内存泄漏的原因之后还要注意以下两类问题。
(1) Flex SDK本身的Bug导致的内存泄漏。这通常由于程序员在开发程序的时候会有各种疏忽或者代码不够严谨导致,Adobe的程序员也不例外,因此,Flex SDK中有些组件或类也存在一些内存泄漏问题。
常见的如下:
Ø 如果对组件应用了效果(Effect),则删除该组件时,需要把应用在该组件及其子组件上的效果停止,然后再把效果的targt属性设置为null,如果不停止效果而直接将其target属性设置为null将不能正常移除对象。
Ø 在Flex SDK 3.2中,从ListBase下派生出来的List和Gri d组件都存在内存泄漏。因为Adobe程序员在ListBase组件的mouseDownHandler()方法中以“强引用”的方式向SystemManager添加了一个Mouse_up事件监听器,代码如下:
SystemManager.getSandboxRoot().addEventlistener(MouseEvent.MOUSE_UP,mouseUPHandler,true,0,true);
Adobe公司在Flex SDK 3.3中已解决了这个问题,因此读者发现内存泄漏后应该检查一下自己的Flex SDK版本,看看是否需要更新Flex SDk,可能在更高版本中会有所改进。
(2)和其他语言一样,尽管Flash Player有内存回收机制,但这不代表所有的资源都可以回收。由程序员使用的外部资源或者系统类,必须由程序员自己释放或清理。比如:
Ø 使用完BindingUtils.bindSetter(),ChangeWatcher.watch()函数后需要调用ChangeWatcher.unwatch()函数来清除引用。
Ø 使用ModuleLoader的1oadModule()方法加载一个模块,在关闭这个模块后应当使用unLoadModule()方法将其卸载,并将该模块对象的引用置为null。
Ø 在开发图形应用时,如果使用Graphics画图,要将不再显示的图使用clear()方法清除。
Ø 当Image对象使用完毕后要把source设置为null。
Ø 当不需要一个音乐或视频时需要停止音乐、删除对象,将引用设置为null。
在http://www.insideria.com/2009/04/51-actionscript-30-and-flex-op.html中列出了一些可以改进Flex程序性能的技巧。感兴趣的学员可以查看原文及其相关讨论。
(1)不要使用new操作符创建数组,应使用:
var a =[];
而不是:
var a = new Array();
(2)创建数组的性能开销很大,请谨慎进行如下操作:
Var vanityCollection01 : Array = new Array();
Var vanityCollection02 : Array = new Array();
Var vanityCollection03 : Array = new Array();
Var vanityCollection04 : Array = new Array();
(3)最快的数组复制方法:
Var copy:Array = sourceArray.concat();
(4)为数组中的元素赋值都是较慢的操作,比如:
employees.push(employee);
employees.[2]=employee;
(5)从数组中读取元素的速度是为元素赋值速度的2倍。
var employee:Employee=employees[2];
(6)使用静态方法不需要实例化对象,可以提高性能(某些工具函数可以这样用全部都用静态方法则违反了面向对象的方法论)。
(7)将应用生命周期内都不会改变的属性声明为常量。
public const APPLICATION_PUBLISHER:String = “Company,Inc.”;
(8)当确定一个类不会派生子类时,应使用final修饰符。
public final class StringUtils;
9)在ActionScript3中,方法和变量的名称长度不会对性能造成影响(在其他语言中也一样)。
someCreazyLongMethodNameDoesntReallyImpactPerformanceTooMuch();
(10)在一行代码中进行多个变量赋值不会提高性能(在其他语言中也一样)。
var i=0;j=10;k=200;
(11)使用if语句和switch语句无内存开销的差异。
使用if语句:
If(condition){
//handle condition
}
使用switch语句
switch(condition){
case ‘A’:
//logic to handle case A
Break;
case ‘B’:
//logic to handle case B
Break;
}
(12)使用if语句时,尽可能地按照最有可能发生的情况的顺序进行判断和处理。
(13) AVM在循环体内部进行计算时,将整型(int)数据提升为浮点型Number进行处理(从版本9到版本10,ActionScript虚拟机已经有所改变,int ,unit, number之间的转换速度不再像之前那么慢了)。
(14)要解决类型转换,就要先解决未知和不正确的对象类型。
(15)谨慎使用unit,它可能会较慢(从版本9到版本10 ActionScript虚拟机已经有所改变int, uint, number之间的转换不再像之前那么慢了)。
Var footerHex : unit=oxooccff;
(16)应在for循环中使用int:
for(var i: int =0;i 而不是Number: for(var i: Number =0;i (17)不要用int类型来表示小数,应使用: var decimal:Number = 14.65; 而不是: var decimal:int = 14.65; (18)乘法性能高于除法:不要用5000/1000而要用5000*0.001。 (19)如果一个值是通过for或者while语句循环多次计算出来的(或者其他耗费较高性能才能得到的值),而且这个值需要在局部多次使用,那么应当在本地将该值存储。而不是每次访问该值时都重新计算。 for(..){a*180/Math.PI;} 声明:toRadians = a*180/Math.PI; 在循环体外 (20)尽量避免在循环体判断条件中进行计算或者方法调用,应当使用 var len:int = myArray.length; for(var i=0;i 而不是: vor(var i=0;i (21)使用正则表达式RegEx进行校验,使用字符串的方法进行查找。 (22)尽量重用对象来保持“内存平稳’,这些对象包括DisplayObjects,URLLoader等。 (23)遵循Flex组件模式(失效机制)。 createChildren(); commitProperties(); updateDisplayList(); (24)把使用DataGrids组件作为最后的显示手段(如果确信真的没有办法使用常规的手段来实现想要的功能,才使用Datagrids) (25)避免将Repeaters用于能够滚动的数据. (26)避免使用setStyle()方法(在Flex框架中最消耗性能的方法之一)。 (27)使用过多的容器将会严重降低系统性能。 (28)定义组件时不一定总要使用一个容器来作为该组件的顶级标签,也不一定需要顶级容器标签。 (29)清除不必要的容器包装以减少容器的嵌套。 (30)避免在标签内嵌套VB ox容器(消除冗余)。 (31)在mx :A pplication标签内部尽量避免使用VBox标签(消除冗余)。 (32)设置Repeater的recycleChildren属性为true可以提高Repeater对象的性能(重新利用已创建的子组件而不是再创建一个新的子组件)。 (33)应用的frameRate应设为60fps以内。 (34)避免在一帧中进行过多的显示操作。 (35)对于重复的操作,应使用Enter_frame事件替代Timer事件: (36)在多帧中延迟对象的创建,使用: (37)使组件不可见时使用Alpha=0与visible=false不是一回事(对象被标记为不可见将不会被渲染与布局处理),应当使用: loginButton.visible=false; 而不是: loginButton.alpha=0; 通过本节的内容。应该可以了解到以一下内容: Ø Flash Player的内存垃圾回收机理。 Ø 如何在开发中规避内存泄漏。 Ø 如何通过工具来发现内存泄漏? Ø 哪些系统级的API可以帮助我们在运行期间杳看系统内存的使用情况? Ø 提高程序性能的编程方式和技巧。 Web打印的方案有很多,根据它们实现方式的不同,大致可以分为以下三类: Ø 原生打印(Native Print),利用嵌入在浏览器中的Applet或Flash Player等插件提供的打印接口进行打印。由子打印的全部过程都在插件中完成,所以称之为原生打印。 Ø 宿主打印(Host Print),使用浏览器提供的打印接口进行打印。我们把使用浏览器插件增强打印功能的打印方式也归为此类,因为这些插件仅仅用于弥补浏览器打印接口的不足之处。 Ø 外部打印(External Print) ,先根据打印格式生成目标文档,然后使用此文档的宿主程序打开并打印。 这类打印方案的特点是直接使用SDK提供的API,具有响应迅速,跨平台跨浏览器,不依赖外部工具的优点。常见的有如下几种。 Ø JavaApplet:利用JDK提供的打印API,可以向打印机输出各种文字、图形图像和swing组件。 Ø Flex:利用Flex Framework提供的打印API,可以将Flex或Flash可视化组件输出至打印机。 由于Web页面必须通过浏览器来呈现,而常见的基于图形的浏览器都提供打印功能,因此可以通过浏览器实现打印。常见的有如下几种。 HTML:利用浏览器提供的打印功能,直接将其呈现的HTML输出至打印机。可以使用层叠样式表来控制HTML元素在打印纸上的呈现方式。 ActiveX+HTML:使用JavaScript调用专用的ActiveX控件来打印HTML元素或页面。这个ActiveX控件可以利用Win32打印API和浏览器的开发接口将HTML输出至打印机。 严格来说,这种方案不能叫做打印,它实际上是一种数据导出方案。它将应用的数据导出到外部工具,然后利用此工具来进行打印。常见的有如下几种。 Ø PDF打印:PDF格式是行业标准,通过PDF开发包(如基于Java的iText ,基于Flex的AliverPDF等)可以将应用的数据形成PDF文档,然后由Adobe Reader或其他工具负责打印。 Ø office打印:Microsoft Office套件中的Word和Excel使用广泛,而且许多其他Office软件都能兼容,因此可以将应用的数据导出为Word和Excel文档,然后再打印。 外部打印又可以按照文档形成的时机分为服务器端打印和客户端打印。前者的打印过程一般是由客户端发出打印命令、服务器根据打印的数据生成目标文档,然后传送至客户端。客户端再使用宿主程序打开此文档并打印。与前者不同。后者的文档直接在客户端生成。上述方案各具优缺点,在进行选择时需要综合考虑项目、团队和用户等方面的因素。表8-1-1对上述方案进行了对比,可以作为选型时的参考。 表8-1-1 Web打印常见方案及其比较 类别 方案 有点 缺点 原生打印 JavaApplet 能够通过程序选择打印机,设置纸张、页边距,打印方向和任务名称等打印任务参数;能够打印图片、文字Swing组件等、延迟生成打印内容,多页打印效率高 依赖于JRE,因此可能需要客户端下载 FLex 能够打印Flex中所有的可视化组件; 延迟生成打印内容,多页打印效率高 不能够通过程序选择打印机。也不能设置纸张、页边距、打印方向等打印任务参数 宿主打印 HTML 不需任何插件、能够打印HTML页面,可以使用CSS控制页面在打印时的呈现方式 需要事先生成好所有打印内容,多页打印时可能使页面过大.共他缺点同上 ActiveX+HTML 一般能够通过程序选择打印机,能够设置纸张,页边距、打印方向等打印任务参数,能够打印HTML页面或HTML元素,可以使用CSS控制页面在打印时的呈现方式 需要下载额外的插件;需要事先生成好所有的打印内容,多页打印时可能使HTML页面过大 外部打印 PDF打印 Office打印 打印内容丰富,布局灵活,数据的导出非常容易 需要安装相应的工具软件;同时也具有HTML方案的缺点 尽管Flex无法通过程序选择打印机和设置打印参数,但是,如果选择了Flex技术作为系统的前端展现,那么PrintJob天生就能与系统的其他功能模块无缝集成。没有进程调用,没有数据转换,能够快速响应打印请求,无需引入额外技术或语言,实现单一技术的易维护性,并且也具有Flex跨主流平台和浏览器的优势。 由于Flex打印的是可视化组件,所以无论多么复杂,只要界面上能展示,那么Flex就能打印出来。依托于Flex布局容器的自动布局功能,可以实现像屏幕显示那样动态地调整元素的布局。 因此,如果不想在项目中引入额外的技术或工具,并且又能说服客户接受Flex打印的缺点,那么可以考虑使用这种“便宜”而且实用的方案。 如果你开发过Win32打印程序,你一定会对打印心有余悸。不过别担心,现在我们有了PrintJob。相信你第一眼看到它一定会对它的简洁感到吃惊。 首先,让我们通过一段简单的代码来看看PrintJob是如何工作的,如示例9.9所示。 示例9.9典型的PrintJob打印过程 var job = new PrintJob();//创建一个PrintJob的实例 if(job.start()){//启动PrintJob job.addPage(Application.application);//将整个application作为一页添加到打印内容 job.send();//发送数据 下面,让我们来分析整个打印过程。 Ø Start():调用这个方法后,Flash Player将会为我们查找系统的默认打印机,并且使用此打印机的默认设置显示打印对话框。如果用户选择取消,那么方法返false,此时如果继续调用addPage或send方法,将会引发F1ash Player异常。如果用户选择打印,那么方法返回true,并且用户通过打印对话框进行的设置将会被保存在PrintJob的这个实例中。 PrintJob有5个属性,分别记录打印方向、纸张大小和可打印区域,其中纸张大小和可打印区域的单位都已映射为像素,这是由Flash Player根据打印机的默认分辨率完成的。 Ø addPage():这个方法告诉PrintJob开始新的打印页,并且在此页上绘制指定的Sprite实例。在一个PrintJob中可以通过多次调用addPage实现多页打印。这个方法是打印过程中最重要和最复杂的,稍后将详细介绍。 Ø send():将数据发送至操作系统的打印任务池并结束这个PrintJob。 除了打印内容的生成外,这几乎是打印功能所需要的所有代码了,当然,你还可以添加异常处理代码以增强程序的健壮性。 从上一节的分析可以看出,打印前的谁备工作很繁琐,幸运的是,F1ash Playr提供的PrintJob. Start()方法为我们完成了这部分工作。 既然知道了用户选择的纸张和打印方向,那么就可以使用PrintJob.addPage()把打印纸映射成屏幕区域,然后以一种我们熟悉的、与屏幕显示类似的方式向打印机输出内容了。 PrintJob.addPage()有4个参数,下面是它的方法签名: addPage(sprite:Sprite,printArea:Rectangle=null,options:PrintJobOptions=null,frameNum:int=0):void 只有第1个参数是必需的,其他参数可以忽略。 (1) sprite 这里必须是一个flash.display.Sprite的实例,它是要打印的根组件。我们知道,Flex中的所有显示组件都是从sprite派生而来,因此这些组件都可以安全地传递给addPage()。但是,要想正确地打印,这个sprite须位于显示列表中。与屏幕显示不同的是,打印时会忽略这个sprit。的visible属性,也就是说,打印内容不必在屏幕上可见。这个例外仅局限于打印的根组件,如果这个sprite包含一个mx.controls.Label且其visible=false,那么打印时也是不可见的,这是Flex考虑得比较周到的地方。示例9.10展示的小例子说明了这个特性,请注意组件的visibile属性。 示例9.10 组件在打印时的可见性演示 layout="vertical" applicationComplete="doPrint();" >
import com.flexbook.blazeds.DataRow; private function doPrint():void{ var job:PrintJob=new PrintJob(); if(job.start()){ job.addPage(printContent); job.send(); } } ]]> 编译并运行示例9.10,程序将在应用启动后打印出如图9.10所示的画面。由此可见printContent作为打印的根组件,它的visible属性不影响打印。但printContent的内部的三个三个Lable的visible属性对可见性的影响仍然遵循“与屏幕显示一致“的规则:visible为true时可见,否则不可见。 图9.10是使用虚拟打印机(pdfFactory)打印出来的结果,因此可能会与物理打印机打印出的实际效果有所不同。 图9-10 printContent的打印结果 (2) printArea 打印范围,它与前述PrintJob保存的打印机的可打印区域不同,它用于指定第一个参数sprite的需要输出的区域范围,而后者表示的是打印机受到物理或机械限制在特定纸张中的打印能力。sprite中不在此区域范围内的内容不会被输出。此区域的左上角坐标被映射成打印机可打印区域的左上角坐标,如果此区域表示的打印的范围超过了打印机的可打印区域,右下方的内容将会被裁剪掉。假设我们想将左图窗口中灰色区域部分打印到A4纸上,那么打印的根对象就是整个左图窗口,灰色区域表示printArea指定的打印范围,右图的外部实线框表示纸张,内部实线框表示打印机的可打印区域,最终我们指定的打印区域会被打印到右图纸张的灰色区域部位。掌握了这一点,我们就可以精确地在纸张的任意位置上打印内容了。 (3)options 指示打印输出位图还是矢量图,如果打印内容不含位图图片,那么使用矢量图可以获得更 高的打印质量。 (4)frameNum 用于指定要打印的帧的序号。一般的Flex应用只有两帧,所以不用指定此参数,默认是打 印当前帧。 小结 在Flex打印中 应该注意一下事项: Ø PrintJob的start方法是阻塞的,也就是说,在它返回之前,Flash Player会阻塞ActionScript代码的执行。 Ø start,addPage和send三个方法调用间隔不能大于1秒,否则会出现脚本超时异常,这在Adobe的文档中有详细说明。 Ø PrintJob. pageHeight和PrintJob. pageWidth记录的是打印机的可打印区域,并未刨除用户设置的页边距,这样我们就可以在程序中控制输出的页边距。 Ø 在将从UIComponent继承而来的显示组件传递给addPage前,必须保证组件已经在显示列表中并被有效化(validated)。这可以通过调用UIomponent.validateNow让Flex立即更新组件,这在动态打印中非常重要。 Ø mx.printing.FlexPrintJob也是Flex提供的打印API ,它实际上封装PrintJob并加入了对UIComponent的支持,在打印前后会分别调用UIComponent的prepareToPrint和finishPrint方法,同时也加入了简单的分页功能。 Ø Flex提供了mx.printing.PrntDataGrid,可用于打印简单的表格。 任务实训部分 训练技能点 RSI的应用。 需求说明 使用RSI技术减少SWF文件体积。 实现思路 (1)创建MXML应用程序。 (2)分别设置工程的框架链接类型为 静态 和动态 并观察生成的SWF文件体积的变化。 设置rpc.swc datavisualization.swc库的摘要,并观察SWF文件体积的变化。 训练技能点 Ø 内存泄露分析工具的使用。 Ø 优化程序的内存使用。 实现思路: (1)导入第六章上机任务3。 (2)启动Flash内存泄露检测工具,分析系统的内存使用情况 找出存在的问题。 (3)结合理论内容8.6 8.7小节的内容 修改程序代码,优化内存使用,防止内存泄露。 巩固练习 选择题 1. 关于减少SWF文件的体积 ,错误的说法是 () A. 可以使用动态链接降低SWF体积。 B. 可以使用静态链接降低SWF体积。 C. 可以使用RSL技术降低SWF体积。 D. 通过垃圾回收可以降低SWF体积。 2. 以下关于Flex程序垃圾回收的说法,正确的是 () A. ActionScript语言是一种支持GC的语言。经过编译后的AS代码运行在AS虚拟机(简称AVM)中,由AVM自动完成垃圾内存回收的工作。不会发生内存泄露。 B. 删除变量就意味着释放掉对象占用的内存。 C. AS根据引用计数法就可以精确的判断对象是否需要释放。 D. 垃圾回收总是在请求内存时触发,而不是在对象删除时发生。 3. 以下哪些操作可能引发内存泄露()。 A. 使用对象绑定。 B. 使用addEventListener()方法“为对象添加事件监听器”。 C. 用弱引用方式注册监听器。 D. 自引用的方式注册监听器。 4. Flex打印分为多种方式 分别是()。 A. 原生打印。 B. 本地打印。 C. 宿主打印。 D. 外部打印。 本阶段课程要掌握如下技能和知识: Ø 理解RIA和Flex的的概念和特点 Ø 熟练掌握AS基本语法 Ø 会熟练使用Flex常用控件和视图状态构建程序界面 Ø 会熟练使用FLex技术与外部进行数据通信,并整合后台各种框架 Ø 会熟练使用客户端MVC框架 Ø 会熟练使用AIR技术开发应用 Ø 熟练掌握美化Flex界面以及性能优化的基本原理和技巧 图10.1.1 Flex知识结构图 本次任务实现一个Flv格式的视频播放器,能够播放flv格式的视频文件,并具有最大化 最小化 开始 暂停 快捷键和系统托盘等功能,使用AIR结合cairngorm框架进行开发。 视频播放器的设计应该说是非常直观的。视频播放功能的实现其实并不复杂,前面我们已经学习过视频播放必须具备的某些核心功能(如Flex的flv播放器),现在马上就可以开始播放器的开发工作了,其实也就是将Flex的flv播放器集成到我们的程序界面中,在添加一些常用功能就可以了,但如果想充分运用AIR的功能来创建视频播放程序,那么还需要投入更多的精力进行规划设计。 我们的视频播放器具有AIR Video player的所有核心功能,这些功能可以大致分为两类,一类是视频播放,一类是程序控制。这些功能如下所示。 Ø 播放控制:所有的视频播放器都至少具有一个play(播放)按钮和一个停止按钮。Play按钮可以在暂停和播放两个状态之间切换,并相应的改变按钮的外观。除了Play Stop控件外,还应该具有一个能让你跳到视频任何部分的时间轴滑轨,一个只是播放进度的时间显示控件和音量控制器。 Ø 易于调整的布局:AIR视频视频播放器的一大特征就是可以实现视频的全屏播放。为了实现该特征,我们需要创建一个能随着视频尺寸变化而调整的布局。例如,如果视频的尺寸由300*250变为640*480或者全屏,那么播放控件要能根据调整后的尺寸显示视频。 Ø 网络摄像头抓图功能:这一功能并不是必须的,但给视频播放器添加这一功能是简单和有趣的事情。这部分工作将会留给学员扩展完成。 在这个视频播放器的主要控件完成后,需要在主容器( Ø 全屏支持:Flash播放器支持全屏播放模式。这一特色对于像AIR视频播放器这样的应用程序来说非常有用,当让也很容易做到。该功能将使用户的电脑变成一个虚拟的影院。 Ø 个性化的主窗口,这也是AIR程序的一大特色,可以随意定制程序主窗口的形状,达到更好的效果,以及窗口的拖拽功能。 Ø 支持快捷键播放 暂停。 Ø 主窗口可以最小化到系统托盘。 Ø 在网络环境下,还可以在程序窗口中打开迅雷看看高清电影的主页,以实现在线播放的功能。 (1)构建目录结构 建立新的AIR工程并导入Cairngorm框架,按照MVC模式在工程中建立目录,目录结构如图10.1.1所示。 图10.1.1 目录结构 目录建好之后将程序所需要的各种资源(图片)放入assets/images目录下备用。下面就可以开始进入视频播放器的开发环节了。 (2)建立Cairngorm框架组件 本任务中并没有严格的按照Cairngorm的模式来实现功能,只是在播放功能中部分使用了Cairngorm的模式。学员们可以在完成主要功能后再严格按照Cairngorm结构重构系统。 OpenVideoEvent.as 该事件将会在选择要播放的视频文件后被派发。 package com.xi.flvPlayer.event { import com.adobe.cairngorm.control.*; import com.xi.flvPlayer.control.*; public class OpenVideoEvent extends CairngormEvent { public function OpenVideoEvent() { super(FlvControl.EVENT_OPEN_VIDEO); } } } OpenVideoCommand.as 用来处理OpenVideoEvent事件的命令。 package com.xi.flvPlayer.commands { import com.adobe.cairngorm.control.*; import com.xi.flvPlayer.business.*; import com.xi.flvPlayer.model.FlvModel; import com.adobe.cairngorm.commands.*; import com.adobe.cairngorm.view.*; import com.xi.flvPlayer.control.FlvControl; import com.xi.flvPlayer.view.*; import flash.events.*; import flash.net.*; import com.xi.flvPlayer.event.*; public class OpenVideoCommand implements Command { public function execute(event:CairngormEvent):void { //定义FlvPlayerViewHelper实例 var fpViewHelper:FlvPlayerViewHelper=FlvPlayerViewHelper(ViewLocator.getInstance().getViewHelper("flvPlayerViewHelper")); //不相同的视频或从未点击过的视频,需要加载 if(FlvModel.currVideo==""||FlvModel.currVideo!=FlvModel.videoSource) { fpViewHelper.loadMediaPlay(FlvModel.videoSource); FlvModel.currVideo=FlvModel.videoSource; } else//相同的视频则重新开始播放 { fpViewHelper.restartVideo(); } } } } FlvModel.as 数据模型。 package com.xi.flvPlayer.model { import com.adobe.cairngorm.model.ModelLocator; //import com.asiatom.englishr.vo.PlayListVO; import mx.controls.ToolTip; public class FlvModel implements ModelLocator { private static var flvModel : FlvModel = new FlvModel(); public static function getInstance() : FlvModel { return flvModel; } public function initialise() : void { } // 当前播放视频地址 public static var currVideo : String=""; //将要加载的视频地址 public static var videoSource:String=""; //是否为全屏状态 public static var isFullSrceen:Boolean=false; //---------- 资源定义 --------------------------------------------------------------- [Bindable] [Embed(source="/assets/images/btn_moveover_break.png")] public var btn_moveover_break:Class; [Bindable] [Embed(source="/assets/images/btn_moveover_fullscreen.png")] public var btn_moveover_fullscreen:Class; [Bindable] [Embed(source="/assets/images/btn_moveover_play.png")] public var btn_moveover_play:Class; [Bindable] [Embed(source="/assets/images/btn_moveover_stop.png")] public var btn_moveover_stop:Class; [Bindable] [Embed(source="/assets/images/btn_normal_break.png")] public var btn_normal_break:Class; [Bindable] [Embed(source="/assets/images/btn_normal_fullscreen.png")] public var btn_normal_fullscreen:Class; [Bindable] [Embed(source="/assets/images/btn_normal_play.png")] public var btn_normal_play:Class; [Bindable] [Embed(source="/assets/images/btn_normal_stop.png")] public var btn_normal_stop:Class; [Bindable] [Embed(source="/assets/images/ico_break.png")] public var ico_break:Class; } } FlvPlayerViewHelper.as 辅助操作视图类。使用ViewHelper类可在AS文件中修改其他MXML文件中的视图。 package com.xi.flvPlayer.view { import com.adobe.cairngorm.view.ViewHelper; import mx.core.Application; public class FlvPlayerViewHelper extends ViewHelper { public function FlvPlayerViewHelper()//构造函数 { super(); } //加载视频处理函数 public function loadMediaPlay(src:String):void { view.mainDisplay.source=src; view.mainDisplay.load(); view.mainDisplay.play(); } //重新播放视频处理函数 public function restartVideo():void { view.mainDisplay.playheadTime=0; } } } FlvControl.as 前端控制器 package com.xi.flvPlayer.control{ import com.adobe.cairngorm.control.FrontController; import com.xi.flvPlayer.commands.*; public class FlvControl extends FrontController { public function FlvControl() : void { initialiseCommands(); } public function initialiseCommands() : void { addCommand( FlvControl.EVENT_OPEN_VIDEO, OpenVideoCommand ); } //------------------- 打开视频 ---------------------------------// public static var EVENT_OPEN_VIDEO:String="openVideo"; } } Utils.as 工具类 主要用来格式化时间和字符串。 package com.xi.flvPlayer.utils { //import com.adobe.cairngorm.view.ViewLocator; //import com.xi.flvPlayer.control.FlvControl; //import com.xi.flvPlayer.model.FlvModel; //import com.adobe.cairngorm.control.CairngormEventDispatcher; public class Utils { public static function formatVideoTime(t : Number) : String//格式化时间处理函数 { var s : Number = Math.floor(t);//取整数 var hour : Number = int(s/3600);//取小时 var minute : Number = int((s-hour*3600)/60);//取分钟 var second : Number = s-hour*3600-minute*60;//取秒 var p : Number = Math.round( ( t - s ) * 1000 );//取毫秒 //转特定格式“hh:mm:ss xxx” return padStr(hour.toString(), 2) + ":" + padStr(minute.toString(), 2) + ":" + padStr(second.toString(), 2) + " " + padStr(p.toString(), 3); } public static function padStr(src : String, len : int ) : String//添加前导零 { if ( src.length > len )//若字符串长度超过指定长度,则截断字符串 return src.substr(0, len); var s : String = src; for ( var i : int = s.length; s.length < len; )//添加前导零 { s = "0" + s; } return s; } } } (3)开发主界面 首先建立主程序Player.MXML,然后修改Player-app.xml,在第50行 添加以下代码。 用来取消主窗口的标题栏和边框。 主程序代码如下。 xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:view="com.xi.flvPlayer.view.*" xmlns:control="com.xi.flvPlayer.control.*" layout="absolute" backgroundColor="#ccffff" minWidth="587" minHeight="439" showFlexChrome="false" width="587" height="439" xmlns:s="library://ns.adobe.com/flex/spark">
import com.xi.flvPlayer.model.FlvModel; import com.xi.flvPlayer.utils.Utils; //拖动条显示提示处理函数 private function SliderToolTipFormatter(val : Number) : String { return Utils.formatVideoTime(val);//格式化为时间格式“00:00:000 000” } ]]> autoRewind="false" volume=" {videoVolume.value / 100 } " playheadUpdateInterval="100" doubleClickEnabled="true" width="586" height="330"/> width="{WindowedApplication(mx.core.Application.application).width*(60/624)}" height="{img_break.width}" alpha="0.7" x="{(mainDisplay.width-img_break.width)/2}" y="{(mainDisplay.height-img_break.height)/2}" visible="false"/> liveDragging="true" showDataTip="true" dataTipFormatFunction="SliderToolTipFormatter" width="100%" allowTrackClick="true" value="{mainDisplay.playheadTime}" enabled="false"/> width="35" height="35" icon="{FlvModel.getInstance().btn_normal_play}" cornerRadius="25" enabled="false" toolTip="播放/暂停" /> width="19" height="19" icon="{FlvModel.getInstance().btn_normal_stop}" overIcon="{FlvModel.getInstance().btn_moveover_stop}" enabled="false" toolTip="停止" /> liveDragging="true" value="50" enabled="false" maximum="100" tickInterval="50" showDataTip="false"/> icon="{FlvModel.getInstance().btn_normal_fullscreen}" overIcon="{FlvModel.getInstance().btn_moveover_fullscreen}" width="35" height="35" cornerRadius="25" /> 运行程序,效果如图10.1.2所示。 图10.1.2 播放器界面 (4)完成基本播放功能 xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:view="com.xi.flvPlayer.view.*" xmlns:control="com.xi.flvPlayer.control.*" layout="absolute" backgroundColor="#ccffff" minWidth="587" minHeight="439" showFlexChrome="false" applicationComplete="initApp()" width="587" height="439" xmlns:s="library://ns.adobe.com/flex/spark">
import com.adobe.cairngorm.control.*; import com.xi.flvPlayer.commands.*; import com.xi.flvPlayer.control.*; import com.xi.flvPlayer.event.*; import com.xi.flvPlayer.model.FlvModel; import com.xi.flvPlayer.utils.*; import flash.filesystem.File; import mx.controls.Alert; import mx.core.Application; import mx.events.CloseEvent; import mx.events.MenuEvent; import mx.events.SliderEvent; //定义FileFilter,只允许.flv格式 private var displayTypes:FileFilter = new FileFilter("播放格式(*.flv)", "*.flv"); private var allTypes:FileFilter=new FileFilter("全部(*.*)","*.*"); private var fileFilter:Array = new Array(displayTypes, allTypes); //定义File实例,用以存储打开的文件 private var choosedFile:File = new File(); //应用程序初始化处理函数 private function initApp():void { // 捕获关闭窗口事件,让用户决定窗口是隐藏还是关闭 this.addEventListener(Event.CLOSING, closingApplication); } /** * 用户决定窗口是隐藏还是关闭 * @Author: S.Radovanovic */ private function closingApplication(evt:Event):void { // 以下语句防止默认的关闭动作发生 evt.preventDefault(); // 弹出窗口,让用户判断是否要关闭窗口 //Alert.buttonWidth = 110; Alert.yesLabel = "关闭"; Alert.noLabel = "最小化"; Alert.show("关闭还是最小化?", "关闭?", 3, this, alertCloseHandler); } // 响应弹出窗口中用户的选择 private function alertCloseHandler(event:CloseEvent):void { closeApp(event); } /** * 关闭程序 */ private function closeApp(evt:Event):void { stage.nativeWindow.close(); } protected function closebut_clickHandler(event:MouseEvent):void { WindowedApplication(mx.core.Application.application).close();//退出应用程序 } protected function openbut_clickHandler(event:MouseEvent):void { choosedFile.browse(fileFilter);//打开选择对话框 choosedFile.addEventListener(Event.SELECT,selectHandle);//添加选择文件后的监听 } //选择文件后的处理函数 private function selectHandle(e:Event):void { FlvModel.videoSource="file:///"+choosedFile.nativePath;//获取视视频路径 //加载视频 var evt:OpenVideoEvent=new OpenVideoEvent(); CairngormEventDispatcher.getInstance().dispatchEvent(evt); } //拖动条放开事件处理函数 private function thumbReleased(event:SliderEvent):void { mainDisplay.pause();//暂停播放 mainDisplay.playheadTime=videoSlider.value;//设置时间点 mainDisplay.play();//继续播放 } //开始/暂停事件处理函数 public function toggle() : void { if (mainDisplay.playing)//当前状态正在播放时,暂停视频 { play.setStyle("icon",FlvModel.getInstance().btn_normal_break);//设置“play”按钮的icon样式 play.setStyle("overIcon",FlvModel.getInstance().btn_moveover_break);//设置“play”按钮的overIcon样式 img_break.visible=true;//显示“暂停”图标 mainDisplay.pause();//暂停视频 } else//当前状态是暂停,播放视频 { play.setStyle("icon",FlvModel.getInstance().btn_normal_play);//设置“play”按钮的icon样式 play.setStyle("overIcon",FlvModel.getInstance().btn_moveover_play);//设置“play”按钮的overIcon样式 img_break.visible=false;//隐藏“暂停”图标 mainDisplay.play();//播放视频 } } //视频停止播放事件处理函数 public function stopPlay() : void { mainDisplay.playheadTime=0;//设置时间点为0 mainDisplay.stop();//停止播放 } //“静音”处理函数 private function LowerVolume():void { videoVolume.value = 0;//设置音量最小 } //拖动条显示提示处理函数 private function SliderToolTipFormatter(val : Number) : String { return Utils.formatVideoTime(val);//格式化为时间格式“00:00:000 000” } //拖动条按下事件处理函数 private function thumbPressed(event:SliderEvent):void { if ( mainDisplay.playing )//正在播放,则暂停播放 { mainDisplay.pause(); } } //playheadTime定时更新事件 处理函数 private function videoPlayheadTimeChanged() : void { if(Math.abs(mainDisplay.playheadTime-videoSlider.maximum)<0.1)//拖曳滑动条到结束位置时,重置时间 { mainDisplay.playheadTime=0;//设置时间点为0 mainDisplay.pause();//暂停播放 } else { videoCurrTime.text = Utils.formatVideoTime(mainDisplay.playheadTime);//修改当前播放时间 } } //视频状态改变事件处理函数 private function videoStateChange() : void { videoState.text = mainDisplay.state;//显示当前视频状态 } //视频准备完毕事件处理函数 private function videoReady() : void { WindowedApplication(mx.core.Application.application).width=mainDisplay.videoWidth;//设置应用程序宽度为实际视频宽度 WindowedApplication(mx.core.Application.application).height=mainDisplay.videoHeight*(458/352);//设置应用程序高度 videoTotalTime.text = Utils.formatVideoTime(mainDisplay.totalTime);//显示总时间 videoSlider.maximum =mainDisplay.totalTime;//设置拖动条数值长度 enableAllCtrl(true);//设置播放组件状态为可用 } //播放控件enable设置处理函数 public function enableAllCtrl(b : Boolean) : void { videoSlider.enabled = b; play.enabled = b; videoVolume.enabled = b; btnStop.enabled=b; btnQueit.enabled=b; } ]]> stateChange="videoStateChange()" autoRewind="false" ready="videoReady()" volume=" {videoVolume.value / 100 } " playheadUpdate="videoPlayheadTimeChanged()" playheadUpdateInterval="100" doubleClickEnabled="true" width="586" height="330"/> width="{WindowedApplication(mx.core.Application.application).width*(60/624)}" height="{img_break.width}" alpha="0.7" x="{(mainDisplay.width-img_break.width)/2}" y="{(mainDisplay.height-img_break.height)/2}" visible="false"/> liveDragging="true" showDataTip="true" thumbRelease="thumbReleased(event);" thumbPress="thumbPressed(event);" dataTipFormatFunction="SliderToolTipFormatter" width="100%" allowTrackClick="true" value="{mainDisplay.playheadTime}" enabled="false"/> width="35" height="35" icon="{FlvModel.getInstance().btn_normal_play}" click="toggle();" cornerRadius="25" enabled="false" toolTip="播放/暂停" /> width="19" height="19" icon="{FlvModel.getInstance().btn_normal_stop}" overIcon="{FlvModel.getInstance().btn_moveover_stop}" click="stopPlay();" enabled="false" toolTip="停止" /> liveDragging="true" value="50" enabled="false" maximum="100" tickInterval="50" showDataTip="false"/> icon="{FlvModel.getInstance().btn_normal_fullscreen}" overIcon="{FlvModel.getInstance().btn_moveover_fullscreen}" width="35" height="35" cornerRadius="25" /> 运行程序 效果如图10.1.3所示。 图10.1.3 播放器基本功能 (5)添加窗口移动功能 由于主窗口取消了边框和标题栏,所以窗口现在无法使用鼠标拖动,现在我们需要在程序的 protected function me_mouseDownHandler(event:MouseEvent):void { this.nativeWindow.startMove(); } //省略部分代码 (6)添加全屏和退出全屏功能 通过设置舞台(stage)的displayState属性设置为 StageDisplayState.NORMAL(非全屏) StageDisplayState.FULL_SCREEN(全屏)即可将Flash播放器改为全屏播放模式或者退出全屏。 在主程序中添加如下代码。 private function initApp():void { // 捕获关闭窗口事件,让用户决定窗口是隐藏还是关闭 this.addEventListener(Event.CLOSING, closingApplication); //为舞台添加监听器 ,在全屏和退出全屏的时候调用fullscreenresize方法 来调整控件大小,这主要是为了解决在全屏状态下按下ESC退出全屏带来的布局混乱问题。 stage.addEventListener(FullScreenEvent.FULL_SCREEN,fullScreenreSize); } protected function fullScreenHandler():void { if(FlvModel.currVideo!="")//不在播放视频时不能全屏 { if(stage.displayState==StageDisplayState.NORMAL)//当前是非全屏状态,则进入全屏状态 { stage.displayState=StageDisplayState.FULL_SCREEN;//进入应用程序全屏 } else //当前是全屏状态,则非全屏 { stage.displayState=StageDisplayState.NORMAL;//应用程序非全屏 } } } //当发生全屏或者退出全屏的事件 则调整控件大小以适应舞台 public function fullScreenreSize(event:Event):void { if( stage.displayState==StageDisplayState.NORMAL)//如果退出应用程序全屏则恢复 控件的原始大小 { this.mainDisplay.width=586; this.mainDisplay.height=330; this.me.width=586; this.controlPanel.width=586; } else {//如果进入全屏 则放大控件 this.mainDisplay.width=this.width; this.me.width=this.width; this.controlPanel.width=this.width; this.mainDisplay.height=this.height-this.controlPanel.height-this.me.height; } } //省略部分代码 stateChange="videoStateChange()" autoRewind="false" ready="videoReady()" volume=" {videoVolume.value / 100 } " playheadUpdate="videoPlayheadTimeChanged()" playheadUpdateInterval="100" doubleClickEnabled="true" doubleClick="fullScreenHandler()" width="586" height="330"/> //省略部分代码 icon="{FlvModel.getInstance().btn_normal_fullscreen}" overIcon="{FlvModel.getInstance().btn_moveover_fullscreen}" width="35" height="35" cornerRadius="25" /> (7)添加快捷键 所谓快捷键 ,其实就是在主窗口监听器keydown事件,当特定键按下的时候,执行指定操作即可。 在主程序中添加以下代码。 //应用程序初始化处理函数 private function initApp():void { // 捕获关闭窗口事件,让用户决定窗口是隐藏还是关闭 this.addEventListener(Event.CLOSING, closingApplication); //为舞台添加监听器 ,在全屏和退出全屏的时候调用fullscreenresize方法 来调整控件大小,这主要是为了解决在全屏状态下按下ESC退出全屏带来的布局混乱问题。 stage.addEventListener(FullScreenEvent.FULL_SCREEN,fullScreenreSize); //添加快捷键的监听 WindowedApplication(mx.core.Application.application).addEventListener(KeyboardEvent.KEY_DOWN,KeyboardClickHandle); //添加定时器 var timer:Timer=new Timer(100,0); timer.addEventListener(TimerEvent.TIMER,checkFocusHandle); timer.start(); } //定时设置focus public function checkFocusHandle(e:TimerEvent):void { WindowedApplication(mx.core.Application.application).setFocus(); } //快捷键处理函数 public function KeyboardClickHandle(e:KeyboardEvent):void { switch(e.keyCode) { case 32: //空格键按下,则暂停or播放 toggle(); break; case 83://按下“S”时停止播放 stopPlay(); break; case 113://按下“s”时停止播放 stopPlay(); break; } } 定时调用checkFocusHandle方法主要是为了解决文件选择框的焦点问题。 (8)最小化到系统托盘 所谓最小化到系统托盘,其实质就是将主窗口隐藏,并在系统托盘中添加一个图标而已。 //定义变量 作为系统图标 private var dockImage:BitmapData; //应用程序初始化处理函数 private function initApp():void { // 捕获关闭窗口事件,让用户决定窗口是隐藏还是关闭 this.addEventListener(Event.CLOSING, closingApplication); //为舞台添加监听器 ,在全屏和退出全屏的时候调用fullscreenresize方法 来调整控件大小,这主要是为了解决在全屏状态下按下ESC退出全屏带来的布局混乱问题。 stage.addEventListener(FullScreenEvent.FULL_SCREEN,fullScreenreSize); //添加快捷键的监听 WindowedApplication(mx.core.Application.application).addEventListener(KeyboardEvent.KEY_DOWN,KeyboardClickHandle); //添加定时器 var timer:Timer=new Timer(100,0); timer.addEventListener(TimerEvent.TIMER,checkFocusHandle); timer.start(); //进行系统托盘的设置 //用loader对象载入一张图像作为系统托盘图标 var loader:Loader = new Loader(); loader.contentLoaderInfo.addEventListener(Event.COMPLETE, prepareForSystray); loader.load(new URLRequest("btn_normal_quiet.png")); } // 响应弹出窗口中用户的选择 private function alertCloseHandler(event:CloseEvent):void { if (event.detail==Alert.YES) { closeApp(event); } else { //用户单击关闭按钮的时候,如果选择了最小化 则调用dock方法。最小化到系统托盘 dock(); } } /** * 设置基本属性。 */ public function prepareForSystray(event:Event):void { // 把图像作为系统图标 dockImage = event.target.content.bitmapData; // windows 支持系统托盘图标, 苹果系统同样也支持, if (NativeApplication.supportsSystemTrayIcon){ setSystemTrayProperties(); // 设置系统托盘菜单 SystemTrayIcon(NativeApplication.nativeApplication .icon).menu = createSystrayRootMenu(); } } /** * 创建系统托盘菜单 */ private function createSystrayRootMenu():NativeMenu{ // 添加菜单元件,每个元件响应相应的方法 var menu:NativeMenu = new NativeMenu(); var openNativeMenuItem:NativeMenuItem = new NativeMenuItem("Open"); var exitNativeMenuItem:NativeMenuItem = new NativeMenuItem("Exit"); // 当用户双击元件时发生的事件 openNativeMenuItem.addEventListener(Event.SELECT, undock); exitNativeMenuItem.addEventListener(Event.SELECT, closeApp); // 把菜单元件添加到菜单中 menu.addItem(openNativeMenuItem); menu.addItem(new NativeMenuItem("",true)); menu.addItem(exitNativeMenuItem); return menu; } /** * 设置隐藏和激活的一些事件侦听 */ private function setSystemTrayProperties():void{ // 当鼠标悬停在系统托盘图标上时显示文字。 SystemTrayIcon(NativeApplication.nativeApplication .icon).tooltip = "Systray test application"; // 双击系统托盘图标打开程序 SystemTrayIcon(NativeApplication.nativeApplication .icon).addEventListener(MouseEvent.CLICK, undock); // 侦听窗口显示状态的改变,这样就可以捕获最小化事件 stage.nativeWindow.addEventListener(NativeWindowDisplayStateEvent.DISPLAY_STATE_CHANGING, nwMinimized); //捕获最小化事件 } /** * 窗口的状态改变时执行合适的动作。 * 当用户点击最小化按钮,隐藏程序到系统托盘 * */ private function nwMinimized(displayStateEvent:NativeWindowDisplayStateEvent):void { if(displayStateEvent.afterDisplayState == NativeWindowDisplayState.MINIMIZED) { displayStateEvent.preventDefault(); // 当按下最小化按钮是,要阻止默认的最小化发生 dock(); } } /** * 在系统托盘中显示程序图标 */ public function dock():void { // 隐藏当前窗口 stage.nativeWindow.visible = false; // 设置 bitmaps 数组,在系统托盘中显示程序图标。 NativeApplication.nativeApplication .icon.bitmaps = [dockImage]; } /** * 重新显示程序窗口,从系统托盘中移除图标 */ public function undock(evt:Event):void { // 把窗口设置为可见, 并确保程序在最前端。 stage.nativeWindow.visible = true; stage.nativeWindow.orderToFront(); // 清空 bitmaps 数组,同时清空了系统托盘中的程序图标。 NativeApplication.nativeApplication .icon.bitmaps = []; } 运行程序效果如图10.1.4所示 ,程序最小化到了系统托盘 通过菜单可以将程序恢复。 图10.1.4 系统托盘 (9)在播放器中添加片库 所谓添加片库,就是使用一个 protected function moviebut_clickHandler(event:MouseEvent):void { if(this.currentState=="movie"){ this.currentState="default"; } else{ this.currentState="movie"; } } ]]> //省略部分代码... 运行程序 效果如图10.1.5所示。 图10.1.5 片库 Ø 使用Flex控件实现视图界面 Ø 使用Spring+Hibernate完成后台 Ø 使用BlazeDS与后台交互 在现实生活中,从事商业活动的企业需要经常进行进货、销售等业务,进货即采购,就是企业购入了一些商品,并需要记录采购商品的名字、价格、数量等信息,同时企业中的原商品库存量相应的增加;销售就是企业把采购的商品以高于采购价的价格售出,并需要记录售出商品的名字、价格、数量等信息,同时企业中的原商品库存量相应的减少。 在激烈的市场竞争环境里,企业需要及时地了解进货、销售等所有进销存环节上的实际情况或具体数据,并以此做出商业决策。随着Internet的发展和计算机的普及,人们发现通过计算机系统进行商品进销存管理的优势十分明显,所以进销存工作也进入了信息时代,众多的进销存管理系统纷纷出现。 进销存管理系统是一个典型的数据库应用程序,根据企业的需求,能有效解决企业账目混乱,库存不准,信息反馈不及时等问题,集采购、销售、库存管理为一体,能帮助企业处理日常的进销存业务,同时提供丰富的实时查询统计功能。 本此项目为书店开发一个图书进销存管理系统,涵盖了基本的进货、销售、查询等功能。 1. 图书进销存管理系统的权限分成两类:管理员和操作员。管理员主要对整个系统进行管理,包括用户管理、出版社管理、类别管理和各种查询,如图11.1.1所示。 图11.1.1 管理员业务 Ø 用户管理:管理员可以对用户进行添加和删除,也可以修改用户的权限。 Ø 出版社管理:管理员可以对出版社进行增删改查操作。 Ø 图书类别管理:管理员可以对图书类别进行增删改查操作。 Ø 图书进货查询:管理员可以根据条件查询图书的进货记录。 Ø 图书销售查询:管理员可以根据条件查询图书的销售记录。 Ø 图书库存查询:管理员可以根据条件查询图书的当前库存数量。 2. 操作员没有管理权限,只能进行日常的进货和销售操作,如图11.1.2所示。 图11.1.2 操作员业务 Ø 图书进货:当书店新采购一批图书时,操作员需要通过本系统记录图书的采购数据 Ø 图书销售:当书店销售图书时,操作员需要通过本系统记录图书的销售数据 3. 无论是管理员还是操作员,都有一些相同的业务,如图11.1.3所示。 11.1.3 相同业务 Ø 登录:要使用本系统首先要能正常进行登录,在登录时系统会自动判断用户的权限,如果用户是管理员就进入管理员后台;如果用户是操作员就进入操作员后台。 Ø 修改密码:方便用户修改自己的登录密码。 Ø 退出:不使用本系统时可以退出并回到登录页面。 Ø JDK 1.5以上 Ø MyEclipse8.5以上 Ø Oracle 10g以上 Ø Tomcat 6以上 图11.1.4 功能模块 图11.1.4详细列出了本项目中的功能模块,下面我们对这些功能模块牵涉到的数据、业务和界面进行详细介绍。 Ø 用户登录:用户只有输入正确的登录名称和登录密码才能进入系统,如果登录失败就进行错误提示,如图11.1.5所示。管理员登录成功后进入如图11.1.6所示的后台,操作员登录成功后进入如图11.1.7所示的后台。 图11.1.5 用户登录 图11.1.6 管理员后台 图12.7 操作员后台 Ø 用户管理:单击管理员后台的【用户管理】菜单打开如图11.1.8所示的页面,该菜单有两个选项,添加用户和编辑用户。在这里可以对用户进行集中管理。 图11.1..8 用户管理 单击【添加用户】超链接打开如图11..9所示的页面,填写用户名和密码并选择权限。 图11.1.9 添加用户 Ø出版社管理和类别管理都是简单的增删改查操作,这里不再多述。 Ø 图书库存查询:管理员可以按照图书ISBN、图书名称、出版社、图书类别进行图书当前库存数量的查询,图书进货/销售查询与次类似,只是多了一个时间段条件,如图11.1.10所示。 图11.1.10图书库存查询 单击【提交】按钮后以表格的形式显示查询到的数据信息。 Ø 图书进货:单击操作员后台的【图书进货】菜单打开如图11.1.11所示的页面。填写表单并单击【提交】按钮即可完成图书进货操作。注意:出版社和图书类别下拉列表框中的选项在页面打开时就需要填充,图书销售功能与此类似。 图11.1.11 图书进货 Ø 其他功能由于比较简单,这里不再多述。 Ø 上述功能中凡是涉及到用户输入的都有进行输入验证。 根据上述需求描述和功能分析设计出以下六个表。 图11.1.12 出版社表 图11.1.13 图书类别表 图11.1.14 用户表 图11.1.15 图书库存表 图11.1.16 图书进货记录表 图11.1.17 图书销售记录表 图11.1.18 权限表 (1) 通过Oracle建库建表 (2) 在MyEclipse中创建Flex工程,指定服务器技术为J2EE,并使用BlazeDS (3) 为项目导入JDBC驱动并添加Spring,Hibernate支持 (4) 使用逆向工程生成映射文件和实体类等,并创建业务类 (5) 将Spring和Flex进行整合,并配置业务类为远程调用 (6) 开发Flex界面,并与后台通信完成功能 图书进销存项目中,后台业务部分并没有什么难度,基本上就是增删改查而已,主要的难点就集中在构建前台界面和各种效果的实现,以及与后台进行通信这部分。下面就通过初始界面部分的代码来展示其中的一些细节。在初始界面阶段主要是显示界面的基本元素,并与后台通信获取到基本权限,并在界面中据此生成基本功能的桌面菜单,包括登陆,退出等。 (1) 后台部分 Right.java package com.zzzy.flexjxc.entity; /** * Right entity. @author MyEclipse Persistence Tools */ public class Right implements java.io.Serializable { // Fields private String id; private String parent; private String righttext; private String righturl; private String source; private Integer type; // Constructors /** default constructor */ public Right() { } /** minimal constructor */ public Right(String parent, String righttext, Integer type) { this.parent = parent; this.righttext = righttext; this.type = type; } /** full constructor */ public Right(String parent, String righttext, String righturl, String source, Integer type) { this.parent = parent; this.righttext = righttext; this.righturl = righturl; this.source = source; this.type = type; } // Property accessors public String getId() { return this.id; } public void setId(String id) { this.id = id; } public String getParent() { return this.parent; } public void setParent(String parent) { this.parent = parent; } public String getRighttext() { return this.righttext; } public void setRighttext(String righttext) { this.righttext = righttext; } public String getRighturl() { return this.righturl; } public void setRighturl(String righturl) { this.righturl = righturl; } public String getSource() { return this.source; } public void setSource(String source) { this.source = source; } public Integer getType() { return this.type; } public void setType(Integer type) { this.type = type; } } RightBizImpl.java package com.zzzy.flexjxc.biz; import java.util.List; import com.zzzy.flexjxc.dao.RightDAO; public class RightBizImpl { private RightDAO rightdao; public List getBaseRight(){ return rightdao.findByType(2); } public RightDAO getRightdao() { return rightdao; } public void setRightdao(RightDAO rightdao) { this.rightdao = rightdao; } } (2) 界面部分 自定义时间控件 deskTop.DIYTimer.mxml cornerRadius="35">
import mx.controls.Alert; import mx.managers.PopUpManager; ]]>
PopUpManager.removePopUp(this); jxc.b_timer = false; ]]> 主界面 jxc.mxml xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" xmlns:new="components.*" xmlns:flexlib="http://code.google.com/p/flexlib/" creationComplete="init()" xmlns:local="*"> global { font-size: 12; } alphaTo="0.8" blurXTo="80.0" blurYTo="80.0" color="#FFFFFF"> alphaFrom="0.8" alphaTo="0" blurXFrom="80.0" blurXTo="0.0" blurYFrom="80.0" blurYTo="0.0" color="#FFFFFF">
import deskTop.DIYTimer; import deskTop.MyToolTip; import mx.collections.ArrayCollection; import mx.controls.Alert; import mx.events.ToolTipEvent; import mx.managers.PopUpManager; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; public static var theWidth:int=996; public static var theHight:int=600; [Bindable] public var thisYears:String="获取失败!"; public var timers:Timer; [Bindable] public var currentTime:String="正在获取系统时间!"; public var thisTime:Date=new Date(); //所有权限的集合 public var ops:ArrayCollection; //拼凑当前时间 private function getTime(event:TimerEvent):void { thisTime=new Date(); currentTime=""+thisTime.getHours()+":"+thisTime.getMinutes()+":"+thisTime.getSeconds(); } //隐藏开始菜单 private function hideStart(evt:MouseEvent):void { if(evt.target==this) { if(start.visible==true) start.setVisible(false); } } public function init():void { this.initRight.getBaseRight(); start.setVisible(false); theWidth= this.width; theHight= this.height; thisTime=new Date(); thisYears=thisTime.getFullYear()+"年"+(thisTime.getMonth()+1)+"月"+thisTime.getDate()+"日"; timers=new Timer(1000); timers.addEventListener(TimerEvent.TIMER,getTime); timers.start(); this.addEventListener(MouseEvent.CLICK,hideStart,false); } protected function init_faultHandler(event:FaultEvent):void { Alert.show(event.fault.toString()); } private function showLogin(event:Event):void{ } private function exit(event:Event):void{ } protected function init_resultHandler(event:ResultEvent):void { ops=event.result as ArrayCollection; var imgnum:Number = 1; for each(var v in ops){ var opimg:Image = new Image(); opimg.source=v.source; opimg.width=64; opimg.height=64; opimg.id=v.id; opimg.toolTip=v.tip; opimg.setStyle("rollOverEffect",this.imgInGlow); opimg.setStyle("rollOutEffect",this.imgOutGlow); if(v.righturl!=''){ opimg.addEventListener(MouseEvent.CLICK,this[v.righturl]); } opimg.toolTip=v.righttext; /* opimg.toolTip=" "; opimg.addEventListener(ToolTipEvent.TOOL_TIP_CREATE,createToolTip); 如果要注册自定义提示方法 一定记得opimg.toolTip=" "; */ if(imgnum<6){ this.sg1.addElement(opimg); } else{ this.sg2.addElement(opimg); } imgnum++; } } private var songFlag:Boolean = true; private function nextSong():void { if(songFlag) { song.source='assets/images/nosong_logo.png' songFlag=false; } else { song.source='assets/images/song_logo.gif' songFlag=true; } } public static var b_timer:Boolean =false; private var mdiTimer:DIYTimer; private function showTimer():void { if(b_timer==false) { mdiTimer= new DIYTimer(); PopUpManager.addPopUp(mdiTimer,this,false); mdiTimer.x = this.width - mdiTimer.width -5; mdiTimer.y = this.height-mdiTimer.height-10; b_timer=true; } } ]]> width="100%" snapDistance="10" tilePadding="10" effectsLib="flexlib.mdi.effects.effectsLib.MDILinearEffects" height="100%"> showEffect="wipeUp" hideEffect="wipeDown" titleIcon="@Embed(source='assets/images/logo_32.png')" status="进销存管理系统" backgroundAlpha="0.5" > rollOverEffect="{imgInGlow}" rollOutEffect="{imgOutGlow}" toolTip="单击这里开始" fontWeight="bold" left="0" verticalCenter="0"> rollOverEffect="{imgInGlow}" rollOutEffect="{imgOutGlow}" right="85" top="5" width="20" height="16" id="song" /> (3) 添加登录退出功能 登录和退出功能本身比较简单,主要是我们要实现登录框最小化到任务栏,和缩略图的功能,这部分代码比较复杂 首先我们要定义一个自定义控件,用来在任务栏上添加最小化窗口的图标。 ButtonTitle.Mxml label="{labelTW}" click="mw.setVisible(true)" toolTip="{labelTW}" toolTipCreate="createToolTip(event)" toolTipShow="positionToolTip(event)" rollOverEffect="{imgInGlow}" rollOutEffect="{imgOutGlow}"> alphaTo="0.8" blurXTo="100.0" blurYTo="100.0" color="#FFFFFF"> alphaFrom="0.8" alphaTo="0" blurXFrom="100.0" blurXTo="0.0" blurYFrom="100.0" blurYTo="0.0" color="#FFFFFF">
import mx.core.IUIComponent; import mx.graphics.ImageSnapshot; private function takeSnapshot(source:IBitmapDrawable,st:ShowTW):void { var imageSnap:ImageSnapshot = ImageSnapshot.captureImage(this.mw); var imageByteArray:ByteArray = imageSnap.data as ByteArray; st.load(imageByteArray); st.width=240; st.height=200; } import flexlib.mdi.containers.MDIWindow; import mx.events.ToolTipEvent; [Bindable] public var labelTW:String="获取失败!"; public var mw:MDIWindow; public function show(mw:MDIWindow):void { this.mw=mw; labelTW=mw.title; } private function createToolTip(e:ToolTipEvent):void { var st:ShowTW = new ShowTW(); e.toolTip = st; takeSnapshot(mw,st); } //将ToolTip定位在距离该组件上方(5,5)的位置 private function positionToolTip(e:ToolTipEvent):void { var pt:Point = new Point(); pt.x = 0; pt.y = 0; pt = this.localToGlobal(pt); e.toolTip.x = pt.x; e.toolTip.y = pt.y-203; } ]]> 定义一个所有弹出框的基类,程序中所有弹出例如登录框,添加出版社对话框等都会继承该类。 MyWindow.mxml xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:code="http://code.google.com/p/flexlib/" layout="absolute" width="400" height="300" fontSize="12" cornerRadius="8" backgroundColor="#FFFFFF" titleIcon="@Embed(source='../../../assets/images/dis/Sketches0.png')" backgroundAlpha="0.8" alpha="1.0" borderAlpha="0.2" fontWeight="normal" headerHeight="25" color="#297AD6" hideEffect="{irisOut}" showEffect="{irisIn}" >
import deskTop.ButtonTile; import flexlib.mdi.containers.MDICanvas; import flexlib.mdi.events.MDIWindowEvent; import mx.containers.Tile; import mx.controls.Alert; import mx.events.CloseEvent; import mx.events.FlexEvent; public var button:ButtonTile; public var mainframe:Object; public function set Mainframe(m:main):void { this.mainframe=m; } private function initlize(event:MouseEvent):void { this.unMinimize(); } public function minlize(event:MDIWindowEvent):void { event.preventDefault(); this.setVisible(false); } public function removeBT(flag:int):void { mainframe.mdiCanvas.windowManager.remove(this); mainframe.tile.removeChild(button); switch(flag){ case 1:{ main.b_login=true; this.mainframe.mdilogin=null; break; } case 2:{ main.b_addUser=true; this.mainframe.adduser=null; break; } case 3:{ main.b_editUser=true; this.mainframe.edit=null; break; } case 4:{ main.b_updateUser=true; this.mainframe.updateUser=null; break; } case 5:{ main.b_BookIn=true; this.mainframe.mdiBookIn=null; break; } case 6:{ main.b_stock=true; this.mainframe.mdiStock=null; break; } case 7:{ main.b_stocklist=true; this.mainframe.mdiStockList=null; break; } } } protected function init():void { this.button=new ButtonTile(); this.addEventListener(MDIWindowEvent.MINIMIZE,minlize); button.addEventListener(MouseEvent.CLICK,initlize); button.show(this); mainframe.tile.addChild(button); this.mainframe.start.setVisible(false); } ]]> 定义VO Userinfo.as 与后台实体类对应。 package com.oa.vo { [RemoteClass(alias="com.zzzy.flexjxc.entity.User")] public class Userinfo { public function Userinfo() { } public var userid:int; public var roleid:int; public var loginpwd:String; public var loginname:String; } } 定义登录框 LoginWindow.mxml xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" xmlns:view="com.oa.view.*" layout="absolute" width="400" height="300" title="登录" initialize="mywindow1_initializeHandler(event)" > global { font-size: 12; }
import com.oa.vo.Userinfo; import flexlib.mdi.events.MDIWindowEvent; import mx.collections.ArrayCollection; import mx.containers.Accordion; import mx.containers.HBox; import mx.containers.VBox; import mx.controls.Alert; import mx.controls.Image; import mx.events.FlexEvent; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; import mx.validators.Validator; protected function sub_clickHandler(event:MouseEvent):void { var check:Array = Validator.validateAll([v1,v2]); if(check.length!=0){ return; } var user:Userinfo = new Userinfo(); user.loginname=this.uname.text; user.loginpwd=this.pwd.text; this.mainframe.userremoting.login(user); } protected function button1_clickHandler(event:MouseEvent):void { this.pwd.text=""; this.uname.text=""; } public function alert(event:MDIWindowEvent):void { event.preventDefault(); removeBT(1); } protected function mywindow1_initializeHandler(event:FlexEvent):void { super.init(); main.b_login=false; this.addEventListener(MDIWindowEvent.CLOSE,alert); } ]]> 在主界面jxc.mxml添加远程调用对象的定义和showLogin ,exit等方法修改后的代码如下。 xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" xmlns:new="components.*" xmlns:flexlib="http://code.google.com/p/flexlib/" creationComplete="init()" xmlns:local="*"> global { font-size: 12; } name="login" result="loginremoting_resultHandler(event)" fault="loginremoting_faultHandler(event)" /> alphaTo="0.8" blurXTo="80.0" blurYTo="80.0" color="#FFFFFF"> alphaFrom="0.8" alphaTo="0" blurXFrom="80.0" blurXTo="0.0" blurYFrom="80.0" blurYTo="0.0" color="#FFFFFF">
import com.oa.view.LoginWindow; import deskTop.DIYTimer; import deskTop.MyToolTip; import mx.collections.ArrayCollection; import mx.containers.HBox; import mx.controls.Alert; import mx.controls.Button; import mx.events.ToolTipEvent; import mx.managers.PopUpManager; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; public static var theWidth:int=996; public static var theHight:int=600; [Bindable] public var thisYears:String="获取失败!"; public var timers:Timer; [Bindable] public var currentTime:String="正在获取系统时间!"; public var thisTime:Date=new Date(); //所有权限的集合 public var ops:ArrayCollection; //建立tooltip组件 private function createToolTip(e:ToolTipEvent):void { var tip:MyToolTip = new MyToolTip(); e.toolTip = tip; } //拼凑当前时间 private function getTime(event:TimerEvent):void { thisTime=new Date(); currentTime=""+thisTime.getHours()+":"+thisTime.getMinutes()+":"+thisTime.getSeconds(); } //隐藏开始菜单 private function hideStart(evt:MouseEvent):void { if(evt.target==this) { if(start.visible==true) start.setVisible(false); } } public function init():void { this.initRight.getBaseRight(); start.setVisible(false); theWidth= this.width; theHight= this.height; thisTime=new Date(); thisYears=thisTime.getFullYear()+"年"+(thisTime.getMonth()+1)+"月"+thisTime.getDate()+"日"; timers=new Timer(1000); timers.addEventListener(TimerEvent.TIMER,getTime); timers.start(); this.addEventListener(MouseEvent.CLICK,hideStart,false); } protected function init_faultHandler(event:FaultEvent):void { Alert.show(event.fault.toString()); } protected function init_resultHandler(event:ResultEvent):void { ops=event.result as ArrayCollection; var imgnum:Number = 1; for each(var v in ops){ var opimg:Image = new Image(); opimg.source=v.source; opimg.width=64; opimg.height=64; opimg.id=v.id; opimg.toolTip=v.tip; opimg.setStyle("rollOverEffect",this.imgInGlow); opimg.setStyle("rollOutEffect",this.imgOutGlow); if(v.righturl!=''){ opimg.addEventListener(MouseEvent.CLICK,this[v.righturl]); } opimg.toolTip=v.righttext; /* opimg.toolTip=" "; opimg.addEventListener(ToolTipEvent.TOOL_TIP_CREATE,createToolTip); 如果要注册自定义提示方法 一定记得opimg.toolTip=" "; */ if(imgnum<6){ this.sg1.addElement(opimg); } else{ this.sg2.addElement(opimg); } imgnum++; } } private var songFlag:Boolean = true; private function nextSong():void { if(songFlag) { song.source='assets/images/nosong_logo.png' songFlag=false; } else { song.source='assets/images/song_logo.gif' songFlag=true; } } public static var b_timer:Boolean =false; private var mdiTimer:DIYTimer; private function showTimer():void { if(b_timer==false) { mdiTimer= new DIYTimer(); PopUpManager.addPopUp(mdiTimer,this,false); mdiTimer.x = this.width - mdiTimer.width -5; mdiTimer.y = this.height-mdiTimer.height-10; b_timer=true; } } public var loginstate:Boolean = false; public static var username:String; public var mdilogin:LoginWindow; public static var b_login:Boolean=true; private function showLogin(event:Event):void{ if(!this.loginstate){ if(b_login) { mdilogin= new LoginWindow(); mdilogin.mainframe=this; mdiCanvas.windowManager.add(mdilogin); mdilogin.x = mdiCanvas.width/2-mdilogin.width/2; mdilogin.y = mdiCanvas.height /2-mdilogin.height/2; } else { mdilogin.setVisible(true); } } } private function exit(event:Event):void{ if(this.loginstate){ Alert.show("确定退出?","进销存",Alert.YES|Alert.NO,null,function(evt){ if(evt.detail==Alert.YES){ loginstate=false; startCan.removeAllChildren(); startButton.visible=false; } }); } } protected function loginremoting_resultHandler(event:ResultEvent):void { if(event.result!=null){ loginstate=true; initMenu(event); username = mdilogin.uname.text; this.mdilogin.removeBT(1); } else{ Alert.show('用户名密码错误'); } } public function initMenu(event:ResultEvent):void{ var sys_rights:ArrayCollection = event.result as ArrayCollection; for each(var v in sys_rights){ if(v.parent=="root_menu"){ var vb1 :VBox= new VBox; vb1.label=v.righttext; vb1.id=v.id; for each( var sr in sys_rights){ if(sr.parent==v.id){ var hb:HBox = new HBox(); var img:Image = new Image(); var but:Button = new Button(); img.width=32; img.height=32; img.source=sr.source; but.label=sr.righttext; but.addEventListener(MouseEvent.CLICK,this[sr.righturl]); hb.addChild(img); hb.addChild(but); vb1.addChild(hb); } } startCan.addChild(vb1); startButton.visible=true; } } } protected function loginremoting_faultHandler(event:FaultEvent):void { Alert.show(event.fault.toString()); } private function showStart():void { if(start.visible==true){ start.setVisible(false); }else{ start.setVisible(true); } } public function addUser(event:Event):void{} public function editUser(event:Event):void{} public function addPublisher(event:Event):void{} public function editPublisher(event:Event):void{} public function addCategroy(event:Event):void{} public function editCategory(event:Event):void{} public function bookInFind(event:Event):void{} public function bookOutFind(event:Event):void{} public function bookStoreFind(event:Event):void{} ]]> width="100%" snapDistance="10" tilePadding="10" effectsLib="flexlib.mdi.effects.effectsLib.MDILinearEffects" height="100%"> showEffect="wipeUp" hideEffect="wipeDown" titleIcon="@Embed(source='assets/images/logo_32.png')" status="进销存管理系统" backgroundAlpha="0.5" > rollOverEffect="{imgInGlow}" rollOutEffect="{imgOutGlow}" toolTip="单击这里开始" fontWeight="bold" left="0" verticalCenter="0"> rollOverEffect="{imgInGlow}" rollOutEffect="{imgOutGlow}" right="85" top="5" width="20" height="16" id="song" /> 运行程序效果如图11.1.19 11.1.20 所示 图11.1.19 登录界面 图11.1.20 登录后生成功能菜单 其他的功能则与此大致相同,在这里就不再详述了,另外,在进行图书进货时,首先要判断新采购的图书在数据库中是否存在,如果存在则只需更新一下库存量即可,然后需要向图书进货表中插入一条进货记录;如果不存在则需要向库存表中插入一条新的图书信息并同时向图书进货表中插入一条进货记录。 在进行图书销售时,除了要向图书销售表中插入一条销售记录外,还需要更新库存表中的库存量。图书销售和图书进货的数据库操作相对复杂,建议通过存储过程并结合事务来实现。 阶段一:分析和讲解需求(50分钟) Ø 教员在教师机上运行参考项目并讲解需求 Ø 学员分析并理解需求 阶段二:学员实现数据库(30分钟) 阶段三:学员实现业务功能(300分钟) 阶段四:运行并测试(20分钟)1.8 Flex打印
1.8.1 原生打印
1.8.2 宿主打印
1.8.3 外部打印
1.8.4 为什么使用Flex来打印
1.8.5 使用PrintJob打印
1.8.6 深入了解PrintJob
实训任务1:使用RSI技术减少SWF文件体积
实训任务2:检测内存泄露
第2章 复习指导
2.1 知识总结
2.1.1 阶段核心技能目标
2.1.2 知识体系总结
2.2 综合实训
2.2.1 任务描述
2.2.2 系统功能
2.2.3 实现步骤
第3章 项目案例:图书进销存系统
3.1 训练技能点
3.2 需求说明
3.2.1 需求概述
3.2.2 系统用例图
3.2.3 开发环境
3.3 设计与思路分析
3.3.1 功能分析
3.3.2 数据库设计
3.4 实现步骤
3.5 关键代码
3.6 时间分配