Écrit par Neil Deakin
,
mise à jour par les contributeurs à MDC
.
Traduit par Romain D. (03/05/2005), mise à jour par Alain B. (04/04/2007) .
Page originale :
http://developer.mozilla.org/en/docs/XUL_Tutorial/Tree_View_Details
Attention : Ce tutoriel est ancien et n'est pas mis à jour. Bien que beaucoup d'informations soient encore valables pour les dernières versions de gecko, beaucoup sont aussi obsolètes. Il est préférable d'aller consulter cette page sur la version française de ce tutoriel sur developer.mozilla.org.
Cette section décrit quelques fonctionalités supplémentaires des vues d'arbre.
Dans la section précédente, nous avons créé une vue d'arbre simple qui était implémentée avec un minimum de fonctionnalités. À présent, regardons quelques fonctions supplémentaires que les vues peuvent implémenter. Ici, nous examinerons comment créer un ensemble hiérarchique d'items utilisant la vue. C'est un processus relativement astucieux qui implique de conserver une trace des items qui sont des enfants et une trace de l'état des lignes, ouvertes ou fermées.
Chaque ligne dans l'arbre possède un niveau d'imbrication. Les lignes les plus hautes
ont un niveau 0, les enfants de ces lignes ont un niveau 1, leurs enfants le niveau 2
et ainsi de suite. L'arbre interroge la vue pour chaque ligne en appelant sa méthode
getLevel()
pour connaître le niveau de cette ligne. La vue devra retourner 0
pour les premiers parents et des valeurs plus élevées pour les lignes intérieures.
L'arbre utilisera cette information pour déterminer la structure hiérarchique de ces
lignes.
En complément de la méthode getLevel()
, la fonction hasNextSibling()
retourne pour une ligne donnée la valeur true si elle est suivie d'une autre ligne de même niveau qu'elle.
Cette fonction est spécifiquement utilisée pour dessiner l'imbrication des lignes sur le côté de la vue de l'arbre.
La méthode getParentIndex()
est supposée retourner la ligne parente d'une ligne donnée,
c'est-à-dire : la ligne précédente qui a un niveau d'imbrication inférieur. Toutes ces
méthodes doivent être implémentées par la vue pour que les enfants soient manipulés correctement.
Trois autres fonctions, isContainer
, isContainerEmpty
et
isContainerOpen
sont utilisées pour manipuler un item parent dans l'arbre.
isContainer
doit retourner true si une ligne est un conteneur pouvant contenir des enfants.isContainerEmpty
doit renvoyer true si une ligne est un conteneur vide, par exemple, un répertoire/dossier qui ne contient aucun fichier.isContainerOpen
sert à déterminer quel item est ouvert ou fermé. La vue a besoin d'en conserver une trace. L'arbre appellera cette méthode pour déterminer quels conteneurs sont ouverts et lesquels sont fermés.Notez que l'arbre n'appellera ni isContainerEmpty
, ni isContainerOpen
pour les lignes
qui ne sont pas conteneurs en se basant sur la valeur de retour de la méthode
isContainer
.
Un conteneur peut être affiché différemment d'un non-conteneur. Par exemple, un conteneur peut avoir un icône de dossier devant lui. Une feuille de styles peut être utilisée pour personnaliser l'aspect des items en se basant sur diverses propriétés telles que l'ouverture d'une ligne conteneur. La stylisation sera décrite dans une prochaine section. Un conteneur non vide sera agrémenté d'une poignée (NdT : "twisty", petit '+' ou '-' ou un triangle sur les Macintosh) permettant à l'utilisateur d'ouvrir ou de fermer la ligne pour voir les items enfants. Les conteneurs vides n'auront pas de poignées, mais seront toujours considérés comme des conteneurs.
Lorsque l'utilisateur clique sur la poignée pour ouvrir une ligne, l'arbre appellera
la méthode toggleOpenState()
. La vue met alors en ½uvre les opérations
nécessaires pour intégrer les lignes enfants et mettre à jour l'arbre avec les nouvelles
lignes.
Voici un récapitulatif des méthodes nécessaires pour implémenter des vues hiérarchiques :
getLevel(ligne)
hasNextSibling(ligne, apresIndex)
getParentIndex(ligne)
isContainer(ligne)
isContainerEmpty(ligne)
isContainerOpen(ligne)
toggleOpenClose(ligne)
L'argument apresIndex
de la fonction hasNextSibling
est utilisée
pour une raison d'optimisation, afin de démarrer la recherche à partir de la prochaine ligne s½ur
(ligne de même niveau d'imbrication). Par exemple, l'appelant pourrait déjà connaître la position de la prochaine
ligne s½ur. Imaginez une situation où une ligne possède des sous-lignes et que ces
sous-lignes aient des lignes enfants dont quelques-unes sont ouvertes. Dans ce cas, la détermination
de l'index de la prochaine ligne s½ur prendrait du temps dans certaines implémentations.
Voyons tous ces points dans un exemple simple qui construit un arbre à partir d'un tableau. Cet arbre ne supporte qu'un niveau parent avec un seul niveau enfant, mais il est possible de l'étendre facilement avec d'autres niveaux. Nous l'examinerons portion par portion.
<window onload="init();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<tree id="elementList" flex="1">
<treecols>
<treecol id="element" label="Élément" primary="true" flex="1"/>
</treecols>
<treechildren/>
</tree>
</window>
Nous utilisons un arbre simple qui ne contient pas de données dans
treechildren
.
La fonction init
est appelée au chargement de la fenêtre pour initialiser l'arbre.
Elle définit simplement la vue personnalisée en récupérant l'arbre et en définissant sa
propriété view
. Nous définirons treeView
plus tard.
function init() {
document.getElementById("elementList").view = treeView;
}
La vue d'arbre personnalisée aura besoin d'implémenter un certain nombre de méthodes dont les plus importantes seront examinées individuellement. Cet arbre supporte un seul niveau de parenté avec un niveau enfant interne, mais il peut être étendu pour intégrer sans trop d'effort des niveaux supplémentaires. Tout d'abord, nous définirons deux structures pour conserver les données de l'arbre, la première contiendra une carte relationnelle entre les parents et leurs éventuels enfants, et la seconde contiendra un tableau des lignes visibles. Souvenez-vous qu'une vue doit conserver elle-même une trace des lignes visibles.
var treeView = {
childData : {
Solides: ["Argent", "Or", "Plomb"],
Liquides: ["Mercure"],
Gaz: ["Hélium", "Azote"]
},
visibleData : [
["Solides", true, false],
["Liquides", true, false],
["Gaz", true, false]
],
La structure childData
contient un tableau des enfants pour chacun des trois n½uds
parents. Le tableau visibleData
commence avec seulement trois items visibles, les trois
items de haut niveau. Des items seront ajoutés et supprimés depuis ce tableau quand les
items sont ouverts ou fermés. Le principe est le suivant : lorsqu'une ligne parente est ouverte,
ses enfants sont récupérés depuis la carte childData
et insérés dans le tableau visibleData
.
Par exemple, si la ligne Liquides est ouverte, son enfant unique dans le tableau childData
, l'enfant Mercure, sera inséré dans le tableau
visibleData
après Liquides mais avant Gaz. La taille du tableau sera incrémentée de un.
Les deux valeurs booléennes présentes dans chaque ligne dans la structure visibleData
indiquent respectivement si une ligne est un conteneur et si elle est ouverte. Évidemment, le nouvel enfant inséré
aura ces deux valeurs initialisées à false.
Ensuite, nous avons besoin d'implémenter l'interface de vue de l'arbre. Tout d'abord, les fonctions simples :
treeBox: null,
selection: null,
get rowCount() { return this.visibleData.length; },
setTree: function(treeBox) { this.treeBox = treeBox; },
getCellText: function(idx, column) { return this.visibleData[idx][0]; },
isContainer: function(idx) { return this.visibleData[idx][1]; },
isContainerOpen: function(idx) { return this.visibleData[idx][2]; },
isContainerEmpty: function(idx) { return false; },
isSeparator: function(idx) { return false; },
isSorted: function() { return false; },
isEditable: function(idx, column) { return false; },
La fonction rowCount
retournera la taille du tableau visibleData
. Notez qu'elle
devrait retourner le nombre courant de lignes visibles, pas le nombre total de lignes. Donc, au début,
seulement trois items sont visibles et la valeur retournée par rowCount
devrait être
trois, même si six lignes sont cachées.
La fonction setTree
sera appelée pour définir l'objet boîte de l'arbre. L'objet boîte
de l'arbre est un type spécialisé d'objet boîte propre aux arbres qui sera examiné
en détail dans la prochaine section. Il est utilisé pour aider à la représentation
graphique de l'arbre. Dans cet exemple, nous avons seulement besoin d'une fonction de
l'objet boîte capable de redessiner l'arbre quand des items sont ajoutés ou
supprimés.
Les fonctions getCellText
, isContainer
et isContainerOpen
retournent juste l'élément
correspondant dans le tableau visibleData
. Enfin, les fonctions restantes peuvent
retourner false puisque nous n'avons pas besoin de leurs fonctionnalités. Si nous avions
eu des lignes parents sans enfant, nous aurions implémenté la fonction isContainerEmpty
pour quelle retourne true pour ces éléments.
getParentIndex: function(idx) {
if (this.isContainer(idx)) return -1;
for (var t = idx - 1; t >= 0 ; t--) {
if (this.isContainer(t)) return t;
}
},
La fonction getParentIndex
sera nécessaire pour retourner l'index du parent d'un item donné.
Dans notre exemple simple, il y a seulement deux niveaux d'imbrication, donc nous savons que les conteneurs
n'ont pas de parents, la valeur -1 est retournée pour ces items. Dans le cas contraire, nous devons
parcourir les lignes en arrière pour rechercher celle qui est un conteneur. Ensuite,
la fonction getLevel
.
getLevel: function(idx) {
if (this.isContainer(idx)) return 0;
return 1;
},
La fonction getLevel
est simple. Elle retourne juste 0 pour une ligne conteneur et 1
pour une ligne non-conteneur. Si nous voulions ajouter un niveau d'imbrication supplémentaire,
ces lignes enfants auraient un niveau de 2.
hasNextSibling: function(idx, after) {
var thisLevel = this.getLevel(idx);
for (var t = idx + 1; t < this.visibleData.length; t++) {
var nextLevel = this.getLevel(t);
if (nextLevel == thisLevel) return true;
else if (nextLevel < thisLevel) return false;
}
},
La fonction hasNextSibling
doit retourner true quand une ligne donnée est suivie d'une ligne de même niveau (une s½ur). Le code ci-dessus utilise une méthode basique qui consiste à parcourir les
lignes après celle donnée, en retournant true si une ligne de même niveau est trouvée et false
si une ligne de niveau inférieur est rencontrée.
Dans cet exemple simple, cette méthode est bonne, mais un arbre avec davantage de données
aura besoin d'utiliser une méthode optimisée pour déterminer s'il existe une ligne suivante s½ur.
La dernière fonction est toggleOpenState
. C'est la plus complexe. Elle a besoin de
modifier le tableau visibleData
lorsqu'une ligne est ouverte ou fermée.
toggleOpenState: function(idx) {
var item = this.visibleData[idx];
if (!item[1]) return;
if (item[2]) {
item[2] = false;
var thisLevel = this.getLevel(idx);
var deletecount = 0;
for (var t = idx + 1; t < this.visibleData.length; t++) {
if (this.getLevel(t) > thisLevel) deletecount++;
else break;
}
if (deletecount) {
this.visibleData.splice(idx + 1, deletecount);
this.treeBox.rowCountChanged(idx + 1, -deletecount);
}
}
else {
item[2] = true;
var label = this.visibleData[idx][0];
var toinsert = this.childData[label];
for (var i = 0; i < toinsert.length; i++) {
this.visibleData.splice(idx + i + 1, 0, [toinsert[i], false]);
}
this.treeBox.rowCountChanged(idx + 1, toinsert.length);
}
},
D'abord nous vérifions si la ligne est un conteneur. Si elle ne l'est pas, la fonction retourne juste que les non-conteneurs ne peuvent pas être ouverts ou fermés. Comme le troisième élément du tableau (celui avec l'index 2) indique si une ligne est ouverte ou fermée, nous utilisons deux blocs de code, le premier pour fermer une ligne et le second pour ouvrir une ligne. Examinons chaque bloc de code, mais en commençant par le second, chargé d'ouvrir une ligne.
item[2] = true;
var label = this.visibleData[idx][0];
var toinsert = this.childData[label];
for (var i = 0; i < toinsert.length; i++) {
this.visibleData.splice(idx + i + 1, 0, [toinsert[i], false]);
}
this.treeBox.rowCountChanged(idx + 1, toinsert.length);
La première ligne de code définit la ligne item comme étant ouverte dans le tableau,
ainsi le prochain appel de la fonction toggleOpenState
saura qu'elle doit fermer la ligne. Ensuite, regardons les données pour la ligne dans la carte childData
.
Le résultat est que la variable 'toinsert' sera définie avec un des tableaux enfants, par exemple
["Argent", "Or", "Plomb"] si la ligne Solides est celle qu'on demande d'ouvrir.
Ensuite, nous utilisons la fonction de tableau splice
pour insérer une nouvelle ligne
pour chaque item. Pour Solides, trois items seront insérés.
Enfin, la fonction de boîte d'arbre rowCountChanged
a besoin d'être appelée.
Rappelez-vous que l'objet treeBox
est un objet de boîte d'arbre qui a été défini plus tôt par un appel de
la fonction setTree
. L'objet de boîte d'arbre sera créé par l'arbre pour vous et vous
pourrez appeler ses fonctions. Dans ce cas, nous utilisons la fonction rowCountChanged
pour informer l'arbre que quelques lignes de données ont été ajoutées.
L'arbre redessinera son contenu avec pour résultat que les lignes
enfants apparaîtront à l'intérieur du conteneur. Les autres fonctions
implémentées ci-dessus, telles que getLevel
et isContainer
, sont utilisées par l'arbre
pour déterminer son affichage.
La fonction rowCountChanged
prend deux arguments, l'index de la ligne où doit se faire l'insertion et le nombre de lignes à insérer. Dans le code ci-dessus nous indiquons
que la ligne de départ est la valeur de idx + 1, elle sera la première ligne enfant
sous le parent. L'arbre utilisera cette information et ajoutera l'espace nécessaire pour le nombre
approprié de lignes en poussant les lignes suivantes vers le bas. Assurez-vous de fournir
le nombre correct, ou l'arbre pourrait se redessiner incorrectement ou essayer de dessiner
plus de lignes que nécessaire.
Le code suivant est utilisé pour supprimer des lignes quand une ligne est fermée.
item[2] = false;
var thisLevel = this.getLevel(idx);
var deletecount = 0;
for (var t = idx + 1; t < this.visibleData.length; t++) {
if (this.getLevel(t) > thisLevel) deletecount++;
else break;
}
if (deletecount) {
this.visibleData.splice(idx + 1, deletecount);
this.treeBox.rowCountChanged(idx + 1, -deletecount);
}
Premièrement, l'item est déclaré fermé dans le tableau. Ensuite, nous scannons les lignes suivantes jusqu'à ce que nous atteignions une ligne de même niveau. Toutes celles qui ont un niveau supérieur auront besoin d'être supprimées, mais une ligne de même niveau sera le prochain conteneur qui ne devra pas être supprimé.
Enfin, nous utilisons la fonction splice
pour supprimer les lignes du tableau
visibleData
et appelons la fonction rowCountChanged
pour redessiner l'arbre. Lors de
la suppression des lignes, vous aurez besoin de fournir un chiffre négatif correspondant au nombre de
lignes à supprimer.
Il existe plusieurs autres fonctions de vue pouvant être implémentées mais nous n'en avons pas l'utilité dans cet exemple, donc nous créons des fonctions qui ne font rien ici. Elles sont placées à la fin de notre exemple complet :
Exemple 8.5.1 : Source
<?xml version="1.0" encoding="iso-8859-1" ?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window onload="init();"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<tree id="elementList" flex="1">
<treecols>
<treecol id="element" label="Élément" primary="true" flex="1"/>
</treecols>
<treechildren/>
</tree>
<script>
<![CDATA[
var treeView = {
childData : {
Solides: ["Argent", "Or", "Plomb"],
Liquides: ["Mercure"],
Gaz: ["Hélium", "Azote"]
},
visibleData : [
["Solides", true, false],
["Liquides", true, false],
["Gaz", true, false]
],
treeBox: null,
selection: null,
get rowCount() { return this.visibleData.length; },
setTree: function(treeBox) { this.treeBox = treeBox; },
getCellText: function(idx, column) { return this.visibleData[idx][0]; },
isContainer: function(idx) { return this.visibleData[idx][1]; },
isContainerOpen: function(idx) { return this.visibleData[idx][2]; },
isContainerEmpty: function(idx) { return false; },
isSeparator: function(idx) { return false; },
isSorted: function() { return false; },
isEditable: function(idx, column) { return false; },
getParentIndex: function(idx) {
if (this.isContainer(idx)) return -1;
for (var t = idx - 1; t >= 0 ; t--) {
if (this.isContainer(t)) return t;
}
},
getLevel: function(idx) {
if (this.isContainer(idx)) return 0;
return 1;
},
hasNextSibling: function(idx, after) {
var thisLevel = this.getLevel(idx);
for (var t = idx + 1; t < this.visibleData.length; t++) {
var nextLevel = this.getLevel(t);
if (nextLevel == thisLevel) return true;
else if (nextLevel < thisLevel) return false;
}
},
toggleOpenState: function(idx) {
var item = this.visibleData[idx];
if (!item[1]) return;
if (item[2]) {
item[2] = false;
var thisLevel = this.getLevel(idx);
var deletecount = 0;
for (var t = idx + 1; t < this.visibleData.length; t++) {
if (this.getLevel(t) > thisLevel) deletecount++;
else break;
}
if (deletecount) {
this.visibleData.splice(idx + 1, deletecount);
this.treeBox.rowCountChanged(idx + 1, -deletecount);
}
}
else {
item[2] = true;
var label = this.visibleData[idx][0];
var toinsert = this.childData[label];
for (var i = 0; i < toinsert.length; i++) {
this.visibleData.splice(idx + i + 1, 0, [toinsert[i], false]);
}
this.treeBox.rowCountChanged(idx + 1, toinsert.length);
}
},
getImageSrc: function(idx, column) {},
getProgressMode : function(idx,column) {},
getCellValue: function(idx, column) {},
cycleHeader: function(col, elem) {},
selectionChanged: function() {},
cycleCell: function(idx, column) {},
performAction: function(action) {},
performActionOnCell: function(action, index, column) {},
getRowProperties: function(idx, column, prop) {},
getCellProperties: function(idx, column, prop) {},
getColumnProperties: function(column, element, prop) {}
};
function init() {
document.getElementById("elementList").view = treeView;
}
]]></script>
</window>
Ensuite, nous verrons plus en détails l'objet de boîte d'arbre.
Mozilla® est une marque déposée de la fondation Mozilla.
Mozilla.org™, Firefox™, Thunderbird™, Mozilla Suite™ et XUL™
sont des marques de la fondation Mozilla.