自定義HT for Web之HTML5表格元件的Renderer和Editor

圖撲軟體發表於2015-05-27

HT for Web提供了一下幾種常用的Editor,分別是:

  • slider:拉條
  • color picker:顏色選擇器
  • enum:列舉型別
  • boolean:真假編輯器
  • string:普通的文字編輯器

除了這幾種常用編輯器之外,使用者還可以通過繼承ht.widget.BaseItemEditor類來實現自定義編輯器。

而渲染器,在HT for Web提供常用的Renderer有:

  • enum:列舉型別
  • color:顏色型別
  • boolean:真假渲染器
  • text:文字渲染器

和編輯器一樣也可以自定義渲染器,但是方式不太一樣,渲染器是通過定義column中drawCell()方法來自定義單元格展現效果。

今天我們就來實現一把自定義HTML5表格元件的Renderer和Editor,為了更直觀地演示編輯效果,我們正好利用HT for Web強大的HTML5拓撲圖元件

首先來瞧瞧效果:

 


 

效果圖中,左邊表格的第二列,是定義了一個編輯器,用一個圓盤來表示當前文字的旋轉角度,可以通過拖拉來實現角度變換;表格的第三列,是通過drawCell()方法來繪製單元格內容,中間線標識旋轉角度為零,向左表示文字逆時針旋轉指定角度,向右表示文字順時針旋轉指定角度。

HT for Web的拓撲圖網路節點的文字,簡單修改label.rotation屬性即可實現文字旋轉功能,為了更直觀我特意加上label.background使得網路拓撲圖節點文字具有背景效果。

接下來我們就來看看具體的實現,先來了解下渲染器的實現:

 

 

{
    name : 'label.rotation',
    accessType : 'style',
    drawCell : function(g, data, selected, column, x, y, w, h, tableView) {
        var degree = Math.round(data.s('label.rotation') / Math.PI * 180),
                width = Math.abs(w / 360 * degree),
                begin = w / 2,
                rectColor = '#29BB9C',
                fontColor = '#000',
                background = '#F8F0E5';

        if (selected) {
            rectColor = '#F7F283';
            background = '#29BB9C';
        }
        g.beginPath();
        g.fillStyle = background;
        g.fillRect(x, y, w, h);
        g.beginPath();
        if (degree < 0) begin -= width;
        g.fillStyle = rectColor;
        g.fillRect(x + begin, y, width, h);
        g.beginPath();
        g.font = '12px arial, sans-serif';
        g.fillStyle = fontColor;
        g.textAlign = 'center';
        g.textBaseline = 'middle';
        g.fillText(degree, x + w / 2, y + h / 2);
    }
}

 

 

上面的程式碼就是定義表格第三列的程式碼,可以看到除了定義column自身屬性外,還新增了drawCell()方法,通過drawCell()方法傳遞進來的引數,來繪製自己想要的效果。

 

渲染就是這麼簡單,那麼編輯器就沒那麼容易了,在設計自定義編輯器之前,得先來了解下編輯器的基類ht.widget.BaseItemEditor,其程式碼如下:

 

ht.widget.BaseItemEditor = function (data, column, master, editInfo) {    
    this._data = data;
    this._column = column;
    this._master = master;
    this._editInfo = editInfo;
};
ht.Default.def(‘ht.widget.BaseItemEditor’, Object, {
    ms_ac:["data", "column", "master", "editInfo"],
    editBeginning: function() {},
    getView: function() {},
    getValue: function() {},
    setValue: function() {}
});

 

它處理建構函式中初始化類變數外,就定義了幾個介面,讓使用者過載實現相關業務操作邏輯處理。那麼接下來說說這些介面的具體用意:

editBeginning:在單元格開始編輯前呼叫

  • getView:獲取編輯器view,值型別為DOM元素
  • getValue:獲取編輯器值
  • setValue:設定編輯器值,並做編輯器的頁面初始化操作

在建立一個自定義編輯器的時候,必須實現這些介面,並在不同的介面中,做不同的操作。

現在我們來看看旋轉角度的自定義編輯是如何設計的:

    1. 按照HT for Web元件的設計慣例,我們需要建立一個Div作為view,在view中包含一個canvas元素,元件內容在canvas上繪製;

    2. editor需要與使用者有互動,因此,需要在view上新增事件監聽,監聽使用者有可能的操作,在這次的Demo中,我們希望使用者通過拖拉角度控制盤來控制角度,所以,我們在view上新增了mousedown、mousemove及mouseup三個事件監聽;

    3. 使用者通過拖拉元件可以改變角度,這個改變是連續的,而且在拖拉的時候有可能滑鼠會離開元件區域,要實現離開元件區域也能夠正確的改變值,那麼這時候就需要呼叫HT for Web的startDragging()方法;

 

以上講述的操作都在建構函式中處理,接下來看看建構函式長什麼樣:

 

// 類ht.widget.RotationEditor建構函式
ht.widget.RotationEditor = function(data, column, master, editInfo) {
    // 呼叫父類建構函式初始化引數
    this.getSuperClass().call(this, data, column, master, editInfo);

    var self = this,
        view = self._view = createDiv(1),
        canvas = self._canvas = createCanvas(self._view);
    view.style.boxShadow = '2px 2px 10px #000';

    // 在view上新增mousemove監聽
    view.addEventListener('mousemove', function(e) {
        if (self._state) {
            ht.Default.startDragging(self, e);
        }
    });

    // 在view上新增mousedown監聽
    view.addEventListener('mousedown', function(e) {
        self._state = 1;
        self.handleWindowMouseMove(e);
    });

    // 在view上新增mouseup監聽,做些清理操作
    view.addEventListener('mouseup', function(e) {
        self.clear();
    });
};

 

    4. 接下來就是通過def()方法來定義ht.widget.RotationEditor類繼承於ht.widget.BaseItemEditor,並實現父類的方法,程式碼如下,在程式碼中,我沒有貼出setValue()方法的實現,因為這塊有些複雜,我們單獨抽出來講解;

 

ht.Default.def('ht.widget.RotationEditor', ht.widget.BaseItemEditor, {
    editBeginning : function() {
        var self = this,
            editInfo = self.getEditInfo(),
            rect = editInfo.rect;

        // 編輯前再對元件做一次佈局,避免元件寬高計算不到位
        layout(self, rect.x, rect.y, rect.width, rect.width);
    },
    getView : function() {
        return this._view;
    },
    getValue : function() {
        return this._value;
    },
    setValue : function(val) {
       // 設定編輯器值,並做編輯器的頁面初始化操作
    }
});

 

    5. 我們要在setValue()方法中繪製出文章開頭的效果圖上面展現的效果,大致分解了些,可以分成以下四步來繪製,當然在繪製之前需要線獲得canvas的context物件:

        5.1. 繪製內外圓盤,通過arc()方法繪製兩個間隔10px的同心圓;

        5.2. 繪製值區域,通過結合arc()方法及lineTo()方法繪製一個扇形區域,在通過fill方法填充顏色;

        5.3. 繪製指標,通過lineTo()方法繪製兩個指標;

        5.4. 繪製文字,在繪製文字的時候,不能直接將文字繪製在圓心處,因為圓心處是指標的交匯處,如果直接繪製文字的話,將與指標重疊,這時,通過clearRect()方法來清除文字區域,在通過fillRect()方法將背景填充上去,不然文字區域塊將是透明的,接下來就呼叫fillText()方法繪製文字。

 

這些就是元件繪製的所有邏輯,但是有一點必須注意,在繪製完元件後,必須呼叫下restore()方法,因為在initContext()方法中做了一次save()操作,接下來看看具體實現(程式碼有些長);

 

setValue : function(val) {
    var self = this;
    if (self._value === val) return;

    // 設定元件值
    self._value = val;

    var editInfo = self.getEditInfo(),
        rect = editInfo.rect,
        canvas = self._canvas,
        radius = self._radius = rect.width / 2,
        det = 10,
        border = 2,
        x = radius,
        y = radius;

    // 弧度到角度的轉換
    val = Math.round(val / Math.PI * 180);
    // 設定canvas大小
    setCanvas(canvas, rect.width, rect.width);
    // 獲取畫筆
    var g = initContext(canvas);
    translateAndScale(g, 0, 0, 1);

    // 繪製背景
    g.fillStyle = '#FFF';
    g.fillRect(0, 0, radius * 2, radius * 2);

    // 設定線條顏色及線條寬度
    g.strokeStyle = '#969698';
    g.lineWidth = border;

    // 繪製外圈
    g.beginPath();
    g.arc(x, y, radius - border, 0, Math.PI * 2, true);
    g.stroke();

    // 繪製內圈
    g.beginPath();
    g.arc(x, y, radius - det - border, 0, Math.PI * 2, true);
    g.stroke();

    // 繪製值區域
    var start = -Math.PI / 2,
        end = Math.PI * val / 180 - Math.PI / 2;
    g.beginPath();
    g.fillStyle = 'rgba(255, 0, 0, 0.7)';
    g.arc(x, y, radius - border, end, start, !(val < 0));
    g.lineTo(x, border + det);
    g.arc(x, y, radius - det - border, start, end, val < 0);
    g.closePath();
    // 填充值區域
    g.fill();
    // 繪製值區域末端到圓心的線條
    g.lineTo(x, y);
    g.lineTo(x, det + border);
    g.stroke();

    // 繪製文字
    var font = '12px arial, sans-serif';
    // 計算文字大小
    var textSize = ht.Default.getTextSize(font, '-180');
    // 文字區域
    var textRect = {
        x : x - textSize.width / 2,
        y : y - textSize.height / 2,
        width : textSize.width,
        height : textSize.height
    };
    g.beginPath();
    // 清空文字區域
    g.clearRect(textRect.x, textRect.y, textRect.width, textRect.height);
    g.fillStyle = '#FFF';
    // 補上背景
    g.fillRect(textRect.x, textRect.y, textRect.width, textRect.height);
    // 設定文字樣式
    g.textAlign = 'center';
    g.textBaseline = 'middle';
    g.font = font;
    g.fillStyle = 'black';
    // 繪製文字
    g.fillText(val, x, y);

    // restore()和save()是配對的,在initContext()方法中已經做了save()操作
    g.restore();
}

 

    6. 這時候編輯器的設計就大體完成,那麼編輯器該如何用到表格上呢?很簡單,在表格定義列的時候,加上下面兩行程式碼就可以開始使用編輯器了;

 

editable : true, // 啟動編輯
itemEditor : ‘ht.widget.RotationEditor' // 指點編輯器類

 

介紹到這裡,編輯器可以正常的繪製出來,但是在操作的時候,你會發現,編輯器並不會根據拖拉的位置而改變角度,這是為什麼呢?請看下一點說明:

 

    7. 在建構函式中,view的mousemove事件呼叫了startDragging()方法,其實這個方法是有依賴的,它需要元件過載handleWindowMouseMove()及handleWindowMouseUp()兩個方法。原因很簡單,就如第3點種提到的,使用者在拖拉元件的時候,有可能拖離了元件區域,這時候只能通過window上的mousemove及mouseup兩個事件監聽令使用者繼續操作;

 

// 監聽window的mousemove事件,在view的mousemove事件中,呼叫了startDragging()方法,
// 而startDragging()方法中的實質就是觸發window的mousemove事件
// 該方法計算值的變化,並通過setValue()方法來改變值
handleWindowMouseMove : function(e) {
    var rect = this._view.getBoundingClientRect(),
        x = e.x - rect.left,
        y = e.y - rect.top,
        radius = this._radius,
        // 通過反三角函式計算弧度,再將弧度轉換為角度
        value = Math.round(Math.atan2(y - radius, x - radius) / Math.PI * 180);

    if (value > 90) {
        value = -(180 - value + 90);
    }
    else {
        value = value + 90;
    }
    this.setValue(value / 180 * Math.PI);
},
handleWindowMouseUp : function(e) {
    this.clear();
},
clear : function() {
    // 清楚狀態元件狀態
    delete this._state;
}

 

加上上面的三個方法,執行程式碼可以發現編輯器可以正常編輯了。但是隻有在結束編輯後,才可以在拓撲圖上看到文字旋轉角度變化,如果可以實時更新拓撲圖上的文字旋轉角度,將會更加直觀些,那麼現在該怎麼辦呢?

 

   8.自定義編輯器這塊並像其他已經實現了的編輯器那樣可以指定編輯器的屬性,自定義編輯器能夠指定的就只有一個類名,所以在編輯器上設定引數是沒用的,使用者無法設定到編輯器中。一個偷巧的方法是在column上做手腳,借鑑其他編輯器的設計思想,在column上新增一個名字為_instant的屬性,在程式碼中通過該屬性值來判斷是否要立即更新對應的屬性值,因此只需要在setValue()方法中新增如下程式碼,就能夠實現實時更新屬性值的效果;

 

// 判斷列物件是否設定了_instant屬性
if (column._instant) {
    var table = self.getMaster();
    table.setValue(self.getData(), column, val);
}

 

   9.至此,編輯器的設計已經完成,現在來看看具體的用法,下面的程式碼是Table中具體的列定義,在列定義中,指定itemEditor屬性值,並設定_instant屬性為true,就可以實現編輯器實時更新的效果

 

{
    accessType : 'style',
    name : 'label.rotation',
    editable : true,
    itemEditor : 'ht.widget.RotationEditor',
    _instant : true,
    formatValue : function(value) {
        return Math.round(value / Math.PI * 180);
    }
}

 

程式碼中你會發現定義了一個formatValue()方法,該方法是為了與編輯器中編輯的值型別一致,都將弧度轉換為角度。

在表格的第三列中,通過渲染器自定義了單元格樣式,同時我也為其定義了另外一個編輯器,通過左右拖拉單元格來實現角度的變化,這個編輯器的實現與上面談及的編輯器略有不同,具體的不同之處在於,第三列的編輯器通過HT for Web中定義的ms_listener模組來新增監聽,讓建構函式與互動分離開,看起來更加清晰明瞭。

介紹下ms_listener模組,如果類新增了ms_listener模組,那麼在類中將會多以下兩個方法:

  • addListeners:將類中定義的handle_XXX()方法(XXX代表某個DOM事件名稱,如:mousemove等)作為相應的事件監聽函式新增到元件的view上;
  • removeListeners:將類中定義的handle_XXX()方法對應的事件從view上移除。

那麼類中如何新增ms_listener模組呢,只需要在def()方法中類的方法定義上,新增ms_listener:true這行程式碼,並在方法定義上新增DOM事件對應的handle函式,再在建構函式中呼叫類的addListeners()方法。

具體的程式碼我就不在闡述了,思路與前面講述的編輯器的思路差不多。

 

最後附上程式的所有程式碼,供大家參考,有什麼問題歡迎留言諮詢。

 

相關文章