Bien, j'espère que vous êtes au point sur les concepts de la programmation orientée objet et du C++. Les choses vont devenir un peu plus intéressantes.
Premièrement, il faut savoir (si vous ne vous en étiez pas encore rendu compte) que nous avons bénéficié des avantages du polymorphisme dès les premiers chapitres de ce tutoriel. En effet,
tous les scene node sont des dérivés de la classe abstraite
irr::scene::ISceneNode. Mais pour expliquer le fonctionnement de la chose, il est plus simple de commencer avec la GUI.
Si vous avez bonne mémoire, vous devez vous souvenir à quel point il est simple et agréable d'ajouter un nouvel élément à l'environnement de GUI. On fait appel à une méthode, et pouf ! Le nouvel élément est tout de suite pris en charge, pas besoin de s'occuper de la mise à jour, ni de la gestion des events, ni de quoi que ce soit d'autre.
On peut donc en déduire que l'environnement de GUI met à jour lui même tous les éléments qui y sont attachés. Mais déjà,
comment fait-il pour "s'attacher" des éléments ? L'autre problème est que chaque élément a un comportement différent, un bouton doit avoir une apparence différente si on clique dessus (changement d'image de fond par exemple) alors qu'une liste déroulante doit être déroulée, etc...
Comment l'environnement de GUI sait-il quelle fonction appeler pour faire la mise à jour de l'élément ?
La réponse à cette deuxième question est simple : il appelle toujours la même !
Tous les éléments de la GUI sont des dérivés de la classe abstraite
irr::gui::IGUIElement. (Jetez un oeil au diagramme en haut de la page de doc pour vous en convaincre). Et il se trouve que cette classe possède des méthodes purement virtuelles permettant la mise à jour graphique de l'élément (
draw) ainsi que la gestion des événements (
OnEvent).
De cette manière, chaque élément de la GUI (bouton, edit box, boîte de dialogue...) redéfinit ces méthodes selon ses besoins, et l'environnement de GUI n'a plus qu'à les appeler à chaque calcul du rendu.
Reste la question de "l'attachement". Comment fait l'environnement de GUI pour stocker des pointeurs vers tous les éléments qui lui sont associés ? Et bien en fait, ce n'est pas l'environnement de GUI qui stocke tous les éléments, mais
les éléments qui se stockent eux mêmes.
Je m'explique : chaque élément possède obligatoirement un élément parent, et un nombre potentiellement illimité d'éléments enfants.
Vous allez me dire qu'il est possible par exemple d'ajouter un bouton via
irr::gui::IGUIEnvironment::addButton sans passer d'argument au paramètre concernant l'élément parent. Et vous avez tout à fait raison mais ce n'est pas pour autant que le bouton n'en a pas. Par défaut, il sera attaché au
rootGUIElement, qui est comme son nom l'indique, l'élément racine à la base de tout ce que vous ajoutez à la GUI.
Chaque élément possède donc en attribut un pointeur de type IGUIElement vers son élément parent.
On peut le constater à la ligne 875 du fichier
IGUIElement.h :
Code : C++
Reste la question des enfants. Comment fait un élément pour stocker un nombre illimité d'accès vers des éléments enfants ? Tout bêtement en se servant d'une liste chaînée. Il s'agit là aussi d'un attribut visible dans le fichier IGUIElement.h, 2 lignes au dessus de Parent :
Code : C++ | core::list<IGUIElement*> Children;
|
Histoire d'achever ceux qui commencent à se dire qu'ils vont sauter le chapitre, je vais vous faire une révélation digne d'un soap opera : le guiRootElement et l'environnement de GUI sont en fait... une seule et même classe !
C'est assez simple à comprendre en fait. La classe IGUIEnvironment n'est en réalité qu'une interface (comme l'indique le I de son nom). Aussi ce n'est pas cette classe que nous instancions en utilisant la méthode
getGUIEnvironment du device, mais CGUIEnvironment (inutile de chercher dans la doc, elle n'est pas référencée).
Cette classe dérive bien évidemment de IGUIEnvironment, mais aussi de IGUIElement. De cette manière, lorsque vous ajoutez votre bouton en ne spécifiant pas de parent, l'environnement de GUI (l'instance de CGUIEnvironment) stocke tout simplement un pointeur vers l'élément créé dans sa propre liste d'éléments enfants, car c'est lui le guiRootElement.
D'ailleurs, si vous faites appel à la méthode
getRootGUIElement de l'environnement de GUI, vous vous retrouverez avec un pointeur qui pointe vers... l'environnement de GUI ! La preuve par le code (tiré du fichier CGUIEnvironment.cpp, ligne 1465) :
Code : C++ | IGUIElement* CGUIEnvironment::getRootGUIElement()
{
return this;
}
|
Maintenant que nous savons comment sont stockés les éléments, et qu'on sait quelles méthodes les mettent à jour, reste à savoir de quelle manière ces méthodes sont appelées. Tout part bien entendu de l'environnement de GUI. Vous n'êtes pas sans savoir que pour que la GUI soit mise à jour à l'écran, il faut appeler la méthode
drawAll de l'environnement de GUI à l'intérieur de la boucle de rendu.
Examinons un peu le contenu de cette méthode (tiré du fichier CGUIEnvironment.cpp, ligne 181) :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | void CGUIEnvironment::drawAll()
{
if (Driver)
{
core::dimension2d<s32> dim = Driver->getScreenSize();
if (AbsoluteRect.LowerRightCorner.X != dim.Width ||
AbsoluteRect.LowerRightCorner.Y != dim.Height)
{
// resize gui environment
DesiredRect.LowerRightCorner.X = Driver->getScreenSize().Width;
DesiredRect.LowerRightCorner.Y = Driver->getScreenSize().Height;
AbsoluteClippingRect = DesiredRect;
AbsoluteRect = DesiredRect;
updateAbsolutePosition();
}
}
// make sure tooltip is always on top
if (ToolTip.Element)
bringToFront(ToolTip.Element);
draw();
OnPostRender ( os::Timer::getTime () );
}
|
La seule ligne qui nous intéresse est la 22, l'appel à la fonction draw. (Je vous laisse vous casser les dents sur le reste de la fonction, c'est du code corsé bien comme on l'aime

).
La fonction draw est une méthode virtuelle de IGUIElement, et comme son nom l'indique,
elle sert à dessiner l'élément ainsi que tous ses éléments enfants. Etant donné que c'est le guiRootElement qui l'appelle, tous les éléments de la GUI vont être dessinés. Voici le code tiré du fichier IGUIElement.h :
Code : C++ | virtual void draw()
{
if (!IsVisible)
return;
core::list<IGUIElement*>::Iterator it = Children.begin();
for (; it != Children.end(); ++it)
(*it)->draw();
}
|
On commence par vérifier ligne 3 que l'élément doit bien être dessiné. Ligne 6, on crée un itérateur pour parcourir la liste des éléments qui sont rattachés à celui-ci. Puis on appelle la méthode draw de chaque élément. Evidemment, chacun des éléments de cette liste va à son tour appeler les méthodes draw de chacun de ses éléments enfants, et ainsi de suite jusqu'à ce que tous les éléments aient été passés en revue.
Si vous avez bien suivi, vous devriez vous demander quel est l'intérêt de cette méthode puisqu'elle ne fait que s'appeler d'élément en élément, et... c'est tout. En regardant le prototype de la fonction, on s'aperçoit qu'elle est virtuelle. Et qu'
elle est donc tout naturellement redéfinie pour chaque type d'élément. C'est ce que nous disions plus haut à propos des méthodes de mise à jour. Ce sont toujours les mêmes qui sont appelées, mais elles sont redéfinies selon les classes.
Le code qu'on voit plus haut reste quelle que soit la classe, mais le code permettant le dessin de l'élément change selon le type de celui-ci. Un bouton ne sera pas dessiné de la même manière qu'une edit box ou qu'un static text. Chacun de ces types d'élément possède sa propre classe dérivée de IGUIElement qui redéfinit draw selon ses besoins. Etant donné que le guiRootElement n'est pas visible, il n'a pas besoin de code pour le dessin.