Now browsing the SCRAPBLOG weblog archives.

カスタムツリービューの基本的な使い方(その1~表示)

何らかのデータをツリー (xul:tree 要素) で表示するためにはいくつかの方法がありますが、最も一般的なのはカスタムツリービュー方式であるかと思います。ここでは、「何らかのデータ」として最も単純な一次元の配列を想定しますが、二次元配列や何らかのオブジェクトの配列など、他のデータ構造にも応用可能です。

fruits.xul

はじめに以下のように tree 要素を配した fruits.xul を作成します。

<?xml version="1.0"?>

<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<page xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
      title="Fruits"
      onload="init();"
      onunload="uninit();">

    <script type="application/x-javascript" src="fruits.js" />

    <tree id="fruitsTree" flex="1">
        <treecols>
            <treecol label="Name" flex="1" primary="true" />
        </treecols>
        <treechildren flex="1" />
    </tree>

</page>

fruits.js

次に、 fruits.js を作成します。はじめに、ツリーに表示したいデータとして、果物の名前の配列 gFruitsData を定義します。配列内の null はツリー上で区切りとして表示する要素です。

var gFruitsData = [
    "Grape",
    "Apple",
    "Orange",
    "Banana",
    null,    // separator
    "Pear",
    "Peach",
    "Strawberry",
    "Cherry",
    "Melon",
    null,    // separator
    "Watermelon",
    "Plum",
    "Papaya",
    "Lemon",
];

次に、後述する FruitsTreeView クラスのインスタンスである gFruitsTreeView と、ウィンドウを開いた直後(onload イベント発生時)に呼び出される init 関数、ウィンドウを閉じる直前(onunload イベント発生時)に呼び出される uninit 関数を定義します。 init 関数では gFruitsData を引数として FruitsTreeView クラスのインスタンスを生成し、グローバル変数 gFruitsTreeView として保持します。これを xul:tree 要素の view プロパティとしてセットすることで、実際にツリーへの表示が行われます。 uninit 関数では null をセットすることで、ツリーへの表示を終了します。

var gFruitsTreeView = null;

function init() {
    gFruitsTreeView = new FruitsTreeView(gFruitsData);
    document.getElementById("fruitsTree").view = gFruitsTreeView;
}

function uninit() {
    document.getElementById("fruitsTree").view = null;
}

nsITreeView インタフェース

ここからは FruitsTreeView クラスを定義します。 FruitsTreeView クラスはコンストラクタの引数として配列形式のデータを受け取り、 _data プロパティとして内部的に参照を保持します。また、 nsITreeView.idl で定義された各プロパティ・メソッドを実装します。ただしすべてのメンバをきちんと実装する必要は無く、ツリーで実現したい機能に応じて適宜実装を加えていくことになります。 nsITreeView メンバの概要は以下の通りです。

メンバ名 概要
rowCount ツリーの行数を表す読み込み専用プロパティ。今回の場合はデータの配列の長さを返せば良い。
selection ツリー上の現在の選択を nsITreeSelection 型オブジェクトとして返す。このプロパティは自前で実装する必要は無い。
getRowProperties
getCellProperties
getColumnProperties
ツリーのセル・列・行に対してクラスを設定し、 CSS を使って見た目の詳細なカスタマイズを行う場合に使用する。今回は使用しない。
isContainer
isContainerOpen
isContainerEmpty
階層構造を有するツリーの場合に実装が必要となるが、今回は階層構造がない単純なツリーであるため使用しない。すべて false を返す。
isSeparator 引数 index の行がセパレータかどうかを表す。今回は前述したとおり、データが null の場合にセパレータとして表示したいので、その行に対応するデータの値が null なら true を返せばよい。
isSorted カラムをクリックして並び替え可能なツリーの場合に使用する。今回は常に未ソート状態なので false を返す。
canDrop ドラッグ&ドロップで移動可能なツリーの場合に使用する。現時点では必要ないので false を返す。
drop ドラッグ&ドロップで移動可能なツリーの場合に使用し、ドロップされたときに呼び出される。
getParentIndex
hasNextSibling
getLevel
これらも階層構造を有するツリーを正しく描画するために必要となる。 getParentIndex は階層構造を持たない場合は常に -1 を返さなければならない。
getImageSrc ブックーマークツリーのようにアイコンなどの画像を表示する場合に使用する。
getProgressMode セルにプログレスバーを表示するツリーを実装する場合に使用する。
getCellValue プログレスバー型のセルおよびチェックボックス型のセルで、その値を返す。
getCellText 引数で指定されたセルの表示文字列の値を返す。引数 row は行番号、 col は nsITreeColumn 型オブジェクトで、 index プロパティから列番号を取得できる。複数の列を有するツリーの場合は列番号に応じた表示文字列から取り出すが、今回は列番号は常に 0 となり、データ中の行番号に応じた要素そのものを返すだけでよい。
setTree ツリービューを xul:tree 要素の view プロパティへセットした際に呼び出される。引数 tree は nsITreeBoxObject 型オブジェクトであるが、この nsITreeBoxObject は後々利用することが多いため、 FruitsTreeView クラスの _treeBoxObject プロパティとして内部的に参照を保持しておく。
引数が null の場合、終了処理として内部的に保持していたいくつかの参照を破棄する。
toggleOpenState 階層構造を有するツリーのフォルダを開閉した時に呼び出される。
cycleHeader カラムをクリックして並び替えが可能なツリーの場合、カラムクリック時に呼び出される。
selectionChanged ツリーの選択が変更された時に何らかの処理を実行したい場合、 xul:tree 要素の onselect=”this.view.selectionChainged();” イベントハンドラをセットして呼び出すようにする?
cycleCell カラムをクリックして並び替えが可能なツリーの場合に使用する?
isEditable 引数で指定されたセルがチェックボックス型で変更可能であれば true を返す。
引数で指定されたセルをダブルクリックしてインライン編集可能であれば true を返す。
isSelectable 詳細不明。特定の行またはセルを選択不可にするためのもの?
setCellValue プログレスバー型のセルおよびチェックボックス型のセルで、その値を変更するためのメソッド。
setCellText インライン編集可能なツリー用。引数で指定されたセルの表示文字列を指定した値に変更するためのメソッド。
performAction
performActionOnRow
performActionOnCell
IDL によればツリー上で Del キーを押下すると引数 action に「delete」が渡されて呼び出されるとあるが、実際はどうやら xul:key 要素を追加するなどして自前で performActionOnRow などを呼び出す必要があるようだ。使い道がよくわからん。

FruitsTreeView クラス

今回はツリーへビューをセットしてデータ(果物の名前と区切り)を表示させるだけなので、きちんと実装する必要があるのは rowCount, isSeparator, getCellText, setTree の4つだけです。その他のメソッドは適宜 return false などにしておきます。

fruits.js
////////////////////////////////////////////////////////////////
// Custom Tree View

function FruitsTreeView(aData) {
    this._data = aData;
}

FruitsTreeView.prototype = {

    /**
     * nsITreeBoxObject
     */
    _treeBoxObject: null,

    ////////////////////////////////////////////////////////////////
    // implements nsITreeView

    get rowCount() {
        return this._data.length;
    },
    selection: null,
    getRowProperties: function(index, properties) {},
    getCellProperties: function(row, col, properties) {},
    getColumnProperties: function(col, properties) {},
    isContainer: function(index) { return false; },
    isContainerOpen: function(index) { return false; },
    isContainerEmpty: function(index) { return false; },
    isSeparator: function(index) {
        return this._data[index] == null;
    },
    isSorted: function() { return false; },
    canDrop: function(targetIndex, orientation, dataTransfer) { return false; },
    drop: function(targetIndex, orientation, dataTransfer) {},
    getParentIndex: function(rowIndex) { return -1; },
    hasNextSibling: function(rowIndex, afterIndex) { return false; },
    getLevel: function(index) { return 0; },
    getImageSrc: function(row, col) {},
    getProgressMode: function(row, col) {},
    getCellValue: function(row, col) {},
    getCellText: function(row, col) {
        switch (col.index) {
            case 0: return this._data[row];
        }
    },
    setTree: function(tree) {
        if (tree) {
            // initialize view
            this._treeBoxObject = tree;
        }
        else {
            // finalize view
            this._treeBoxObject = null;
            this._data = null;
        }
    },
    toggleOpenState: function(index) {},
    cycleHeader: function(col) {},
    selectionChanged: function() {},
    cycleCell: function(row, col) {},
    isEditable: function(row, col) { return false; },
    isSelectable: function(row, col) {},
    setCellValue: function(row, col, value) {},
    setCellText: function(row, col, value) {},
    performAction: function(action) {},
    performActionOnRow: function(action, row) {},
    performActionOnCell: function(action, row, col) {},

};

関連記事

TOP

Custom Tree View でドラッグ&ドロップ時に固まる

Custom Tree Views を使って階層構造がない単純なデータを表示するツリーを作成し、ツリーアイテムのドラッグ&ドロップによる並び替えを実装しようとしたところ、ドロップ時に Firefox が固まる問題が発生。

原因は nsITreeView#getParentIndex で「return -1;」していなかったことであることに気付くまで、3時間くらい無駄にした。

CustomTreeView.prototype = {
    ...
    getParentIndex: function(rowIndex) { return -1; },
    ...
};

また、 nsDragAndDrop.js を使ってドラッグしたときの転送データを生成する際、データフレーバに「text/plain」とすると、なぜか実際に転送データの中身が文字化けしたようなデータになってしまうという問題にも悩まされた。これは、データフレーバを「text/unicode」にすることで解決した。

gDragAndDropObserver = {
    ...
    onDragStart: function(aEvent, aXferData, aDragAction) {
        aXferData.data = new TransferData();
        aXferData.data.addDataForFlavour("text/unicode", "Hello!");
        aDragAction.action = Ci.nsIDragService.DRAGDROP_ACTION_MOVE;
    },
    ...
};

TOP