HT圖形元件設計之道(一)

圖撲軟體發表於2014-08-11

HT for Web簡稱HT提供了涵蓋通用元件、2D拓撲圖形元件以及3D引擎的一站式解決方案,正如Hightopo官網所表達的我們希望提供:Everything you need to create cutting-edge 2D and 3D visualization. 這個願景從功能上是個相當長的戰線,從設計架構上也是極具挑戰性的,其實HT團隊是非常保守的,我們從不貪多圖大,只做我們感覺自己能得更好,能給使用者綜合體驗更佳的功能,在這樣理念驅動下我們慢慢形成了這樣的願景,慢慢實現了幾個有意義的里程碑,慢慢積累下了不少圖形元件設計上的創新和經驗,我不知道這個系列會寫多少篇,也許永遠也不會結束,也沒有系統的提綱規劃,想到什麼就寫什麼,只希望文章能啟發有興趣的同學對圖形元件設計更深的思考就足夠了。

討論前先設定話題的邊界,HT是基於HTML5的圖形元件庫,因此文章的案例更多會涉及HTML和JavaScript語言,但並不侷限於Web前端,設計思想上同樣適用於任何GUI語言平臺。完整的前端設計是需要考慮到後臺載入併發等因素,但本系列更側重於純客戶端圖形元件,不涉及網路通訊部分的思考,例如最近阿里無線前端招聘讓談談:講講輸入完網址按下回車,到看到網頁這個過程中發生了什麼。這是個能討論出很多方方面面,讓你瞭解面試者的好話題,但這裡討論的話題會與以下關鍵字更為相關:企業應用、Single Page Application、重客戶端互動、監控、MV*等。

如Linus大神所言:Talk is cheap, show me the code.  因此我選擇在話題展開之前,先用HT來擴充套件定製幾個應用案例,以便大家瞭解HT元件及其擴充套件設計思路。

1282039088303

熟悉Flex的程式設計師應該都瞭解Tour de Flex這個包羅永珍的大雜燴,其中的網路監控拓撲Network Monitor特別其動畫切換效果一直給我很深印象,這裡不可能有篇幅實現完整例子,我們僅嘗試實現其展示CPU和MEM的介面部分。

IMG_3641

實現的最終效果如上圖所示,模型資料就兩個數值,一個代表CPU佔用率,一個代表記憶體佔用率,左側通過HT的圖形元件GraphView自定義了向量圖形展示,右上角自定義了屬性頁PropertyView的兩單元格的Renderer,右下角兩個Slider可拖動改變CPU和MEN值。

此例子麻雀雖小五臟俱全,三個部分分別採用三種方式實現了自定義元件,同時不同元件共享同一資料來源,在呈現的基礎上還支援桌面和移動端的Mouse和Touch的互動,還有不同終端螢幕的元件佈局功能。

業務上需要在佔用率小於40%時呈現律師,40%-70%時顯示黃色,超過70%時呈現紅色,因此定義瞭如下getColor的工具函式:

1
2
3
4
5
6
7
getColor = function(value) {
    if (value < 40)
        return '#00A406';
    if (value < 70)
        return '#FFCC00';
    return '#A60000';
};

PropertyView上採用的最基礎和原始的方式,通過Canvas畫筆進行單元格的自定義繪製,在註冊PropertyView時過載drawPropertyValue函式即可實現單元格自定義Renderer的繪製

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
drawFunc = function(g, value, x, y, w, h){
    g.fillStyle = '#A1A1A3';
    g.beginPath();
    g.rect(x, y, w, h);
    g.fill();                    
    g.fillStyle = getColor(value);
    g.beginPath();
    g.rect(x, y, w * value / 100, h);
    g.fill();
    ht.Default.drawText(g, value + '%''12px Arial''white', x, y, w, h, 'center'); 
};
propertyView.addProperties([
    {
        displayName: 'CPU',
        drawPropertyValue: function(g, property, value, rowIndex, x, y, w, h, data, view)    {
            drawFunc(g, data.a('cpu'), x, y, w, h);
        }
    },
    {
        displayName: 'MEM',
        drawPropertyValue: function(g, property, value, rowIndex, x, y, w, h, data, view) {
            drawFunc(g, data.a('mem'), x, y, w, h);
        }        
    }
]);

Slider拉條部分直接在HT封裝的元件之上應用,因而無需接觸到最底層的Canvas畫筆繪製,僅需要在onValueChanged時更新leftBackgroud拉條左側顏色即可,其實也可以通過過載Slider的getLeftBackground函式實現:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
formPane.addRow(['CPU', {
    slider: {
        step: 1,
        onValueChanged: function(){
            var value = this.getValue();
            node.a('cpu', value);
            this.setLeftBackground(getColor(value));
        },
        value: node.a('cpu')        
    }
}], [50, 0.1]);
formPane.addRow(['MEM', {
    slider: {
        step: 1,                        
        onValueChanged: function(){
            var value = this.getValue();
            node.a('mem', value);
            this.setLeftBackground(getColor(value));
        },
        value: node.a('mem')        
    }
}], [50, 0.1]);

GraphView部分採用了《HT全向量化的圖形元件設計》文章介紹的HT自定義的向量方式來實現圖形效果,這種方式介於以上兩種擴充套件方式之間,需要自定義繪製效果,但通過HT提供的向量格式,使用者可採用較為直觀易讀的JSON格式來描述圖形,並通過資料繫結的方式實現模型資料與介面呈現的關聯,避免如第一種自定義renderer的方式,即需要接觸到底層繪製函式,同時業務邏輯程式碼與繪製程式碼混雜一起不易維護的問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
ht.Default.setImage('server_image', {
    width: 300,
    height: 200,
    comps: [
        {
            type: "roundRect",
            rect: [3, 5, 291, 189],
            background: "#E3E3E3",
            gradient: "linear.northeast",
            shadow: true
        },
        {
            type: "text",
            text: "CPU",
            font: "16px Arial",
            rect: [20, 45, 59, 41]
        },
        {
            type: "text",
            text: "MEM",
            font: "16px Arial",
            rect: [20, 108, 59, 41]
        },
        {
            type: "rect",
            rect: [82, 55, 145, 22],
            background: "#A1A1A3"
        },
        {
            type: "rect",
            rect: {
                func: function(data) {
                    return [82, 55, 145 * data.a('cpu') / 100, 22];
                }
            },
            background: {
                func: function(data) {
                    return getColor(data.a('cpu'));
                }
            }
        },
        {
            type: "rect",
            rect: [82, 117, 145, 22],
            background: "#A1A1A3"
        },
        {
            type: "rect",
            rect: {
                func: function(data) {
                    return [82, 117, 145 * data.a('mem') / 100, 22];
                }
            },
            background: {
                func: function(data) {
                    return getColor(data.a('mem'));
                }
            }
        },
        {
            type: "text",
            font: "16px Arial",
            rect: [240, 49, 53, 31],
            text: {
                func: function(data) {
                    return data.a('cpu') + '%';
                }
            },
            color: {
                func: function(data) {
                    return getColor(data.a('cpu'));
                }
            }
        },
        {
            type: "text",
            font: "16px Arial",
            rect: [240, 108, 47, 39],
            text: {
                func: function(data) {
                    return data.a('mem') + '%';
                }
            },
            color: {
                func: function(data) {
                    return getColor(data.a('mem'));
                }
            }
        }
    ]
});

以上程式碼註冊了名為server-image的圖片,繫結了attr上的mem和cpu的兩個屬性,因此做完這些手腳架的基礎工作後,應用人員只需要構建ht.Node物件,通過node.setImage(‘server-image’)即可實現該圖元在GraphView上呈現’server-image’描述的向量效果,並且PropertyView、Slider和GraphView三個元件都通過node的attr上的cpu和mem來顯示介面,這樣當後臺獲取到採集的實時資料後,只需要更新到node的attr上的cpu和mem屬性,則介面上的所有元件就會自定更新顯示:

1
2
3
4
5
6
7
8
node = new ht.Node();
node.setName('SERVER');
node.setImage('server_image');
node.a({
    cpu: 30,
    mem: 70
});
dataModel.add(node);

 

當然實際應用中並不需要拉條改變CPU和MEN值,這些值一般通過後臺採集實時自動更新僅作為呈現,但有了前端這些元件的一站式支援,我們不需要連線後臺也可以很方便在客戶端進行模擬測試,有了這樣的機制我們就可以分離模組一步步測試,例如我們現在不需要連線伺服器也可以測試向量描述定義的是否正確,數值改變後綠黃紅的業務顏色更新是否正確,各個元件的資料同步是否正常,Mouse和Touch互動是否能正常操作,介面在不同裝置螢幕上顯示是否正常等等,這些純客戶端元件的封裝工作都做到位後,你就可以安心連線後臺資料進行測試了。

見過太多客戶出問題時只會說:介面顯示不對。這樣的問題描述完全無法定位根源,到底時後臺資料庫問題,網路通訊問題,解析資料問題,設定模型問題還是元件封裝問題?這也是MVC/MVP/MVVM存在的另外一個層面的意義,MV*除了事件派發資料繫結外,能更好的進行呈現、模型和業務邏輯的分工切割進行獨立測試的重要意義,就行TCP/IP七層協議的分類,每個協議層都應該確保正確實現自己的協定約定,並且每一層可進行獨立的測試,這才是可維護可擴充套件的系統,因此對於HT客戶遇到問題時,我們一般也是一層層的幫忙梳理找根源,如果向量描述沒問題呈現出錯,那是HT元件庫的問題,如果模擬到Node上的attr資料顯示正確,那就去找找實際執行後臺通訊解析後的資料是否正確的設定到模型上,這樣一步步的分析很容易就能定位到問題的根源。

 

以上三種擴充套件方式各有利弊,我將在下篇中繼續展開分析,本篇結尾上一段該例子在移動終端的執行操作視訊

相關文章