Openframeworks . Créer une classe (et plein d'objets)

2008.10.15

Douglas Edric Stanley

Dans ce cours, nous allons créer notre propre class : comme dans les cours sur Processing, une classe qui servira à dessiner une ellipse sur l'écran. Nous commencerons par créer un seul objet à partir de cette classe. Ensuite, on remplacera cet objet unique pour une suite d'objets contenus dans une liste.

Si vous n'avez jamais vu les notions de « class » ou d'« objet », je suggère que vous révisez le cours sur Processing concernant ce sujet : Programmation orientée-objet, avant d'attaquer ce cours.

Aussi, plus loin nous allons utiliser des listes pour controller plusieurs objets à la fois. Si vous ne savez pas comment créer un « array », reportez-vous sur le cours sur les Listes dans Processing.

Nouveau projet

Comme dans tout projet OpenFrameworks, commencez par copier un projet déjà existant, ici le projet _empyExample.

Notez que vous pouvez aussi créer un autre dossier à côté du dossier des examples

...et mettre cette copie dedans.

Ceci vous permet de gérer vos propres projets et de ne pas les mélanger avec les exemples. Le seul hic, c'est qu'il faut absoluement dans ce cas respecter la hierarchie des dossiers vis-à-vis les bibliothèques lib et addons. C'est pour cette raison que nous avons créer notre (nos) dossiers à côté du dossier examples et non pas ailleurs.

Ouvrez ensuite le fichier du « Project file » (.xcodeproj sur mac) pour ouvrir votre projet dans votre éditeur de code. Appuyez sur « Build and Run » pour être sûr que vous avez bien copié votre projet.

Créer les fichiers de la classe (xCode)

Nous allons maintenant créer deux fichiers. Nous allons créer un fichier « header » qui se nommera sous la forme nomDufichier.h, et un fichier « implementation » qui se nommera sous la forme nomDufichier.cpp. Le premier fichier sera le fichier qui déclare notre classe, déclare ses variables, et déclare ses méthodes. Le deuxième implemente cette classe, en explicant comment marche les variables et méthodes dans les objets créés à partir de cette classe.

Dans xCode, ces deux fichiers (header et implementation) se créent en même temps. Ouvrez l'onglet du dossier « src » dans votre projet xCode. Nous allons créer nos fichiers dedans. Vous avez deux options: 1) faire un clic gauche et sélectionnez  Ajouter > Nouveau fichier , ou 2) sélectionner dans le menu fichier > Nouveau fichier.

Ensuite, vous allez voir une fenêtre présentant différent types de fichiers (qui ne ressemblera peut-être pas à 100% ma photo d'écran). Il suffit de sélectionner dans cette liste l'option C++ file (elle offrira le choix d'ajouter un fichier.h tout de suite après).

Vous aller donner un nom à votre fichier et je recommande de lui donner le même nom que votre classe. Vérifiez aussi qu'il a coché automatiquement la création d'un fichier Balle.h pour accompagner votre Balle.cpp. Attention aussi de noter le nom exacte pour plus tard, y compris si vous avez utilisez des majuscules ou minuscules pour le nom.

Vous devriez enfin voir les deux fichiers de votre classe à l'intérieur de votre projet. Si d'ailleurs vous voulez voir le code de vos fichiers dans l'éditeur sur la droite comme dans ma capture d'écran, il suffit d'appuyer sur l'icône de l'éditeur, ou appuyer sur Cmd-maj-E pour « Editeur de code ».

Créer les fichiers de la classe (CodeBlocks)

Nous allons maintenant créer deux fichiers. Nous allons créer un fichier « header » qui se nommera sous la forme nomDufichier.h, et un fichier « implementation » qui se nommera sous la forme « nomDufichier.cpp ». Le premier fichier sera le fichier qui déclare notre classe, déclare ses variables, et déclare ses méthodes. Le deuxième implemente cette classe, en explicant comment marche les variables et méthodes dans les objets créés à partir de cette classe.

Dans CodeBlocks, on doit créer ces deux fichiers (header et implementation) séparément. Ouvrez l'onglet du dossier src dans votre projet CodeBlocks. Nous allons essayer de finir avec nos deux fichiers dedans, même si CodeBlocks va temporairement essayer de nous créer une arborescence bien bordellique -- pas d'inquiétudes, on le rangera bien vite.

Pour commencer, sélectionnez file > New > file d'un des deux menus.

Sélectionner à gauche l'options files ce qui devrait faire apparaître trois choix : C/C++ Header, C/C++ Source, et Empty file. Sélectionnez C/C++ Header.

Ensuite, ouvrez le petit bouton ... et donner le nom Balle à votre fichier. Vérifiez bien que vous êtes en train de sauvegarder votre fichier Balle.h à l'intérieur du dossier src de votre projet. Attention!!! Il faut absolument cocher les deux options release et debug. Si vous ne cochez pas ces cases, votre classe ne sera pas inclus dans votre projet lorsque vous créez et executez votre projet (pas bien ça). Regardez bien ma capture d'écran pour être sûr de savoir de quoi je parle. Si après tout ça, vous ne cochez pas, écoutez, ce n'est plus mon problème; on peut les amener à l'eau mais on ne pas les faire boire...

Ok. Ça y est, vous avez coché les petites cases comme un bon mouton citoyen, et CodeBlocks vous a généré votre fichier. Par contre, regardez-bien comme il l'a placé: c'est moche comme tout, à l'intérieur d'une arborescence pas possible de sous-dossiers. Réctifiez bien cette mochetté comme dans l'illustration en glissant votre fichier Balle.h sous l'icône src.

Ok. On a créé le fichier « header ». Il faut maintenant créer celui du code. Faites pareil que tout à l'heure, file > New > file et sélectionnez C/C++ Source.

Il vous demanderez si vous voulez créer des codes en langage C ou en langage C++. Choisissez C++.

Comme avant, vérifiez bien que vous sauvegardez dans votre dossier src et donnez le nom de Balle à votre fichier.

N'oubliez pas de cocher les cases release et debug. Attention!!! Il faut absolument cocher les deux options release et debug. Yada yada yada...

Vérifiez aussi que votre fichier s'appelle Balle.cpp et se trouve dans le dossier src.

Ok. Génial. On a généré notre fichier Balle.cpp. Achk! C'est quoi cette @#&§% !? CodeBlocks a créé une arborescence pas possible pour mon fichier. Ugh. Bon, d'accord, comme avant, sortez ce fichier de là-dedans et replacez-le à l'intérieur de l'icône src.

Définir la classe

Ouvrez votre fichier Balle.h. Si vous êtes dans CodeBlocks, notez qu'il a déjà créé une suite de phrases commençant par #ifndef. Ces instructions veulent dire plus ou moins: « Salut ordinateur. Lorsque que vous convertissez mon code en langage machine, vérifiez bien si vous n'avez pas déjà converti ce bout de code. Si ce n'est pas effectivement le cas, convertissez-le et souvenez vous de ce nom pour plus tard ». Tout ce qui se trouve entre #ifndef et #endif sera traité de cette mannière. C'est juste une façon d'éviter d'inclure 10x votre code si une dixaine de parties de votre programme font référence à ce code.

Si vous êtes sur xCode il faut le faire rentrer à la main. Voici les instructions qu'il faut ajouter:


#ifndef BALLE
#define BALLE

#endif

Tout ce que vous aller écrire, s'écrira dans l'espace blanc entre ces lignes.

Il y a une alternative, si #ifndef et #endif vous semble trop compliqué : vous pouvez tout simplement écrire #pragma once en haut de votre fichier. Si vous utilisez cette instruction, vous n'aurez plus besoin ni de #ifndef, ni de #define, ni de #endif. Il faut juste écrire, en haut du fichier :

#pragma once

Plus simple, non ? Quoi qu'il en soit, vous trouverez beaucoup de code en ligne qui utilise les deux méthodes d'écriture.

Include

Il faut ensuite rentrer dans notre code une connection à l'ensemble de la bibliothèque OpenFrameworks. Ici, tout marche par référencement d'un fichier à un autre fichier. Si on veut inclure un bout de code dans un autre bout de code sans faire copier-coller (ce qui serait ingérable, vu le nombre de fonctions et sous-fonctions dans OpenFrameworks), on doit dire à la machine via son nom de fichier. Ce sont les noms des fichiers qui permettent de relier tout le code ensemble: votre code + le code d'OpenFrameworks + le code de toutes les bibliothèques open-source dont dépend OpenFrameworks + le code de votre système d'expoitation. Tout ce code est relié par un mot magique : #include.

Créez donc une connection de votre classe en direction de celui d'OpenFrameworks en écrivant ceci:


#include "ofmain.h"

Ceci permettra à toute référence aux fonctions d'OpenFrameworks dans votre classe d'être compris par le « compilateur ». Notez que vous n'avec pas besoin de point-virgule car il ne s'agit pas d'action, mais d'inclusion d'un fichier dans un autre lors de la « compilation » de votre code. Si vous ne comprennez pas cette phrase, comprennez au moins ceci : c'est le compilateur qui convertit votre code en langage machine avant son execution, et il doit vérifier toutes les erreurs de frappe et de connection (#include). malheureusement il ne peut pas corriger toutes les erreurs logique, même s'il peut en signaler pas mal. Par contre, il verra tout de suite si vous faites référence à une partie du programme dans un autre fichier qui n'a pas encore été relié par #include. Ce mot include est donc très important et très puissant. Il relie tout le code ensemble.

Ok, nous allons maintenant déclarer notre classe. C'est la déclaration de la classe qui déterminera les variables et méthodes à notre disposition. On va également déclarer le « constructeur » de la classe qui plus loin (dans le Balle.cpp) définira ce qui se passera lors de chaque création d'objet à partir de notre class.

Voici le code tout ensemble qui définit notre class, entouré maintenant de notre #ifndef et le #include :


#ifndef BALLE
#define BALLE

#include "ofmain.h"

class Balle {

public:

    Balle();

    void update();
    void draw();

    float x,y;

};

#endif

Notez que contrairement à Processing/Java, nous écrivons un point-virgule à la fin de la déclaration de la classe.

Nous avons maintenant défini notre classe. Il y a une classe Balle, avec un constructeur Balle(), deux fonctions void update() et void draw() et deux variables x et y. Toutes ses propriétés sont public:, c'est-à-dire accessible depuis l'extérieur de cette objet (par exemple depuis le testApp qui va bientôt les appeler.

Définissons maintenant ce que doit faire une balle de type Balle. Ouvrez donc le fichier Balle.cpp et ajoutez ces lignes:


#include "Balle.h"

Balle::Balle() {
    x = ofRandom(0, ofGetWidth());
    y = ofRandom(0, ofGetHeight());
}

void Balle::update() {

}

void Balle::draw() {
    ofSetColor(255,0,0);
    offill();
    ofEllipse(x,y,25,25);
}

Que veut dire tout ça? Eh bien, c'est pas si différent que les classes dans Processing. Ici, nous expliquons ce qui se passera lors de la création d'un objet Balle::Balle() et chaque fois que nous voulons dessiner un objet void Balle::draw(). C'est indentique à Processing sauf le Balle:: qui indique le nom de la classe que nous sommes en train de définir. Le signe :: est nécessaire à cause de la séparation des fichiers .h et .cpp. Il faut savoir si update() et draw définissent ces méthodes de la classe Balle ou d'une toute autre classe, par exemple la classe OuiOui ou la classe Pimprenelle. Comme les fichiers sont (presque) toujours séparés dans C++, le :: est ici nécessaire.

Voilà, notre classe maintenant est déclaré et définie. Il suffira juste de s'en servir dans notre programme testApp.

Ajouter la classe au programme testApp

Ouvrez d'abord le fichier testApp.h. Vous allez d'abord lui dire d'inclure le fichier de votre code dans le sien, juste en dessus des autres inclusions :


#include "ofmain.h"
#include "ofAddons.h"
#include "Balle.h"

Vous allez ensuite rajoutez un objet à partir de votre classe Balle, juste après la liste de méthodes :


Balle leNomDemonBallon;

Comme on voit dans l'illustration, on déclare notre classe Balle à côté de toutes les autres classes d'OpenFrameworks qui vont être appellées depuis notre programme (testApp est notre application ou programme). Ensuite, on déclare qu'il y aura un objet nommé leNomDemonBallon (drôle de nom, d'accord, mais c'était pour que vous sentiez bien que vous pouvez lui donner n'importe quel nom). Juste en déclarant Balle leNomDemonBallon vous venez de créer un objet de type Balle dans testApp.

Par contre, pour voir l'objet se dessiner, il faut lui dire de le faire. Voici le code qu'il faut modifier au fichier testApp.cpp :


void testApp::update(){
    leNomDemonBallon.update();
}

void testApp::draw(){
    leNomDemonBallon.draw();
}

Voilà! Ça devrait marcher. Appuyer sur Build and Run (F9 dans CodeBlocks, Cmd-R dans xCode) et appréciez votre magnifique balle!

Créer plusieurs objets

Il y a deux façons de faire plusieurs objets. La façon simple, mais pas très souple, et la façon pas très compliqué non plus, et qui nous offre beaucoup de souplesse.

Commençons par le plus simple: transformons notre objet d'une seule balle en plusieurs balles. On va donc changer leNomDemonBallon à tousMesBallons dans le header de testApp.h, qu'on va également transformer en une liste en ajoutant le signe [100], ce qui créera un talbeau d'une centaine d'objets de type Balle:


Balle tousmesBallons[100];

Ensuite, il faut changer les méthodes update() et draw() pour dessiner non pas une seule balle, mais un tableau rempli d'une centaine de balles:


void testApp::update(){
    for(int i=0; i<100; i++) {
        tousmesBallons[i].update();
    }
}

void testApp::draw(){
    for(int i=0; i<100; i++) {
        tousmesBallons[i].draw();
    }
}

Comme nous avons maintenant 100 objets, il faut alors changer le update() et le draw() pour refleter ce changement. Au lieu de dessiner un seul objet, nous devons dessiner une centaine via le boucle for().

Remplacer le tableau par un tableau dynamique

Un « Vector » se comporte comme un tableau, avec quelques avantages en plus : notamment la possibilité de commencer avec une liste vide, puis de le remplir progressivement. Commencez par re-écrire la déclaration de votre liste de balles dans testApp.h :


vector<Balle> tousmesBallons;

maintenant que notre tableau est dynamique, et que celui-ci est d'une taille variable, nous avons besoin de savoir combien d'élements il y a dans notre Vector pour pouvoir le dessiner. Heureusement, les méthodes des Vector nous donne cette taille via la fonction size(). Remplacez alors les lignes de votre testApp.cpp en donnant un taille variable à votre boucle for():


void testApp::update(){
    for(int i=0; i<tousmesBallons.size(); i++) {
        tousmesBallons[i].update();
    }
}

void testApp::draw(){
    for(int i=0; i<tousmesBallons.size(); i++) {
        tousmesBallons[i].draw();
    }
}

Ajoutez ensuite une méthode dans mousePressed() pour permettre d'ajouter des élements à notre tableau dynamique:


void testApp::mousePressed(int x, int y, int button){
    tousmesBallons.push_back( Balle() );
}

La fonction push_back() ajoute un élement à notre Vector. C'est cette ligne qui permet au Vector de s'agrandir; ce qui n'est pas possible dans une liste classique comme plus haut (où on était limité à 100 élements).

Happy Code Farm

Si vous voulez savoir plus sur les vectors, les étudiants de l'Atelier hypermédia propose quelques exemples dans le Happy Code Farm à partir des exemples que nous venons d'explorer. Vous y trouverez un exemple plutôt simple Click O'Rama, suivi d'une version plus complexe Click O'Rama Record du même.

Il y a aussi deux projets qui montrent à la fois la puissance des vectors mais aussi la difficulté du syntaxe quand on veut commencer à faire des choses plutôt complexes Listes dynamiques.