HT图形组件设计之道(二)

上一篇咱们本身定义CPU和内存的展现界面效果,这篇咱们将继续採用HT完毕一个新任务:实现一个能进行展开和合并切换动做的刀闸控件。对于电力SCADA和工业控制等领域的人机交互界面常需要提早定义一堆的行业标准控件。以便用户能作可视化编辑器里,经过拖拽方式高速搭建详细电力网络或工控环境的场景,并设置好设备相应后台编号等參数信息。将拓扑图形与图元信息一并保存到后台,实际执行环境中将打开编辑好的网络拓扑图信息,链接后台实时数据库,接下来就是接受实时数据库发送过来的採集信息进行界面实时动态刷新。包含用户经过client对设备进行的各类下发遥控等操做,发送到后台终于实现对硬件设备的控制。这个过程就是典型的实时监控系统的基本架构流程。javascript

咱们今天仅仅作好小小螺丝钉工做,提供一个可控制的刀闸开关控件。html

详细实现以前先看看咱们要达到的终于效果图片和视频前端

Screen Shot 2014-08-12 at 9.14.50 PM

记得十多年前我刚毕业的第一份工做就是负责电力SCADA的人机界面交互模块。当时大部分电力行业都是採用VC/MFC或QT来实现界面呈现,事实上至今也依旧如此,前端时间和老朋友聚会了解到他们还在用VC6编译系统,如今的VS20**根本跑不动他们庞大的古老系统,固然或许他们没配置好工具參数。但从一个側面你可以感觉到老系统迁移之重。大部分程序猿处于为项目业务功能疲于奔命状态,上百号人这么多年在根本无力优化和重构的架子上不断堆积功能,我记得当时一个mousedown函数居然堆了六千多行代码,各类图元类型的draw代码也是长得不堪入目,这些老系统尽管很差维护但也考这么多程序猿活生生的维护下来了,咱们天天能正常的用水用电用气。背后都是靠着众多程序猿的血汗维护着以如今眼光看全然不堪入目的烂代码。不得不认可在中国能用是第一位。其它问题仅仅要堆人能解决的都不是问题。有点扯远了,上几张我曾经电力实现的图库工具:java

Screen Shot 2014-08-12 at 9.46.22 PM

实现功能并不难。当时也实现了组合和分解图元。能进行图库管理和用户本身定义,我相信全世界确定不下几百上千套画图软件,刚開始我仍是很是兴奋,天天学习不一样的绘制API,就能捣鼓出新效果,我也不在意代码架构,天天就是以学习掌握不少其它的庞大MFC库为荣,但当你掌握大部分画图技巧后,我发现本身天天维护这样的庞大到没法以我的力量进行大规模重构,又不得不持续维护天天堆积功能性体力活代码时,我感受本身在浪费生命。因而跳槽到了另一家公司打算作电子商务,结果阴差阳错又被安排到电力部门干起来画图工具。还好此次我能换个新语言Java,没有历史包袱全然本身重头设计图形架构,因而地球上出现了第1001个画图工具:node

Screen Shot 2014-08-12 at 10.35.33 PM

这一版设计上仍是有很是大的改进。图形绘制逻辑,交互代码以及界面布局等都进行了较合理的分工设计,那个Java和设计模式很是火,人手一本Martin Fowler《Refactoring: Improving the Design of Existing Code》,宛如宗教信仰坚定运行一个函数不超过几十行的时代。一个mousedown几千行的代码已经绝迹了,但我仍是很是不惬意,数据模型和界面绘制没有很是好的有机结合机制,尽管电力要求界面有***的毫秒级响应,但大部分公司都是像游戏刷新机制那样不断repaint界面,是的。当时的数据模型没有不论什么事件派发机制,就是内存中的一堆数据,你没法知道哪一个数据何时change了,于是仅仅能不断的repaint界面,刷新周期过短对于大的网络拓扑图根原本不及更新,更新周期太长又达不到响应要求,至于所谓的***毫秒级响应我仅仅能呵呵了,为了上这个系统一堆兄弟在沈阳某农村封闭了八个多月,我很是好奇那个老系统现在是否健在…数据库

回到咱们的任务。一个刀闸最基本的就是可开闭的部分,其它部分都是装饰物效果而已,所以我採用HT的矢量来描写叙述整个刀闸外观,当中需要开闭部分採用type为shape的一个线段来描写叙述。并将其的rotation旋转參数经过func: ‘style@switch.angle’的描写叙述来绑定到Node图元的switch.angle样式属性上json

ht.Default.setImage('switch', {
    width: 100,
    height: 50,
    comps: [
        {
            type: 'roundRect',
            rect: [0, 0, 100, 50],
            background: '#2C3E50',
            gradient: 'linear.north'
        },
        {
            type: 'circle',
            rect: [10, 10, 10, 10],
            background: '#34495E',
            gradient: 'radial.center'
        },
        {
            type: 'circle',
            rect: [80, 10, 10, 10],
            background: '#34495E',
            gradient: 'radial.center'
        },
        {
            type: 'shape',
            points: [10, 40, 40, 40],
            borderWidth: 8,
            borderColor: '#40ACFF',
            border3d: true
        },
        {
            type: 'shape',
            points: [60, 40, 90, 40],
            borderWidth: 8,
            borderColor: '#40ACFF',
            border3d: true
        },
        {
            type: 'shape',
            points: [5, 40, 35, 40, 65, 40],
            segments: [1, 1, 2],
            borderWidth: 8,
            borderColor: '#40ACFF',
            border3d: true,
            borderCap: 'round',
            rotation: {
                value: -Math.PI/4,
                func: 'style@switch.angle'
            }
        },
        {
            type: 'circle',
            rect: [30, 35, 10, 10],
            borderColor: 'red',
            borderWidth: 5,
            border3d: true
        },
        {
            type: 'circle',
            rect: [60, 35, 10, 10],
            borderColor: 'red',
            borderWidth: 5,
            border3d: true
        }        
    ]
});

Screen Shot 2014-08-07 at 8.20.12 PM

以上是在矢量编辑器中打开的效果图,你可以清晰的看获得咱们定义的几个元素的位置大小演示等,这样应用时仅仅要构建一个Node对象。将其image设置为switch矢量,那么未来仅仅需要调用node.setStyle(‘switch.angle’, Math.PI/6)就可以随时随地控制刀闸展开角度 。设计模式

这样封装还不够完美,相应用着来讲他们仅仅关心刀闸的打开和关闭的操做,他们并不关心旋转角度。开和关是业务角度的理解,而旋转角度是底层实现图形上的參数。并且用户还需要开关过程有动画效果。因而咱们进行了进一步的封装,设计了ht.Switch的类,提供了setExpanded的函数,在函数里面操做底层绑定图形的‘switch.angle’属性,以及启动动画封装网络

ht.Switch = function(){    
    ht.Switch.superClass.constructor.call(this); 
    this.s('switch.angle', 0);
};
ht.Default.def('ht.Switch', ht.Node, {
    _image : 'switch',
    _icon: 'switch',

    toggle: function (anim) {
        this.setExpanded(!this.isExpanded(), anim);
    },
    isExpanded: function () {
        return this.s('switch.angle') !== 0;
    },
    setExpanded: function (expanded, anim) {
        if(anim == null){
            anim = true;
        }
        var self = this,
            animation = self._animation,
            oldValue = self.isExpanded();

        if(animation){
            animation.stop(true);
            delete self._animation;
        }

        if (oldValue !== expanded) {                        
            var targetAngle = expanded ?

-Math.PI/4 : 0; if(anim){ oldValue = self.s('switch.angle'); self._animation = ht.Default.startAnim({ action: function(t){ self.s('switch.angle', oldValue + (targetAngle-oldValue)*t); } }); }else{ self.s('switch.angle', targetAngle); } } } });架构


在咱们的视频操做中你会发现经过属性页的拉条可以随意控制刀闸张角。同一时候经过isExpanded/setExpanded的boolean类型属性也可以勾选动画切换刀闸的开与关,细心的程序猿你会发现不仅拓扑图上的刀闸动起来了,连TreeView上的刀闸相应的icon图标也是和矢量描写叙述的效果同样。更惊喜的是树上的icon也是实时显示刀闸的展开角度,这是传统图片做为树的icon图片没法实现的,这也是咱们一直强调的HT for Web整体架构已经为矢量打下基础,并非为了拓扑才实现矢量。所有通用组件都享有矢量的功能特性,这个兴许咱们会有不少其它的应用案例让你们体会到这样的结合的强大之处。固然可维护性已经不用我多说了。传统的通用组件tree上本身定义renderer也能实现一个能动的icon,但你可以想一想工做量,咱们没有写一行绘制代码,只经过定义一个json的矢量就把GraphView和TreeView的事都干了,并且业务接口对上层应用人员来讲就是一个node.setExpanded(true/false)之简单。

这里我仅仅是随手搞了个很ugly的刀闸。你可以让美工採用矢量画图工具可视化的绘制更美丽的效果,界面操做上你也可以经过graphView.mi监听交互事件,好比监听到双击刀闸时进行开关切换,甚至可以參考《透过WebGL 3D看动画Easing函数本质》的章节採用更洋相的Easing动画效果。

最后几点设计控件的建议:

  1. 切换到使用者角度。即站在上层应用者角度提供最简洁符合业务逻辑的API接口,尽可能不暴露图形相关參数,图形參数对上层使用着是晦涩的,暴露了你本身也是很是难修改和维护
  2. 不要一開始设计就考虑怎样操做,怎样动画,操做和动画都可以在基础API基础上扩展再封装,某种程度上来讲,怎样操做和怎样动画甚至不属于控件封装该干的,至少可再提供进一层的封装。这样可任意切换操做和动画逻辑,而不影响底层控件的数据模型和绘制逻辑
  3. 尽可能让绘制代码和业务逻辑代码分离,这点假设採用最基础的绘制代码的确很是难分离,这也是HT尽可能採用矢量描写叙述,不让用户控制底层绘制代码的初衷

Screen Shot 2014-08-12 at 8.57.11 PM

相关文章
相关标签/搜索