上一篇我們自定義CPU和內存的展示界面效果,這篇我們將繼續采用HT完成一個新任務:實現一個能進行展開和合並切換動作的刀閘控件。對於電力SCADA和工業控制等領域的人機交互界面常需要預定義一堆的行業標准控件,以便用戶能做可視化編輯器里,通過拖拽方式快速搭建具體電力網絡或工控環境的場景,並設置好設備對應后台編號等參數信息,將拓撲圖形與圖元信息一並保存到后台,實際運行環境中將打開編輯好的網絡拓撲圖信息,連接后台實時數據庫,接下來就是接受實時數據庫發送過來的采集信息進行界面實時動態刷新,包括用戶通過客戶端對設備進行的各種下發遙控等操作,發送到后台最終實現對硬件設備的控制,這個過程就是典型的實時監控系統的基本架構流程。我們今天只做好小小螺絲釘工作,提供一個可控制的刀閘開關控件。
具體實現之前先看看我們要達到的最終效果圖片和視頻
記得十多年前我剛畢業的第一份工作就是負責電力SCADA的人機界面交互模塊,當時大部分電力行業都是采用VC/MFC或QT來實現界面呈現,其實至今也依然如此,前端時間和老朋友聚會了解到他們還在用VC6編譯系統,如今的VS20**根本跑不動他們龐大的古老系統,當然也許他們沒配置好工具參數,但從一個側面你可以感受到老系統遷移之重,大部分程序員處於為項目業務功能疲於奔命狀態,上百號人這么多年在根本無力優化和重構的架子上不斷堆積功能,我記得當時一個mousedown函數居然堆了六千多行代碼,各種圖元類型的draw代碼也是長得不堪入目,這些老系統雖然不好維護但也考這么多程序員活生生的維護下來了,我們每天能正常的用水用電用氣,背后都是靠着眾多程序員的血汗維護着以如今眼光看完全不堪入目的爛代碼,不得不承認在中國能用是第一位,其他問題只要堆人能解決的都不是問題。有點扯遠了,上幾張我以前電力實現的圖庫工具:
實現功能並不難,當時也實現了組合和分解圖元,能進行圖庫管理和用戶自定義,我相信全世界肯定不下幾百上千套繪圖軟件,剛開始我還是很興奮,每天學習不同的繪制API,就能搗鼓出新效果,我也不在乎代碼架構,每天就是以學習掌握更多的龐大MFC庫為榮,但當你掌握大部分繪圖技巧后,我發現自己每天維護這種龐大到無法以個人力量進行大規模重構,又不得不持續維護每天堆積功能性體力活代碼時,我感覺自己在浪費生命,於是跳槽到了另外一家公司打算做電子商務,結果陰差陽錯又被安排到電力部門干起來繪圖工具,還好這次我能換個新語言Java,沒有歷史包袱完全自己重頭設計圖形架構,於是地球上出現了第1001個繪圖工具:
這一版設計上還是有很大的改進,圖形繪制邏輯,交互代碼以及界面布局等都進行了較合理的分工設計,那個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樣式屬性上
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
}
]
});
以上是在矢量編輯器中打開的效果圖,你可以清晰的看得到我們定義的幾個元素的位置大小演示等,這樣應用時只要構建一個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動畫效果。
最后幾點設計控件的建議:
- 切換到使用者角度,即站在上層應用者角度提供最簡潔符合業務邏輯的API接口,盡量不暴露圖形相關參數,圖形參數對上層使用着是晦澀的,暴露了你自己也是非常難改動和維護
- 不要一開始設計就考慮如何操作,如何動畫,操作和動畫都可以在基礎API基礎上擴展再封裝,某種程度上來說,如何操作和如何動畫甚至不屬於控件封裝該干的,至少可再提供進一層的封裝,這樣可隨意切換操作和動畫邏輯,而不影響底層控件的數據模型和繪制邏輯
- 盡量讓繪制代碼和業務邏輯代碼分離,這點如果采用最基礎的繪制代碼的確很難分離,這也是HT盡量采用矢量描述,不讓用戶控制底層繪制代碼的初衷





