结合 GFX,DnD 与 Dijit 创建基于 Dojo 的 Web 图形类应用

红薯 发布于 2010/08/08 22:35
阅读 764
收藏 3

GFX(dojox.gfx)作为 Dojo 扩展组件之一,封装了底层浏览器中实际的图形引擎,使开发人员具备了 Web 绘图的基本能力,是此类应用的基础。同时,作为 Dojo 核心组件的 DnD(dojo.dnd),则实现了基于浏览器的鼠标拖拽操作,从而为图形组件选择,组件间连线等高级绘图操作提供了技术支持。再者,通过引入自定义 Dojo 小部件(dijit),开发人员可以对已有应用进行合理的扩展,使用户可以通过更为灵活的方式去操作图形。本文首先将对基于浏览器的绘图原理做一介绍,而 后以层进的方式向读者展示如何将 GFX,DnD 以及 dijit 进行紧密的结合,在浏览器中完成绘图类应用常见的各种操作,最后将通过一个实际的 Web 绘图应用来让读者对本文所述内容有更进一步的体会。

浏览器绘图的基本原理

基于 HTML 和 JavaScript 的浏览器绘图方式,依赖于各个浏览器内部所提供的图形引擎。但由于不同浏览器所支持的网络图形标准不尽相同,给软件的兼容性造成了很大的困难。Dojo 作为目前流行的 JavaScript 框架之一,虽然在一定程度上为开发人员屏蔽了这些差异,但当遇到某些特殊的绘图需求时,仍显得力不从心,因而也就有必要对浏览器绘图的基本原理有所了解, 这样才会写出性能更稳、效率能高的优秀代码。目前几个主流的网络图形标准包括 IE 支持的 VML,Firefox,Safari 和 Opera 支持的 SVG 以及 HTML5 支持的 Canvas。

VML 是微软开发并在 IE 5.0 以上版本提供支持的基于 XML 的一种标记语言,使用 VML 描述的矢量图形,由 shape 和 group 两个基本元素定义,shape 描述了一个矢量图形元素,而 shape 则将这些图形元素集合在一起,从而使其可以作为一个整体被处理。由于使用简单的文本来表示图像,因而 VML 可用很少的字节来表示相对复杂的图像。

SVG 是由 W3C 制定的同样基于 XML 的矢量图形描述语言,SVG 元素是指示如何绘制图像的一些指令,由图形引擎解释这些指令,把 SVG 图像在浏览器上显示出来。使用 SVG 可以在网页上显示出各种各样的高质量的矢量图形,其最明显的特征是灵活的文件格式,矢量图形、位图和文字三部分共同组成一个 SVG 图形,并具备对运行中的 Web 页面图像进行实时修改的能力。

Canvas 是指 HTML 5 中新加入的 canvas 元素,最初由 Apple 的 Safari 浏览器引入,而后受到 Firefox 和 Opera 的广泛支持。Canvas 元素相对 VML 和 SVG 的一个重要不同在于,canvas 提供了通过 JavaScript 绘制图形的方法,每一个 canvas 元素都有一个上下文,在其中可以绘制任意图形;而 SVG 和 VML 都使用 XML 文档来描述图形,开发人员通常通过修改其中的 XML 标记来完成对图像的修改。

明确了不同标准之间的差异,便更体会到 JavaScript 框架给开发人员所带来的便捷,下面这样一段 Dojo 代码,可以在 IE 和 Firefox 下渲染出相同效果的的图形。但透过现象来发

现本质,我们使用 Firebug 不难看出同一图形在不同浏览器背后的不同实现。

 var surface = dojox.gfx.createSurface(gTestContainer, 300, 300); 
var line = surface.createLine({ x1: 20, y1: 20, x2: 100, y2: 100 })
.setFill([255, 0, 0, 0.5]).setStroke({color: "red", width: 5})
.setTransform({ dx:70, dy: 100 });
var circle = surface.createCircle({ cx: 170, cy: 200, r: 50 })
.setFill([0, 255, 0, 0.5]);

 

图 1 是 IE 下 VML 图形 DOM 结构, Firefox 下 SVG 图形的 DOM 结构则如图 2 所示。所以,应该了解的是,Dojo 所提供的上层绘图 API,是能够根据不同的浏览器选择生成相应标准的绘图元素,这种封装的方式自然也就无法顾及各个绘图标准的独特之处,因而当我们在进行某项较为复杂的绘 图操作或者调试一个跨浏览器的代码缺陷时,不妨先确认一下 Dojo 是否很好的兼顾了这个问题,如果没有,那就需要我们针对不同的浏览器编写相应的代码来屏蔽这些差异。


图 1. IE 下 VML 图形的 DOM 结构
图 1. IE 下 VML 图形的 DOM 结构

图 2. Firefox 下 SVG 图形的 DOM 结构
图 2. Firefox 下 SVG 图形的 DOM 结构

Web 绘图类应用的基本结构

目前,常见的绘图类应用大致可以分为这样两类:一类是以画图板为代表的强调图形绘制的应用,另一类则是以流程图、UML 建模工具等为代表的图形互操作类应用。从实现的角度而言,此种绘图类应用均可视为由多个图形对象构成一个画面并基于此而涉及的对整个画面或其中各个图形对 象一系列操作的 Web 应用。

Dojo 提供了足够的支持来实现此类应用,DojoX 提供的 GFX 图形工具包,具备对图形对象的生成和基本的二维操作能力,足以满足第一类应用的需求;而 Dojo 核心提供的 DnD 包,使得用户可以通过“拖放”操作完成图形对象的添加,在 GFX 的基础上增强了用户操作体验,为第二类应用所需要的图形选择和图形间连线等操作提供了技术支持;同时,为了实现应用的可扩展性,可以使用自定义的 dijit 来引入更多的功能特征,比如,如果每个图形具有可访问的 URL 属性,那么通过提供一个基于图形对象的工具栏 dijit,则可以实现打开此 URL 的功能,当然,工具栏 dijit 可以提供更多的功能以达到更好的扩展性。图 3 展现了结合 GFX,DnD 与 Dijit 完成 Web 绘图类应用的基本组织结构图。


图 3. 实现 Web 绘图类应用的基本组织结构图
图 3. 实现 Web 绘图类应用的基本组织结构图

使用 GFX,实现基本绘图

GFX (dojox.gfx) 是 Dojo 提供的一套跨平台的图形生成包,用于生成基于 Web 的矢量图,能够做到动态生成以及和用户发生交互。使用 GFX 并非一件难事,其概念模型非常简单,由绘图画面(surface)、图形(shape)以及组(group)构成。开发人员只需首先创建一个绘图画面,而 后调用 GFX 中提供了图形创建 API,即可完成相应图形的绘制。对于一些常见的绘图操作,都可以通过设置图形对象相应的属性来实现,例如图像的边框以及颜色填充,可以通过 Stroke 和 Fill 属性进行设定,而图形之间的叠放层次则可以使用 z-order 来控制,z-index 值最大的图形位于整个画面的最上方。清单 1 是使用 GFX 绘制一条简单的直线,虽然简单但完整展示了 GFX 绘图的基本步骤。


清单 1. GFX 绘图基本步骤

				 
<script type="text/javascript" src="dojo/dojo.js" djConfig="parseOnLoad:true,
gfxRenderer:'svg,silverlight,vml' "></script>
//1. 加载 gfx 包
dojo.require("dojox.gfx");

dojo.addOnLoad(function(){
//2. 获取 DOM 节点,用于创建 surface
gTestContainer = dojo.byId('testcontainer');

//3. 创建 300*300 的绘图画面
var surface = dojox.gfx.createSurface(gTestContainer, 300, 300);

//4. 在画面上绘制直线,并设置相应填充色、线条以及位移等属性
surface.whenLoaded(dojo.hitch(this, function() {
var line = surface.createLine({x1: 20, y1: 20, x2: 100, y2: 120})
.setFill([255, 0, 0, 0.5]).setStroke({color: "red", width: 5}); }));
});
</script>

 

这里有三处值得注意,首先开发人员可以通过全局 djConfig 对象中的 gfxRender 属性来指定在加载过程中使用何种绘图引擎及其优先级,清单 1 中 SVG 将会被首先尝试,如果失败将再尝试 Silverlight,最后再试 VML,而 Canvas 将不再所尝试的范围之内;再者,当使用 dojox.gfx.createSurface() 方法生成 surface 对象时,必须对其长宽值赋予初值,即使其初始大小均为 0,不然在 IE 下会有绘图画面无法渲染的问题出现,对象生成后可以通过 setDimensions (width, height) 方法对其大小进行动态调整。最后一点,在 surface 对象生成之后,由于其背后关联的引擎有所不同,有的引擎需要额外的时间进行初始化工作,所以我们不能在得到 surface 对象之后立即使用其进行图形绘制,而是应该采用异步回调的方式,当 surface 对象初始化完毕后由再执行绘图操作,以确保不同绘图引擎之间的兼容性。

在得到一个图形对象之后,2D 绘图中一个常见的操作便是对图形进行各种变形 (transformation) 操作,其中包括旋转(Rotate)、缩放(Scale)、平移(Translate)以及倾斜(Skew)。GFX 通过提供 setTransform(matrix) 方法,使开发人员只需提供正确的矩阵对象参数便可以实现相应的变形,同时为了简化计算,还提供了一系列变形矩阵帮助类 (dojox.gfx.matrix), 从而使得开发人员只需选择相应的变形类,即可完成对应操作。例如,存在一个 500*500 大小的图形组,使用如下的矩阵变形数组即可完成图形扩大 2 倍并顺时针旋转 45 度的操作。

 [translate(250, 250), rotateg(-45), scale(2), translate(-250, -250)] 

 

另外,GFX 支持组对象概念,当一个 surface 对象中涉及对多个图形的操作时,我们可以将这些图形划分加入到同一个组中。组兼具 surface 和 shape 二者的特征,复杂图形的创建、多图形的事件响应处理以及多图形属性的统一调整等操作,都需要借助组的概念来完成。开发人员不仅可以将画面中存在的图形添加 到组中,还可以使用与画面相同的 API 创建图形,surface 和 group 对象都支持使用 add()/remove() 操作来完成图形的添加或删除操作。清单 2 首先创建了一个 group 对象,而后将 surface 对象里的直线添加到自身,并对这两个图形进行了统一的变形操作。


清单 2. 对 GFX 图形对象进行变形操作

				 
//5. 创建 group,并在其中创建一个绿色圆形
var group = surface.createGroup();
var circle = { cx: 250, cy: 250, r: 50 };
var shape_circle = group.createCircle(circle).setFill([0, 255, 0, 0.5]);

//6. 将画面中的红色直线移动到组对象中
group.add(line);

//7. 将组对象扩大至 2 倍并逆时针旋转 45 度
group.setTransform([dojox.gfx.matrix.translate(250, 250),
dojox.gfx.matrix.rotateg(-45),dojox.gfx.matrix.scale(2),
dojox.gfx.matrix.translate(-250, -250)]);

引入移动和拖放,增强图形操作能力

在使用 GFX 实现了基本绘图操作之后,本章将再深入一步,借助 DnD(dojo.dnd) 完成对图形的移动和拖放 (DnD,Drag and Drop) 操作,增强用户对 Web 2.0 应用的体验。

首先来看移动操作,为了移动 GFX 绘图画面中的某个图形对象,GFX 提供了专用于图形的移动的 dojox.gfx.Moveable 类,开发人员在使用时只需简单的将所要移动的图形对象作为参数传入即可,例如,为了能够使清单 2 所创建的组图形具备可移动性,我们可以在图形创建完毕后通过如下这条语句来实现。

 new dojox.gfx.Moveable(group) 

 

这里顺便介绍一下的是,在 Dojo 其核心库中提供有一个专用于 DOM 节点移动的 dojo.dnd.Moveable 类,较 dojox.gfx.Moveable 而言具有更为通用的移动能力,而并非仅限定于 GFX 图形,例如我们可以使用如下代码来移动一个 dijit 对象。

 new dojo.dnd.Moveable(somedijit.domNode) 

 

拖放是图形操作类应用中最为常见的一种高级操作,它为用户提供了更为灵活方便的交互方式,工作流、流程图等典型的绘图应用中图形的选择,图 形间连线等操作都使用拖放来完成。GFX 并没有直接的方法对图形的拖放操作进行支持,因而,开发人员只能依赖 Dojo 核心库提供的 DnD 包实现对 GFX 图形的拖放操作。

为了能更好地阐述如何将 DnD 拖放效果运用到图形操作上,这里将以对各种图形的选择拖放功能为例,从实现的角度出发逐一引出 DnD 中所涉及的主要概念。首先,在选择一个图形并将其拖放到指定位置的过程中,源(Source)和目标区域是两个必要的参数,通过 dojoType=dojo.dnd.Source 的方式,可以将一个 HTML 标记定义为一个 DnD 源。事实上,目标区域和源从本质而言没有任何区别,他们都具备相同的事件响应能力,dojo.dnd.Target 仅仅是将 isSource 属性设置为 false 的 Source 类。再进一步,在源区域,我们需要能够选择不同的图形实体,如方形,圆形,椭圆等,也即是我们希望在一个源区域能包括一个或多个源实体对象,并从源区域自 身的角度而言能够对这些实体进行管理,这便是 dojo.dnd.Container 类的职责。除此之外,针对源区域的多个图形实体,dojo.dnd.Selector 类还能够实现基于 Ctrl 或 Shift 功能键的多图形的同时选择的能力,这是在继承 Container 类所有方法和属性的基础之上,增加对所管理实体的选择能力。Source 则是 Selector 的子类,它具备所有 Container 和 Selector 的能力,因而,对于一般的拖放需求,我们并不需要对 Container 和 Selector 类进行重写,只需直接定义 Source 和 Target 对象便可以完成。清单 3 显示了如何添加图形的拖放操作。


清单 3. 使用 DnD 实现图形的选择和拖放

				 
var gShapeContainer = dojo.byId("shapecontainer");
var gShapeBtn = dojo.byId("someshape");
dojo.addClass(gShapeBtn, "dojoDndItem");
// 创建源区域,并限制一次拖放操作只能选择并复制一个 dataitem 到目标区域
var source = new dojo.dnd.Source(gShapeContainer, {copyOnly:true, singular:true});

// 创建目标区域,creator 方法用于创建一个 dataitem 在拖放过程中的呈现形式
var target = new dojo.dnd.Target(gTestContainer, {
creator:function(data, hint){
var dummyNode = dojo.doc.createElement("div");
dummyNode.id = "dummyShape";
return {node: dummyNode, data: data, type:[]};
}
});

// 获取鼠标停止位置,即拖放位置 (x, y) 坐标
var dropPosition = null;
dojo.connect(gTestContainer, "onmouseup", dojo.hitch(this, function(e) {
var pos = dojo.coords(gTestContainer, true);
dropPosition = {x: (e.pageX - pos.x), y: (e.pageY - pos.y) };
}));

// 目标区域在拖放位置创建相应的图形
dojo.connect(target, "onDrop", dojo.hitch(this, function(source, nodes, copy) {
surface.createCircle({cx: dropPosition.x, cy: dropPosition.y, r:20})
.setFill([0, 255, 0, 0.5])
.setStroke({color: "green", width: 1});
}));
// 删除拖放过程中创建的“临时 avatar”的 DOM Node
dojo.subscribe("/dnd/drop", dojo.hitch(this, function(source, nodes, copy, target) {
var dummyNode = dojo.byId("dummyShape");
gTestContainer.removeChild(dummyNode);
}));

 

在上面,我们不难发现,在图形的整个拖放的过程中,使用 creator() 方法生成的拖放对象如图 4 左侧所示,这是 DnD 默认移动过程中的对象呈现形式。如果开发人员希望能够使用相应图形的缩略图来表示当前选择的图形,则需要 DnD 中另外一个重要的概念 Avatar,dojo.dnd.avatar 是源区域中的实体在拖放过程中的“化身”,如果我们需要更个性化的“化身”,就需要重写 dojo.dnd.Manager 中的 makeAvatar() 方法。图 4 右侧则是实现了缩略图形式的 Avatar,清单 4 自定义 Avatar 的实现代码。


清单 4. 实现自定义 Avatar

				 
dojo.declare("demo.MyAvatar", dojo.dnd.Avatar, {
construct: function(){
// 使用 GFX 创建一个 r=20px 的绿色圆形 avatar
var avatarNode = dojo.doc.createElement("div");
dojo.style(avatarNode, {
position: "absolute"
});
var surface = dojox.gfx.createSurface(avatarNode, 40, 40);
surface.whenLoaded(dojo.hitch(this, function() {
surface.createCircle({cx: 20, cy: 20, r:20})
.setFill([0, 255, 0, 0.5])
.setStroke({color: "green", width: 1});
}));
// 由于在 dojo.dnd.Avatar 的 destroy 方法中会销毁 this.node,所以需要将创建的 avatar 赋给 //this.node
this.node = avatarNode;
},
update: function(){
//override as empty, do nothing
}
});

 

 // 重写 makeAvatar 方法,其中使用 demo.MyAvatar 
dojo.dnd.manager().makeAvatar = function(){
return new demo.MyAvatar(this);
};



图 4. 默认 Avatar 与自定义 Avatar
图 4. 默认 Avatar 与自定义 Avatar

默认 Avatar 自定义 Avatar

引入自定义 Dijit,扩展画图应用

从图形操作的完整性而言,到目前为止我们具备了对图形的增加以及移动能力,那么如何实现针对图形本身的修改和删除等操作呢?从 Web 2.0 应用所应为用户带来的全新用户体验的角度出发,我们引入自定义 Dijit,实现一个针对单个图形对象的操作工具栏,工具栏提供各种针对当前的图形的功能选项,从而实现图形的删除,修改等操作。

Dijit 是 Dojo 为开发人员提供的具有高可复用性的小部件,其本质是一组相对独立的 HTML/CSS/JavaScript 代码片段,常见的如日历,Combobox,Tree 等常用 UI 小部件都可以直接使用 Dojo 预置的 Dijit 来实现,但通常我们为了满足特定应用的需求,更常编写自定义的 Dijit 来完成。

为了能将 GFX 图形与自定义 Dijit 加以结合,首先需要编写好一个图形操作工具栏 dijit,代码如 清单 5所示。这个 Dijit 包含三个功能选项,即删除图形和放大 / 缩小图形。


清单 5. 图形对象操作工具栏 dijit

				 
MyToolbar.html:
<div class="toolbar">
<button dojoAttachPoint="enlargeNode">+</button>
<button dojoAttachPoint="lessenNode">-</button>
<button dojoAttachPoint="removeNode">X</button>
</div>

MyToolbar.css:
.toolbar {
position:absolute;
background:#666666;
}
.toolbar button {
margin:0;
padding:0;
}

MyToolbar.js:
dojo.provide("demo.MyToolbar");
dojo.require("dijit._Widget");
dojo.require("dijit._Templated");

dojo.declare("demo.MyToolbar", [dijit._Widget, dijit._Templated], {
templatePath: dojo.moduleUrl("demo", "MyToolbar.html"),

enlargeNode: null,
lessenNode: null,
removeNode: null,

shape: null,

postCreate: function() {
dojo.connect(this.enlargeNode, "onclick", this, "onClickEnlarge");
dojo.connect(this.lessenNode, "onclick", this, "onClickLessen");
dojo.connect(this.removeNode, "onclick", this, "onClickRemove");
},

setPosition: function(pos) {
dojo.style(this.domNode, {
left:pos.x + "px",
top: pos.y + "px"
});
},

onClickEnlarge: function() {
console.info("Enlarging a shape.")
},

onClickLessen: function() {
console.info("Lessening a shape.")
},

onClickRemove: function() {
this.shape.removeShape();
}
});

 

有了工具栏 dijit,下面一步便是将其引入我们的 Web 图形应用,与 GFX 生成的图形对象进行绑定。这里采用一种比较常见的所见即所得的方式,当用户准备对某个图形进行操作并将鼠标移动至相应图形上是,通过 onmouseover 事件触发工具栏 dijit 的出现,而 dijit 出现的位置则会根据当前对象的相对位置而设置。清单 6 展示了这部分代码。


清单 6:在图形对象的事件处理函数中管理 dijit

				 
//7. 当鼠标悬停在图形上,显示工具栏 ; 点击空白处则隐藏工具栏。
var toolbar = null;
line.connect("onmouseover", dojo.hitch(this, function(shape, e) {
var containerPosition = dojo.coords(gTestContainer, true);
if(toolbar != null) {
toolbar.destroy();
}
toolbar = new demo.MyToolbar({shape:shape});
gTestContainer.appendChild(toolbar.domNode);
toolbar.setPosition({
x: (e.pageX - containerPosition.x),
y: (e.pageY - containerPosition.y)
});
}, line));

dojo.connect(gTestContainer, "onclick", dojo.hitch(this, function() {
if(toolbar != null) {
gTestContainer.removeChild(toolbar.domNode);
toolbar.destroy();
toolbar = null;
}
}));

 

图 5 显示了借助工具栏 dijit 完成的针对图形的特定操作,这种基于事件和自定义 dijit 的模式能够充分发挥开发人员的想象空间,帮助开发出具备更加丰富用户体验的功能。


图 5. 使用 dijit 完成对图形对象的删除或缩放
图 5. 使用 dijit 完成对图形对象的删除或缩放

一个简单的 Web 绘图应用

本章在前三章所讲解内容的基础之上,稍加整理丰富,完成了一个类似 Windows 画图板的 Web 应用,如图 6 所示。通过该应用,用户可以直接在 Web 页面中完成各种图形的绘制和常见操作,主要支持的功能包括:

  • 支持直线、矩形、圆形、椭圆形、画笔涂鸦以及图片和文字
  • 支持前景 / 背景(填充)颜色、画笔宽度和样式的选择
  • 支持多种变形(任意旋转和任意大小放缩)
  • 支持变换画布大小
  • 支持图形 / 图像 / 文字的拖动
  • 支持图形 / 图像 / 文字的鼠标直接绘制方式和 DnD 拖放方式
  • 支持多选、删除操作
  • 支持图形 / 图像 / 文字的置顶 / 置底操作
  • 支持简单的串行化 / 反串行化操作

读者可以在本文末尾代码下载章节获取该应用的详细代码,代码基于 dojo-release-1.4.1,运行之前可以将 Dojo release 复制到解压缩后的目录 code/app 下或者修改该目录下 index.html 中对 dojo.js 文件的引用位置,指向正确的文件所在路径。设置完毕之后,使用 Firefox 浏览器直接打开 index.html,便可以开始体验上述所列各种绘图功能。


图 6. 基于 dojo 的 Web 绘图应用
图 6. 基于 dojo 的 Web 绘图应用

结束语

本文首先介绍了浏览器绘图的基本原理,而后讲解如何使用 GFX 完成基本的绘图操作,并在此基础之上,引入 DnD 操作,完成图形的移动和拖放等高级操作。最后,通过借助自定义 dijit,完成对图形的修改和删除等操作,实现了一个 Web 2.0 风格的 Web 绘图应用,当然,使用 dojo 进行 Web 绘图的能力绝不仅限于此,这个画图应用也只是希望能够作为一个示例,起到抛砖引玉的效果。相信只要彻底掌握 GFX,灵活运用 DnD,并合理引入自定义 dijit,我们就有理由期待更加精彩 Web 绘图类应用的出现。

下载本文示例代码

加载中
0
李永波
李永波

Dojo 太臃肿了吧!

返回顶部
顶部