陋室铭
永远也不要停下学习的脚步(大道至简至易)

提要:

    VML是Microsoft IE 5.0及其后续版本内嵌的矢量图形实现,虽然MS也提倡日后使用SVG,但是作为一个IE内嵌的标记语言,在某些时候的应用还是比较方便的。本文通过完整的描述一个统计饼图的建立过程,来展现VML在Web方面的魅力。文章通过实现一个JavaScript类,读者能够完整地看到整个饼图的制作过程。



    VML(Vector Markup Language)自从问世以来似乎都处于默默无闻的地步,直到现在为止,情况依然没有改变。其实细心的一点你就可以发现,在Web方面,MS得很多产品还是内置使用了,最典型的就是Office的自选图形,将word或者ppt文档存储成html,如果文档内部使用了自选图形,你就可以看到那些图形使用过VML来表述的,另外一个典型的应用就是Visio的导出到web这个工具。



    前段时间,James在CSDN发表了使用ASP生成统计图的例子,我也认真拜读了其中的代码,并且也针对一些问题找他请教了,James的颜色感觉非常好,可惜我没有那样的功底,因此在代码实现中我就使用了随机颜色来实现,可能整体的界面看起来会稍微差劲一点。



    统计图比较典型的是饼图,柱状图,曲线图,本文着重讲解Pie的制作过程,文章采用了JavaScript实现了一个类,如果相关Javascript面向对象不是特别了解的,可以参考我另外的文章《面向对象的Javascript编程》和《再论面向对象的JavaScript编程》。

     

      暂且不考虑如何实现,我们先看看代码最终的使用如何。



      objPie=new VMLPie("600px","450px","人口统计图"); //初始化宽度,高度,标题
        objPie.BorderWidth=3; //图表边框
        objPie.BorderColor="blue"; //图表边框颜色
        objPie.Width="800px"; //定义图表宽度
        objPie.Height="600px"; //定义图表高度       
        objPie.backgroundColor="#ffffff"; //定义背景颜色
        objPie.Shadow=true; //是否需要阴影 true为是 false为不要阴影
        //添加图表数据
        //顺序为名称,值,描述
        objPie.AddData("北京",50,"北京的人口");
        objPie.AddData("上海",52,"上海的固定人口");
        objPie.AddData("天津",30,"天津的外地人口");
        objPie.AddData("西安",58,"西安城市人口");
        objPie.AddData("武汉",30,"武汉的外地人口");
        objPie.AddData("重庆",58,"重庆城市人口");
        result.innerHTML=objPie.Draw(); //生成VML数据


      这段代码就是最终的调用,我封装了一个VMLPie的JavaScript类,而本文的重点也就是详细地描述类的具体实现过程,另外要使用VML必须作如下的声明。

1.  HTML Tag的名字空间声明

<html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">

2.  <head>之间的style声明

                <STYLE>
                          v\:* { BEHAVIOR: url(#default#VML) }
                          o\:* { BEHAVIOR: url(#default#VML) }
                          .shape { BEHAVIOR: url(#default#VML) }
                </STYLE>
完成了上述工作之后,代码就能够完整的工作了。



现在我们开始讲述这个VMLPie的实现了,首先,我将那些实现的函数作一个简单的说明。

//VMLPic主函数,提供创建一个VMLPie实例
function VMLPie(pWidth,pHeight,pCaption,pContainer){}
//开始画图,将图形画到指定的容器上面
VMLPie.prototype.Draw=function(){}
//画饼图的各个块
VMLPie.prototype.CreatePie=function(){}
//接口功能:
//        放大或者缩小图形
//参数说明:
//        iValue:放大的或者缩小的倍数,1为原图大小,0.5原图的50%,2为原图的
//            两倍,以此类推
VMLPie.prototype.Zoom=function (iValue){}
//接口功能:
//        添加饼图数据
//参数说明:
//        sName:数据标签名称
//        sValue:数据值
//        sTooltipText:数据描述
VMLPie.prototype.AddData=function(sName,sValue,sTooltipText){}
//接口功能:
//        清除所有数据
VMLPie.prototype.Clear=function(){}
//以下四个函数是扩展使用,就是提供一些VML的交互
function HoverPie(el){}
function RestorePie(el){}
function LegendMouseOverEvent(){}
function LegendMouseOutEvent(){}

看完这些函数的说明,我想读者对于整个类的结构有一个大致的想法思路了吧,可是对于VML的制作,可能还是没有一个很清晰的思路吧,那么,下面我就着重介绍几个VML元素,我只是大致的介绍一些用法,具体的在 美洲豹的 《VML中文教程》有比较详细地介绍,如果有兴趣的话可以去参考参考。



1.    V:Group

作为VML其它元素的容器,其属性coordsize定义其坐标大小,内部的元素的位置都只是相对于group元素所定义的coordsize,假设coordsize定义为21600,21600,就是定义了21600 * 21600的画布,如果内部有一个v:shape或者其它元素, shape.style.left=”2160px”,其实际位置只是在v:group的1/10宽度的位置。

2.    V:Rect

定义一个矩形元素,fillcolor表示填充的背景颜色,stokecolor表示边框颜色,strokeweight表示边框宽度

3.    V:Shape

VML提供的默认形状元素,通过定义path可以定义出需要的任何形状,至于path的用法,可以参考w3c的文档。

4.    V:Fill

作为shape的子元素,用来设置shape的背景效果,通过type来设置填充的方法,具体用法如下

1)      solid:实心填充,通过color设置填充颜色

2)      gradient:线状渐变,这个时候需要color和color2这两个参数来设置渐变的开始颜色和结束颜色,Angle则设置渐变方向。

3)      gradientradial:圆心渐变,其他的使用方法和gradient类似

4)      tile:使用图片平铺,src设置图片

5)      pattern:使用图片作为一个图章填充模式

6)      frame:使用图片拉神填充



        另外Opacity则用来设置透明度

5.    V:Shadow

设置shape是否需要阴影,主要使用如下参数

ON:设置是否启用阴影

Color:阴影的颜色

Offset:阴影的偏离位置

6.    V:TextBox

定义shape的文字区域



以上的这些元素是我在制作VMLPie需要使用到的,所以做了一个简单的介绍,具体的可以参考豹子的《VML中文教程》,另外MSDN里头的Vector Markup Language(VML)的SDK Document是一个非常好的参考资料。W3c的note比较抽象,在做一些比较深入的开发的时候,也是一个非常重要的参考。

做完了一系列的准备工作,那么现在应该开始真正介绍如何绘制Pie了吧。首先看看下面的一个示意图,清楚地描述了VMLPie的元素结构。


为了更加清楚的显示构成VML的文本结构,我使用XML的格式给出了如下的方式。

<v:group>
    <!--设置饼图的背景及其标题 -->
  <v:rect>
          <v:textbox>VML饼图</v:textbox>
  </v:rect>
    <!--设置饼图的各个块-->
  <v:shape path="...">     
  </v:shape>
  <!--图例元素集合 -->
  <v:group>
          <!--设置图例的颜色 -->
          <v:rect fillcolor="green"></v:rect>
          <v:rect>
                    <!-- 设置图例文字-->
                    <v:textbox>重庆(50)</v:textbox>
          </v:rect>
  </v:group>
</v:group>
完成了制图的基本思路之后,剩下的就是通过DOM逐步完成建立过程了,为了简单起见,我们安装函数的功能进行逐步分析。



1.            VMLPie

      this.Container=pContainer;
        this.Width= pWidth || "400px";
        this.Height=pHeight || "250px";
        this.Caption = pCaption || "VML Chart";
        this.backgroundColor="";
        this.Shadow=false;
        this.BorderWidth=0;
        this.BorderColor=null;
        this.all=new Array();
        this.id=document.uniqueID;
        this.RandColor=function(){                 
                return "rgb("+ parseInt( Math.random() * 255) +"," +parseInt( Math.random() * 255) +"," +parseInt( Math.random() * 255)+")";
        }
        this.VMLObject=null;
        this.LegendObject=null;


这个函数只是简单的初始化了一些变量,将class可以使用的一些属性在这里做了声明。RandColor函数提供了生成随机颜色的作用,这个也就是我在前文提到的,对于饼图的各个块的颜色,我都采用随机颜色,这样带来的一个问题就是会出现有些时候两个颜色比较接近,如果随能够给我提供几个基础的颜色列表,我将感激不尽。

2.            VMLPie.prototype.AddData

添加图表数据,在这里我采用了prototype滞后加载的方式实现,如果读者认为这样不够清晰,可以修改成this.RandColor那样的内置形式。

实现代码如下:

VMLPie.prototype.AddData=function(sName,sValue,sTooltipText){
    var oData=new Object();
    oData.Name=sName;
    oData.Value=sValue;
    oData.TooltipText=sTooltipText;
    var iCount=this.all.length;
    this.all[iCount]=oData;
}
这里使用一个Object来存储每一个数据项,然后放到this.all这个数组中。

3.            VMLPie.prototype.Draw

整个类实现最关键的函数,也就是在这个函数和后续的CreatePie函数中,一步一步地实现了图形的绘制工作。

//画外框
var o=document.createElement("v:group");
o.id=this.id;
o.style.width=this.Width;
o.style.height=this.Height;
o.coordsize="21600,21600";
//添加一个背景层
var vRect=document.createElement("v:rect");
vRect.style.width="21600px"
vRect.style.height="21600px"
o.appendChild(vRect);
//添加标题   
var vCaption=document.createElement("v:textbox");
vCaption.style.fontSize="24px";               
vCaption.style.height="24px"
vCaption.preSize="24";
vCaption.style.fontWeight="bold";
vCaption.innerHTML=this.Caption;
vCaption.style.textAlign="center";

vRect.appendChild(vCaption);
//设置边框大小
if(this.BorderWidth){
    vRect.strokeweight=this.BorderWidth;
}
//设置边框颜色
if(this.BorderColor){
    vRect.strokecolor=this.BorderColor;
}
//设置背景颜色
if(this.backgroundColor){             
    vRect.fillcolor=this.backgroundColor;
}
//设置是否出现阴影
if(this.Shadow){
    var vShadow=document.createElement("v:shadow");
    vShadow.on="t";
    vShadow.type="single";
    vShadow.color="graytext";
    vShadow.offset="4px,4px";
    vRect.appendChild(vShadow);
}
this.VMLObject=o;



完成上述工作之后,已经构建出了一个canvas(画布),完成了图形外框及其标题的制作,剩下的也就是最最重要的工作调用CreatePie来画出各个块,并且作出图例。
4.            VMLPie.prototype.CreatePie

CreatePie提供了一个参数,也就是饼图制作的容器,我们通过Draw的上述代码已经建立了一个V:Group元素,这个也就是饼图绘制的容器了。

var mX=Math.pow(2,16) * 360;
//这个参数是划图形的关键
//AE x y width height startangle endangle
//x y表示圆心位置
//width height形状的大小
//startangle endangle的计算方法如下
// 2^16 * 度数       
var vTotal=0;
var startAngle=0;
var endAngle=0;
var pieAngle=0;
var prePieAngle=0;           

//计算数据的总和
for(i=0;i<this.all.length;i++){
    vTotal+=this.all[i].Value;
}
//建立图例容器
//这里子元素的left,top或者width都是针对于容器设置的坐标系统而言的
//例如
//图表容器我设置了coordsize为 21600,21600,那么objLengendGroup的位置都是相对于这个坐标系统而言的
//和实际图形显示的大小没有直接关系
var objLegendGroup=document.createElement("v:group");
with(objLegendGroup){
    style.left="17000px";
    style.top="4000px";
    style.width="4000px";
    style.height=1400 * this.all.length +"px";
    coordsize="21600,21600";                   
}
//做图例的背景填充并且设置阴影
var objLegendRect=document.createElement("v:rect");
objLegendRect.fillcolor=" #EBF1F9";
objLegendRect.strokeweight=1;
with(objLegendRect){         
    //设置为21600,21600,就是保证完全覆盖group客户区
    style.width="21600px";
    style.height="21600px";                   
}           
//对于图例加入阴影
var vShadow=document.createElement("v:shadow");
vShadow.on="t";               
vShadow.type="single";
vShadow.color="graytext";
vShadow.offset="4px,4px";             
objLegendRect.appendChild(vShadow);

//将其放到图例的容器中
objLegendGroup.appendChild(objLegendRect);

this.LegendObject=objLegendGroup;
vGroup.appendChild(objLegendGroup);



这个时候,我们已经完成了各个区域位置的绘制,通过如上的代码,我绘制了一个LegendGroup,将其作为图例的显示位置,另外主的V:group就作为pie的绘制容器,如果出于规范的考虑,也应该将Pie的各个shape放到一个group中,那样在日后的编程控制中会更加方便一点。

下面的这段代码也就是我要讲述的,因为代码比较关键,除了给出代码,我还着重的说明各个语句的作用。

for(i=0;i<this.all.length;i++){ //顺序的划出各个饼图
        var vPieEl=document.createElement("v:shape");
        var vPieId=document.uniqueID;
        vPieEl.style.width="15000px";
        vPieEl.style.height="14000px";
        vPieEl.style.top="4000px";
        vPieEl.style.left="1000px";
        vPieEl.adj="0,0";
        vPieEl.coordsize="1500,1400";
        vPieEl.strokecolor="white";                       
        vPieEl.id=vPieId;
        vPieEl.style.zIndex=1;
        vPieEl.onmouseover="HoverPie(this)";
        vPieEl.onmouseout="RestorePie(this)";             
        pieAngle= this.all[i].Value / vTotal;
        startAngle+=prePieAngle;
        prePieAngle=pieAngle;
        endAngle=pieAngle;
        vPieEl.path="M 750 700 AE 750 700 750 700 " + parseInt(mX * startAngle) +" " + parseInt(mX * endAngle) +" xe";
        vPieEl.title=this.all[i].Name +"\n所占比例:"+ endAngle * 100 +"%\n详细描述:" +this.all[i].TooltipText;
        vPieEl._scale=parseInt( 360 * (startAngle + endAngle /2));
                                 
        var objFill=document.createElement("v:fill");
        objFill.rotate="t";                       
        objFill.focus="100%";
        objFill.type="gradient";                   
        objFill.angle=parseInt( 360 * (startAngle + endAngle /2)); 
       
        var objTextbox=document.createElement("v:textbox");
        objTextbox.innerHTML=this.all[i].Name +":" + this.all[i].Value;
        objTextbox.inset="5px 5px 5px 5px";
        objTextbox.style.width="100px";
        objTextbox.style.height="20px";   
       
        var vColor=this.RandColor();
        vPieEl.fillcolor=vColor; //设置颜色
        //开始画图例     
        p.innerText=vPieEl.outerHTML;
        var colorTip=document.createElement("v:rect");
       
        var iHeight=parseInt(21600 / (this.all.length * 2));
        var iWidth=parseInt(iHeight * parseInt(objLegendGroup.style.height) /parseInt(objLegendGroup.style.width) /1.5 );
       
        colorTip.style.height= iHeight;           
        colorTip.style.width=iWidth;     
        colorTip.style.top=iHeight * i * 2 + parseInt(iHeight /2);                   
        colorTip.style.left=parseInt(iWidth /2);
        colorTip.fillcolor=vColor;
        colorTip.element=vPieId;
        //colorTip.attachEvent("onmouse",LegendMouseOverEvent);
        colorTip.onmouseover="LegendMouseOverEvent()";
        colorTip.onmouseout="LegendMouseOutEvent()";
       
        var textTip=document.createElement("v:rect");
        textTip.style.top=parseInt(colorTip.style.top)- 500;
        textTip.style.left= iWidth * 2;           
        textTip.innerHTML="<v:textbox style=\"font-size:12px;font-weight:bold\">" + this.all[i].Name +"("+ this.all[i].Value+")</v:textbox>";
       
        objLegendGroup.appendChild(colorTip);
        objLegendGroup.appendChild(textTip);
       
        vGroup.appendChild(vPieEl);       
}
我们现在就开始来看如上代码的实现。



1.                          首先建立一个v:shape,left,top,width,height分别设置为1000px,4000px,15000px;14000px;这个的数字是我根据大致的位置确定的。

2.                          设置每个shape的id,我采用了document.uniqueID,就是用DOM的方法提供的随机id,本来我没有打算设置这个ID,但是后来考虑到和legend的交互,所以就设置了每个shape的ID.

3.                          设置shape的onmouseover,onmouseout事件,开始的时候我是采用attachEvent的方式来实现的,后来发现无法起作用(到现在我也没有找到原因),求出每个数据所占的比例。

4.                          设置Path,这个也就是比较令人难以看懂的部分了,我首先求出了startAngle和endAngle,startAngle的意义是这样的,假设有3个数,0.2,0.2,0.4,对于第二项来说,startAngle应该是 0.25,endAngle是0.25,对于第三项来说,startAngle是0.5,endAngle是0.5,总而言之,startAngle可以表示为前面数据所占的比例,engAngle表示当前数据所占的比例。Path有很多指令,对于其他指令,我就不多作解释,而这里使用的是M 750 700 AE 750 700 750 700 start end 。对于shape我重新定义了coordsize=”1500,1400”,m 750,700则表示移动到 750,700,就是移动到shape定义的中心,AE用来画曲线,总共有6个参数,w3c的note描述如下center (x,y) size(w,h) start-angle, end-angle,前面四个参数不难理解,剩下的两个参数我不是特别明白意思,但是在国外的一篇文章看到其中的算法如下应该是 2^16 * 度数,对应于程序,就应该是startAngle * 2^16 * 360,因为我的startAngle是比例。

5.                          通过设置fillcolor来设置饼图块的颜色,同时设置title提供一些提示信息.

6.                          对于图例开始着色,同时添加图例的文字说明.

7.                          因为所有的坐标系统都是相对的,为了保证美观,对于objLegendGroup我通过1400 * this.all.length +"px"来求得,而对于图例的colortip,高度和宽度就是动态计算的。

到目前为止,通过程序已经建立了一个基于VML的Pie,剩下的就是输出的问题了,前文我也提到过,我通过DOM方式建立的VML在显示方面没有任何问题,可是在动态交互方面,似乎不起作用,obj.VMLObject就是一个DOM对象,可是对于onmouseover,onmouseout等等的设置如果直接使用appendChild的方式的时候,根本无法起作用。而通过设置innerHTML来生成VML图形的时候,那些鼠标事件却可以响应,有过这个方面编程的朋友麻烦告诉我具体的原因如何。

function HoverPie(el){}
function RestorePie(el){}
function LegendMouseOverEvent(){}
function LegendMouseOutEvent(){}
这四个函数就是提供了简单的交互作用。具体的代码如下:

function HoverPie(el){
        var v_length=500;
        var x_cur=Math.cos( el._scale * Math.PI /180);
        var y_cur=Math.sin (el._scale * Math.PI /180);     
        x=parseInt(x_cur * v_length);
        y=parseInt(y_cur * v_length);     
        el.style.top=4000 -y;
        el.style.left=1000 +x;
        el.strokecolor="black";

}
function RestorePie(el){
        el.style.top=4000;
        el.style.left=1000;
        el.strokecolor="white";
       
}
function LegendMouseOverEvent(){
       
        var eSrc=window.event.srcElement;
        var vPie=document.all(eSrc.element);       
        HoverPie(vPie);
}
function LegendMouseOutEvent(){
        var eSrc=window.event.srcElement;
        var vPie=document.all(eSrc.element);
        RestorePie(vPie);
}
HoverPie提供了简单的图形相对位移的功能.



5.            VMLPie.prototype.Zoom

提供了图形的放大和缩小的功能,既然是作为矢量图形,那么其放大和缩小就应该是没有任何图像损失的,实现的原理很简单,只需要修改group的坐标系统就可以实现,这个也就是我一直强调的需要将shape都放到group中的主要原因,对于文字,指能够使用和html Dom相同的方式进行设置。



VMLPie.prototype.Zoom=function (iValue){
        var vX=21600;
        var vY=21600;
        this.VMLObject=document.all(this.id);
        this.VMLObject.coordsize=parseInt(vX / iValue) +","+parseInt(vY /iValue);
        var texts=this.VMLObject.getElementsByTagName("TEXTBOX");
        for(var i=0;i<texts.length;i++){
                if (texts[i].preSize){
                          texts[i].style.fontSize= parseInt(texts[i].preSize) * iValue +"px";
                }
                else{
                          texts[i].style.fontSize= 12  * iValue + "px";
                }
        }
}
到目前为止,已经全部介绍完了VMLPie的全部制作过程,我是第一次写这样的文章,可能在很多东西的表述方面不是特别清楚,希望可以和大家共同探讨,另外完整的源代码我已经贴在CSDN的论坛里头,大家可以去下载。

http://expert.csdn.net/Expert/topic/2160/2160456.xml?temp=.4498102
posted on 2007-01-11 15:40  宏宇  阅读(2025)  评论(5编辑  收藏  举报