使用Flex开发时空线形图实例详解
本文根据项目实例,详解如何使用Flex技术开发时空线形图。大象目前从事的是工程管理方面的开发,在土木工程建设行业有一种时空线形图,它是用实线,虚线,图形等方式,来描述出工程进度计划中的数据信息的方法。呈现出来的一般是报表形式,多采用Excel来实现。
对于客户来说,手动绘制时空线形图是比较麻烦的。因为要画线,画图片。如果知道甘特图或使用过Microsoft Project的朋友应该很快就能明白,对于一个工程的进度计划来说,它包含有很多的任务项,而这些任务项都需要在线形图上表示出来,因此手动做的话工作量是很大的。现在有了系统,就可以把这一任务交给计算机来完成,客户只需要输入数据即可。
系统中的时空线形图最早是采用JavaScript脚本编写的,后来由我改成了Flex版。先说下实现的思路。因为只是显示和打印,没有其它的要与用户交互的需求。所以后台数据是直接以XML流文件的形式,通过异步加载获得。然后再利用Flex强大的XML解析能力处理这些数据,最后就是根据这些数据使用Flash的API画图。呵呵,是不是很简单? 先看几张截图大致了解下。
本文的AS代码是基于ActionScript 3.0,下面进行详细说明。
1、获得数据
我采用URLLoader.load方法实现。通过传递请求地址,注册监听器,然后在回调函数中就可以获得XML数据了。当然你也可以采用RemoteObject方式。
public function LoadXML():void {
var loader:URLLoader = new URLLoader();
loader.load(new URLRequest("/baseAction.do?method=ajaxData")); //这个地址可以直接是一个xml文件
loader.addEventListener(Event.COMPLETE, handleComplete);
}
private function handleComplete(event:Event):void {
var xml:XML = new XML(event.target.data);
……
}
这是个异步加载过程,当数据在后台获取完成,并转换为XML格式向前台发送后,注册的Event.COMPLETE事件类型就会触发回调函数handleComplete,然后我们就能得到数据并将它转型为XML对象。var loader:URLLoader = new URLLoader();
loader.load(new URLRequest("/baseAction.do?method=ajaxData")); //这个地址可以直接是一个xml文件
loader.addEventListener(Event.COMPLETE, handleComplete);
}
private function handleComplete(event:Event):void {
var xml:XML = new XML(event.target.data);
……
}
2、布局
我们现在有了数据,下一步就是布局。本项目中的时空线形图上有这些基本信息:序号、工程名称、工期、标段划分和图例。这其中的工期是一个时间跨度,对应进度计划的最早开始日期和最晚结束日期。我们取的是年+月的形式来表示。而图例则是对线形图中的任务项列出对应的说明。大象当时为了方便,使用Flex的Grid、GridRow、GridItem来生成布局框架,要是考虑性能方面的原因,应该换成其它方法实现,因为这种方式速度会有点慢,好在没有其它的交互情况。那么先让大象把做法讲完,性能调优的话题已经超出本文的范围了。
这里就是要不停的生成单元格,由单元格组成行,再由多行组成Grid,这跟html中的table、tr、td是一个道理。
var grid:Grid = new Grid(); //Grid只需要一个
var orderRow:GridRow = new GridRow(); //序号行
var projectRow:GridRow = new GridRow(); //工程名称行
……
创建了行,我们同时还要向里面添加单元格,也即GridItem。这时就要根据得到的数据来处理了,是一个循环。var orderRow:GridRow = new GridRow(); //序号行
var projectRow:GridRow = new GridRow(); //工程名称行
……
var spaces:* = xml.spaces.space; //可以直接用.符号来获取XML文件的节点
for each(var space:Object in spaces){
orderRow.addChild(createTitleColItem(space.order,space.id)); //循环加载序号
projectRow.addChild(createVerTitleColItem(space.projectName); //循环加载工程名称
}
createTitleColItem和createVerTitleColItem里面封装了单元格的创建,写成方法方便调用。for each(var space:Object in spaces){
orderRow.addChild(createTitleColItem(space.order,space.id)); //循环加载序号
projectRow.addChild(createVerTitleColItem(space.projectName); //循环加载工程名称
}
工期的日期时间和标段划分也是同理,对于工期来说,数据都显示在最左侧一栏,其余的单元格都为空,这样做是当完成框架布局后,在这空白的区域绘图。
好了,到这里布局完成,将grid加入到上一层容器,我用的是Canvas并且将它的宽度和高度设置和grid一样,然后在Canvas外面再加上一层容器,比如VBox。然后给VBox一个合适的宽高(比较好的做法是取当前显示器的宽高,否则页面显示会有点问题)。让它比Canvas要小(一般来讲,Canvas会比VBox大很多),这时就会出现滚动条,拖动滚动条进行查看。
3、计算坐标
在这个应用当中,最重要的应该就是计算坐标了,不管是画线还是画图,都需要坐标来定位。那到底怎么计算坐标呢?其实也不难,通过ID与日期就能把它们算出来。
①、X坐标
注意在设置第一行序号的时候,createTitleColItem(space.order,space.id)这个方法的第二个参数就是一个ID,每个序号项的GridItem的id属性都会保存space.id值,而任务数据项中也有一个这样的ID,所以就可以通过查找ID来计算出它的水平位置来。
private
function
getX(position:String,orderChilds:Array):int{
var x:int = title_width; // 最左侧的标题列宽
for ( var i:int=1;i<orderChilds.length-1;i++){
if ((child[i] as GridItem).id==position) break ;
x += col_width; // 普通列宽
}
return x;
}
position是任务项ID,orderChilds 是orderRow.getChildren()得到第一行的所有列返回的数组。任务项中会有两个position,起始和终止。通过这两个值可以求出x1与x2坐标。var x:int = title_width; // 最左侧的标题列宽
for ( var i:int=1;i<orderChilds.length-1;i++){
if ((child[i] as GridItem).id==position) break ;
x += col_width; // 普通列宽
}
return x;
}
②、Y坐标
y坐标的是通过任务项包含的开始日期和结束日期与整个计划的最晚日期进行比较计算出来的。
private
function
getY(date:Date):int{
var rowSize:int = 5; // 行数,序号和工程名称占据了五行
rowSize += Math.abs(dateDiff( "m" ,date, this .endDate));
var day:int = date.getDate();
day = day > 30 ? 0 : 30-day;
return rowSize*title_height+Math.round((day%30/30)*title _height);
}
在应用里我将行高都设为相同的值,工程名称这行是四倍的行高。生成的布局框架里,日期时间是倒排序,它们之间的间隔是一个月。dateDiff就是计算当前日期与最晚日期(this.endDate)之间相隔多少个月,而每相隔一个月,就是一行。因此通过这种方式计算出y坐标。var rowSize:int = 5; // 行数,序号和工程名称占据了五行
rowSize += Math.abs(dateDiff( "m" ,date, this .endDate));
var day:int = date.getDate();
day = day > 30 ? 0 : 30-day;
return rowSize*title_height+Math.round((day%30/30)*title _height);
}
我补充说明一下,因为最终展现结果的是Canvas容器,所以坐标的计算都是相对Canvas的,因此这个x和y的坐标是相对Canvas内的绝对坐标。
4、绘图
绘图分为画线和画图形。如果是画线,则可以直接利用Flash的graphics来画图。画图形要麻烦点,得先取得图片,再用图片来画图。不管是画线还是画图形,都是和数据有关的。数据中会有一个类型,是说明到底这个任务项是线还是图。如果是线,会有一个颜色值,为了防止没有设置颜色值,我们应该给定一个默认的颜色值。如果是图,会有个图片的相对地址,录数据的时候会上传图片,不过为了防止没有上传图片,我们应该准备一个默认图片。
①、画线
var line:UIComponent = new UIComponent();
line.graphics.lineStyle(thickNum,color,1);//设置粗细、颜色、透明度
line.graphics.moveTo(x1,y1); //从某个坐标开始
line.graphics.lineTo(x2,y2); //画到某个坐标
②、画图line.graphics.lineStyle(thickNum,color,1);//设置粗细、颜色、透明度
line.graphics.moveTo(x1,y1); //从某个坐标开始
line.graphics.lineTo(x2,y2); //画到某个坐标
public
function
load (url:String,callback:Function,options:*):void{
var loader:Loader = new Loader();
loader.load( new URLRequest(encodeURI(url))); // 图片地址
loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
function(e:Event):void {callback(e.currentTarget.content,options);});
}
load(url,paintRect,{"x1":x1,"y1":y1,"x2":x2,"y2":y2});
public function paintRect(source:*,options:*):void{
var shape:UIComponent = new UIComponent();
var rect_width:int = Math.abs(options.x2–options.x1); //图形宽度
var rect_height:int = Math.abs(options.y2–options.y1); //图形高度
var bitmap:BitmapData = new BitmapData(source.width, source.height); //用图片源的宽高定义一个位图对象
bitmap.draw(source); //将图片源在位图上绘制出来
shape.graphics.beginBitmapFill(bitmap); //用位图填充绘图区域
shape.graphics.drawRect(options.x1, options.x2,rect_width,rect_height); //定义矩形绘图区域,这块区域将用图片填充
shape.graphics.endFill(); //应用填充
}
load方法就是用来获取图片。注册监听器,这里我们还是使用Event.COMPLETE事件类型,当图片加载完成后,会调用回调函数,并将参数也一起传给回调函数。画图工作是在回调函数中进行。另外有一点要说明的是,Loader类是用来加载SWF文件或图像(JPG、PNG 或 GIF)文件,而URLLoader 类则是加载文本或二进制数据,请大家使用的时候要注意这点区别。var loader:Loader = new Loader();
loader.load( new URLRequest(encodeURI(url))); // 图片地址
loader.contentLoaderInfo.addEventListener(Event.COMPLETE,
function(e:Event):void {callback(e.currentTarget.content,options);});
}
load(url,paintRect,{"x1":x1,"y1":y1,"x2":x2,"y2":y2});
public function paintRect(source:*,options:*):void{
var shape:UIComponent = new UIComponent();
var rect_width:int = Math.abs(options.x2–options.x1); //图形宽度
var rect_height:int = Math.abs(options.y2–options.y1); //图形高度
var bitmap:BitmapData = new BitmapData(source.width, source.height); //用图片源的宽高定义一个位图对象
bitmap.draw(source); //将图片源在位图上绘制出来
shape.graphics.beginBitmapFill(bitmap); //用位图填充绘图区域
shape.graphics.drawRect(options.x1, options.x2,rect_width,rect_height); //定义矩形绘图区域,这块区域将用图片填充
shape.graphics.endFill(); //应用填充
}
另外这里不管是画线还是画图都用的是UIComponent类,这是因为它有一个toolTip属性,当鼠标移至图或线上时,会显示你设置的信息。如果用Sprite则没有这属性。UIComponent是Flex定义的,而Sprite则是Flash本身的。
画图例的方法是一样的,画好的图需要加入到父容器中显示出来。到此整个做法就讲完了,其实也不是很复杂,关键是要理清思路。因为涉及到商业项目和保密原则,请原谅大象不能将此源码拿出来给大家分享,不过我已经提供了部分代码,而且这些代码是整个应用中关键点。只要大家能从中得到一点帮助,那么我就很满足了。
本文为菠萝大象原创,如要转载请注明出处。