Tutoriel BasicCalculator
Par Contributeur - Publié le
Bon alors ici, on a un projet facile puisque c'est une application relativement "haut-niveau" (attention, ça ne veut pas dire que c'est compliqué, ça veut dire que c'est assez loin des couches basses puisque l'application n'y touche pas?)
On commence donc par lancer Project Builder et on crée un projet "Application Cocoa" que l'on peut appeler "Basic Calculator" (ou Paté en Croute si ça vous chante?)
On commence donc par ouvrir le "MainMenu.nib" avec Interface Builder pour créer notre interface?
Alors, on va plagier la calculette d'Apple (je sais, c'est très mal mais bon?) donc vous ouvrez l'application "Calculette" et vous essayez de faire une interface à peu près similaire. Je vais pas m'attarder là parceque c'est pas trop compliqué et je pense que vous saurez vous débrouiller?;) En fait, il suffit de créer un bouton à la bonne taille, de le dupliquer plein de fois et de lui mettre comme titre ce qu'il faut? (on aurait aussi pu le faire avec une NSMatrix mais ça complique un peu?)
Seulement on ne va pas mettre de bouton virgule pour l'instant.
On ajoute à ça un champ (NSTextField) qu'on prend soin de rendre non-editable (décocher la case correspondante dans la palette d'info). Ensuite, on peut régler la fenêtre à la bonne taille et décocher sa case resize pour éviter que l'utilisateur fasse n'importe quoi (et oui, c'est ça être programmeur, c'est prévoir les c*******[ que peut faire l'utilisateur, pas toujours très habile de ses mains? et je ne vous parle pas des pauvres programmeurs PC?;) )
Pendant qu'on est parti, on va aussi empêcher l'utilisateur de fermer la fenêtre sans quitter l'application en décochant la case appropriée?
Enfin, pour faire joli, on crée un NSImageView, on l'adapte à la taille et on place dedans le magnifique logo M4E? Pour cela, il suffit d'ajouter l'image par glisser-déposer dans la zone "Images" du .nib et de remplir le champ Icon du NSImageView par le nom de l'image?
Pour tout cela, si vous avez des difficultés, vous pouvez utiliser le MainMenu.nib que vous trouverez dans les sources du logiciel. Voila un aperçu de ce que devrait vous donner l'interface:
Ici, on a de la chance, les opérations à effectuer sont plutôt simples?
Fidèles au principe de MCV (enfin presque?), on va utiliser 3 classes: un controlleur d'interface, un "model", en l'occurence un buffer, et une classe de gestion du tout, qu'on pourra éventuellement appeler "Calculator". Seules les classes qui nécessiteront des connections par "outlets" doivent être crées sous IB. Ici, on en a donc 2: Calculator et le controlleur d'interface, que j'appellerai IController? Pour les créer, on sélectionne donc l'onglet "Classes" du MainMenu.nib, sous IB evidemment, on sélectionne NSObject au premier niveau et on en crée une sous-classe (Classes>SubClass). Ensuite les propriétés de la classe sont accessibles dans la palette d'info. En fait, il y a juste 2 types de propriétés: les outlets et les actions. Les outlets sont des références à des éléments d'interface et les actions sont des méthodes qu'on définira dans le code et qui seront déclenchées directement par les éléments d'interface. Pour Calculator, c'est simple, on ne crée ni outlet ni action, logique puisque ce n'est pas lui le controlleur de l'interface. Par contre, pour IController, on va créer deux outlets et 7 actions. Vous pouvez les appeller n'importe comment mais moi j'ai choisi "calculator" et "displayField" pour les outlets, dont il ne faut pas oublier de préciser la classe là où IB avait mis "id" par défaut (choisir respectivement "Calculator" et "NSTextField" dans les pop-ups). Ensuite viennent les actions: "add", "substract", "div", "mult", "reset", "Equals", "Numeric". j'espère que les noms que j'ai donné sont assez explicites?
Maintenant, il nous faut encore écrire le corps des fonctions et également définir la classe "Buffer". Tout cela va se faire dans PB. Mais avant pour rendre les fichiers accessibles, on sélectionne chacune de nos deux classes et on choisit "Create files for?" dans le menu "Classes".
Validez simplement le dialogue qui s'ensuit et vous devriez voir apparaitre les fichiers correspondants dans PB.
Pour IController, c'est simple, on a qu'a remplir les squelettes des fonctions qui sont déjà créés. Seulement on ne va pas le faire maintenant parcequ'il vaut mieux commencer par les classes qui sont totalement indépendantes avant de s'attaquer aux liens view-controller.
Pour créer la classe Buffer, c'est très simple, on choisit "New File" dans le menu File de PB.
Notre programme va donc se baser sur deux buffers qui contiennent chacun un nombre. Le premier contiendra la saisie de l'utilisateur jusqu'à ce qu'il demande une opération (addition, soustraction?). A ce moment là, on note quelquepart l'opération que le buffer attend et on remplit le second. Si l'utilisateur clique alors sur égal ou sur une autre opération, on effectue l'opération initialement demandée et le second buffer peut donc être réinitialisé et réutilisé pour la nouvelle opération.
Commençons donc par écrire la classe Buffer?
C'est donc une classe assez simple puisqu'elle ne contient qu'un nombre. Seulement on a dit qu'il fallait noter quelquepart quelle opération effectuer avec le second buffer (l'ajouter au premier, le soustraire? ?). On va donc trouver un moyen pour que chaque buffer aie un état, pour savoir ce qu'il attend. Pour cela, on va créer un nouveau type de données qui pourra prendre des valeurs excplicites: WaitingForNumberA, WaitingForNumberS, WaitingForNumberM, WaitingForNumberD, WaitingNewNumber, WaitingForNumberToAppend. Les quatres premiers servent à ce que le buffer sache l'opération qu'il attend et les deux autres servent au remplissage du buffer. WaitingNewNumber est l'état initial du buffer. Pour créer ce type de donnée, on va utiliser le mot-clé typedef. Il s'utilise ainsi:
On pourra appeler le nouveau type BufferState (ou autre chose si ça vous chante?). Pour sa définition, on va utiliser une "enum". Le mot-clé enum sert à définir une suite de constantes. Il s'utilise ainsi:
le compilateur attribue automatiquement une valeur entière à chaque constante: il commence par 0 et ajoute 1 àchaque fois. Ainsi, ici CONST1 vaut 0, CONST2 1? On peut aussi lui demander explicitement d'attribuer une valeur différente:
enum CONSTS {CONST1 = 15, CONST2 = 36, CONST3};
Ici, CONST3 vaut 37.
typedef et enum sont souvent utilisés ensemble pour créer un type de donnée qui peut prendre pour valeur une des constantes définies. Souvent, la valeur entière qui leur est attribuée ne sert pas au programmeur, c'est d'ailleurs notre cas. La définition de notre type s'écrit donc:
Cette définition se place dans le fichier Buffer.h, juste avant le "@interface".
Maintenant, on peut définir notre classe proprement dite. On commence par les données membres: un "float val;" (qui contient la valeur du buffer) et un "BufferState state;". Pour toutes les déclarations de variables, on donne toujours le type de la variable, puis son nom. Par contre, dans un "@interface", on en peut pas donner de valeur par défaut. La seule chose qu'on fait ici, c'est juste dire de quels types de données la classe sera composée. Pour pallier à cela, on peut soit créer une méthode d'instantiation qui permet de spécifier les valeurs initiales, soit faire un override de la méthode "init" standard. J'en profite ici pour insister sur la différence entre une classe et un objet: un objet est une instance de classe. La classe en elle même ne peut rien faire de concret, elle est juste le modèle sur lequel pourront être fabriqués des objets qui auront exactement les mêmes caractéristiques. La seule différence entre deux objets d'une même classe, c'est la valeur des données membres, pas leur nature.
Bref, toutes les classes dérivées de NSObject ont une méthode init (c'est l'équivalent du constructeur en C++). On va la surcharger pour définir une initialisation spécifique à la classe: pour l'instant on ne fait que l'annoncer et toujours dans le "@interface", on ajoute une ligne "- (id)init;". C'est ce qu'on appelle un prototype de fonction, comme je l'ai expliqué dans mon dernier article: on annonce qu'il y aura dans le code une fonction init qui renverra un "id". Le moins devant veut dire que c'est une fonction d'objet, par opposition aux fonctions de classes, plus rares, précédées d'un signe + (la plupart du temps, elles servent seulement à instantier la classe)
Maintenant, il ne nous reste plus qu'a écrire le prototype des accesseurs et des mutateurs: notre classe a deux données membres et il faut qu'on puisse obtenir leurs valeurs et les modifier. Bien sur, il existe un moyen pour y accéder directement mais ce serait contraire à un des principes de base de la programmation orientée objet: l'encapsulation (pour information, les trois grands principes de la POO sont l'héritage, le polymorphisme et l'encapsulation). Ce principe dit que pour chaque classe, l'accès aux données membres doit être réservé à la classe elle même: si une autre classe a besoin de les manipuler, elle fait appel à des fonctions prévues à cet effet, appelées accesseurs pour celles qui renvoient les valeurs des donnes membres et mutateurs pour celles qui les modifient. Bien sur le code de ces fonctions est extrêmement simple. Et pour en revenir à leurs prototypes, ce sont le suivants:
Comme on pouvait s'y attendre, les mutateurs ne renvoient rien et prennent un argument et les accesseurs renvoient le type de la donnée membre correspondante. C'est tout pour l'interface du Buffer, on peut donc passer à son implementation.
On peut commencer par recopier les prototypes des fonctions définies dans l' "@interface". Un copier-coller fait tout à fait l'affaire. Ensuite, pour chaque fonction, on efface le point-virgule qui termine la ligne, on fait un retour-chariot, accolade ouvrante, retour-chariot, accolade fermante. Il reste encore à remplir ces fonctions. Commencons par init. C'est facile, il faut tout simplement définir val et state. Seulement comme on fait un override de la méthode de la superclasse (la classe parente quoi, c-a-d la classe dont celle-ci est dérivée), il ne faut pas oublier de l'appeler. Cela nous donne pour init:
Maintenant, les mutateurs:
Et les accesseurs:
Voila, comme vous le remarquez, l'implementation de cette classe est très simple, puisqu'elle ne propose pas d'autres fonctions que de stocker ensemble deux types de données. Et en effet, on aurait pu faire autrement, c-a-d relier ces deux types de données dans une "struct", sans avoir besoin de créer de classe mais mon but ici était de vous présenter cet aspect de la POO et on peut toujours avoir besoin d'ajouter des fonctions plus tard.
Une fois tout cela fait, on peut s'attaquer à la classe Calculator. Et je vous préviens tout de suite, ça va être un peu plus compliqué?
Commencons par les données membres: c'est très simple, on a deux buffers, buffer1 et buffer2, ce qui nous donne:
Seulement, la classe Buffer n'est définie nul part, il faut préciser au compilateur ce que c'est et lui dire de compiler ses fichiers également. Ici, on va juste l'informer que c'est bien une classe en rajoutant avant le @interface la ligne:
Faire cela ne suffit pas: il faut aussi dire au compilateur quel fichier contient la définition de cette classe. Pour cela, on rajoute une ligne en tout début du fichier Calculator.m:
Cette commande import est une directive à l'intention du préprocesseur. Remarquez d'ailleurs que chaque fichier .m importe son propre header (.h).
Dans le noms des buffers, l'astérisque représente un pointeur, c-a-d une adresse de zone de mémoire. Dans la pratique, on se sert de pointeur dès lors que l'on utilise pas un des types de base (float, int, char?) ou un type défini par typedef?
Seulement, ces pointeurs ne contiennent pour l'instant rien. Il faudra donc leur donner l'adresse de buffers lors du lancement.
Comme tout à l'heure, on va donc "overrider" la méthode init. On aura aussi besoin d'une fonction reinit, qui sera utilisée lorsque l'utilisateur cliquera sur le bouton "C". Vous remarquerez qu'on commence déjà à se rapprocher un peu de l'interface? Ces méthodes s'écrivent:
Ensuite, on aura besoin de quatres fonctions pour définir l'attente des différentes opérations pour le buffer1. En effet, le buffer2 n'aura jamais besoin d'attendre une opération quelconque car à ce mopment là, on effectuera l'opération précédente en attente et le buffer2 pourra être réutilisé. Relisez lentement si vous avez pas tout compris? ;)
Ces quatres méthodes donnent donc:
Je pense que vous comprendrez à quoi correspond chacune grâce à son nom.
On va aussi avoir besoin de méthodes pour accéder aux résultats des différentes opérations. On peut en faire une qui donne toujours la valeur du buffer1 et une autre qui donne la valeur du buffer en cours de modification:
On a encore besoin d'une méthode pour effectuer l'opération en attente, du contenu de buffer2 vers celui de buffer1:
Et enfin, une méthode pour gérer l'envoi de nombres par l'utilisateur (savoir si c'est toujours le même nombre que l'utilisateur est en train d'entrer?) et une autre pour effectuer l'opération en attente et être prêt à une nouvelle opération (méthode appellée lorsque l'utilisateur clique sur égal):
Voila, l'interface de Calculator est terminée, il ne reste plus qu'à écrire son code? et c'est là qu'est le plus gros du travail?
Commençons par l'init, c'est assez simple, il suffit d'instantier deux buffers:
La technique alloc-init est celle qu'on utilise la plupart du temps pour associer un pointeur à son objet. La méthode alloc est une méthode de classe puisque le receveur du message est bien Buffer, la classe, et non pas le nom d'un objet buffer, variable.
On peut aussi écrire facilement
Tout simplement?
Toujours dans les méthodes faciles, il y a encore reinit:
Maintenant, un tout petit peu plus compliqué, getCurrentEditingBuffer:
Alors ici, il faut que j'explique: on utilise un opérateur ternaire. L'opérateur ternaire evalue une condition A, effectue B si la condition est vraie et C si elle est fausse, et le tout s'écrit:
On pourrait aussi l'écrire de cette façon:
Comme vous le voyez, l'emploi d'un opérateur ternaire permet de gagner en concision. Ici, les actions a effectuer sont simples à comprendre, si le test est vrai, on renvoie la valeur de buffer1, sinon, celle de buffer2. On en déduit que ce test est en fait celui qui nous dit si buffer1 est le buffer en cours d'utilisation. Si c'est le cas, son statut ne peut prendre que les valeurs suivantes: WaitingNewNumber ou WaitingForNumberToAppend. C'est ce que détermine le test: la première paire de parenthèses encadre le test qui renvoie vrai si le statut est WaitingForNumberToAppend, non sinon. Le deuxième test fonctionne sur le même principe et le tout est relié grâce à un "ou" logique, "||".
Passons à nos quatres fonctions d'opérations. Si l'utilisateur clique sur un des boutons d'opérations,il faut à ce moment là que l'éventuelle opération en attente s'execute, que le state du buffer1 devienne celui qui convient à l'opération nouvellement demandée et que le buffer2 soit prêt à recevoir le deuxième acteur de cette opération. Cela nous donne:
Remarquez que dans le cas de la multiplication et de la division, on prend soin de vérifier que le buffer2 ne contient pas 0, pour éviter les déconvenues lors de la première opération sur le nombre (et même les autres pour la division).
Passons à la dernière méthode de ce genre, celle d'opération finale, soit le "égal", soit encore endOp:
C'est exactement comme pour les précédentes sauf qu'après l'execution de cette méthode, le buffer1 contient encore le résultat mais sera rempli avec un nouveau nombre à la prochaine saisie de l'utilisateur.
Attaquons nous maintenant à execB2toB1:
On utilise ici une structure qui permet d'examiner différentes possibilités de valeurs pour une variable, le switch. Je crois qu'on a déjà fait un article sur son utilisation donc je vais pas m'appesantir dessus. On examine juste les 4 possibilités d'états qui peuvent nous intéresser pour cette méthode (les autres cas servent à un autre moment) et on effectue l'opération appropriée.
Enfin, il ne reste plus qu'à écrire la méthode de gestion de l'entrée utilisateur de chiffres, handleNumber:
Là encore, on utilise un switch, sauf qu'on ne s'intéresse qu'aux possibilités qu'on a pas traitées précédemment. Si il se trouve que le buffer1 n'est pas celui en cours d'édition, on fait exactement la même chose sur le buffer2. Pour ajouter un chiffre à la suite d'un nombre, on ruse un peu: on multiplie ce nombre par 10 et on ajoute le chiffre? (ouais, bon, pas la peine d'être très rusé?;)
Et ben, vous voyez qu'on en a vu le bout? c'était pas si difficile que ça? mais bon maintenant ça va se compliquer quand même? non, je plaisante, le plus dur est derrière nous?
En effet, il ne nous reste plus qu'a paramétrer le controller de l'interface. Pour l'interface, il n'y a rien a faire: IB a déjà tout fait pour nous. On a plus qu'a remplir les squelettes de méthodes qu'il nous a gentiment placé dans IController.m.
Maintenant on arrive vraiment au niveau de l'interface puisque toutes ces méthodes seront déclenchées par un clic de l'utilisateur. Lorsq'il cliquera sur l'un des quatre boutons d'opération, c'est facile, on fait appel à la méthode correspondante que l'on a définie dans Calculator et on affiche le resultat, ce qui nous donne:
setFloatValue est une méthode de la classe NSTextField, et vous pouvez donc avoir plus de renseignements sur cette classe dans le Framework AppKit, parcequ'on se sert presque toujours des NSTextField et c'est bien de les connaitre un peu.
Le comportement du bouton égal est très facile à programmer:
Il nous faut encore assigner une action au bouton C:
Et maintenant, c'est presque fini, il n'y a plus qu'a définir Numeric:
Vous avez sans doute remarqué que nous n'avons pas touché au fichier main.m et c'est normal, la plupart du temps pour des applications Cocoa, on le laisse tel quel, il sert juste à lancer l'application. C'est en fait la seule partie un peu plus procédurale du programme, tout le reste est très Orienté Objet.
Maintenant qu'on a créé toutes les classes qui vont nous servir, il faut encore les instantier. Retour à IB, dans le MainMenu.nib. Selectionnez tour à tour Calculator et IController et instantiez les. Les objets ainsi créés apparaissent logiquement dans l'onglet Instances. Il n'y a plus qu'a créer les connections: pour faire celles des deux outlets de IController, faites deux control-glisser-déposé de IController vers respectivement Calculator (l'objet) et le champ NSTextField. Ensuite pour assigner les actions, on fait encore des controls-clics en partant des boutons vers IControler. A chaque fois, choississez dans "target" l'action appropriée:
Tous les boutons de nombres déclenchent la même action; Numeric.
Sauvegardez votre MainMenu.nib.
Ca y est, c'est fini ! vous pouvez lancer la compilation dans PB et tester.
Ca marche (évidemment?) mais on peut encore améliorer quelques points de détails?
On va maintenant s'occuper des petits détails qui font une application finie.
Commençons par l'icone. C'est très simple, on prend la zolie icone que nous a préparé tonton Grouiky, on l'appelle NSApplicationIcon et on l'ajoute au projet par simple glisser-déposer. Tout le reste est automatique.
On peut aussi s'occuper des menus: il faut en effacer quelques-uns. Reportez vous au MainMenu.nib téléchargé pour savoir lequels garder.
Ensuite, on peut encore éditer le InfoPlist.strings, toujours dans PB.
Le mien me donne:
Ces chaînes seront affichées dans la fenêtre de lecture des infos du Finder ou dans la AboutBox de l'application.
Et non, je n'ai pas honte de signer un torchon pareil ! ;)
Mais si vous voulez mettre votre nom à la place?
Bon, j'espere que grâce à ce tutoriel, vous aurez compris les techniques de base de programmation Cocoa. Je vous conseille aussi le tutoriel "CurencyConverter", que vous trouverez avec la documentation installée par les devTools. Bien sûr, ici, on a fait une application très simple mais on a la base pour l'améliorer si on veut. Vous pourez par exemple ajouter une mémoire, d'autres opérations? (ou aussi localiser le logiciel)
Si l'envie vous en prend, envoyez-moi un mail, je pourrais écrire un article pour expliquer les améliorations
On commence donc par lancer Project Builder et on crée un projet "Application Cocoa" que l'on peut appeler "Basic Calculator" (ou Paté en Croute si ça vous chante?)
Avant tout je vous conseille quand même de télécharger les sources [mac="video"]ici[/mac].
Et surtout ne faites pas de copier-coller du code ! J'ai été obligé de mettre des espaces là où il n'en faut pas de temps en temps pour contourner les tags?
L'interface
On commence donc par ouvrir le "MainMenu.nib" avec Interface Builder pour créer notre interface?
Alors, on va plagier la calculette d'Apple (je sais, c'est très mal mais bon?) donc vous ouvrez l'application "Calculette" et vous essayez de faire une interface à peu près similaire. Je vais pas m'attarder là parceque c'est pas trop compliqué et je pense que vous saurez vous débrouiller?;) En fait, il suffit de créer un bouton à la bonne taille, de le dupliquer plein de fois et de lui mettre comme titre ce qu'il faut? (on aurait aussi pu le faire avec une NSMatrix mais ça complique un peu?)
Seulement on ne va pas mettre de bouton virgule pour l'instant.
On ajoute à ça un champ (NSTextField) qu'on prend soin de rendre non-editable (décocher la case correspondante dans la palette d'info). Ensuite, on peut régler la fenêtre à la bonne taille et décocher sa case resize pour éviter que l'utilisateur fasse n'importe quoi (et oui, c'est ça être programmeur, c'est prévoir les c*******[ que peut faire l'utilisateur, pas toujours très habile de ses mains? et je ne vous parle pas des pauvres programmeurs PC?;) )
Pendant qu'on est parti, on va aussi empêcher l'utilisateur de fermer la fenêtre sans quitter l'application en décochant la case appropriée?
Enfin, pour faire joli, on crée un NSImageView, on l'adapte à la taille et on place dedans le magnifique logo M4E? Pour cela, il suffit d'ajouter l'image par glisser-déposer dans la zone "Images" du .nib et de remplir le champ Icon du NSImageView par le nom de l'image?
Pour tout cela, si vous avez des difficultés, vous pouvez utiliser le MainMenu.nib que vous trouverez dans les sources du logiciel. Voila un aperçu de ce que devrait vous donner l'interface:
L'implementation (le code, quoi?)
Ici, on a de la chance, les opérations à effectuer sont plutôt simples?
Créer les fichiers de classe
Fidèles au principe de MCV (enfin presque?), on va utiliser 3 classes: un controlleur d'interface, un "model", en l'occurence un buffer, et une classe de gestion du tout, qu'on pourra éventuellement appeler "Calculator". Seules les classes qui nécessiteront des connections par "outlets" doivent être crées sous IB. Ici, on en a donc 2: Calculator et le controlleur d'interface, que j'appellerai IController? Pour les créer, on sélectionne donc l'onglet "Classes" du MainMenu.nib, sous IB evidemment, on sélectionne NSObject au premier niveau et on en crée une sous-classe (Classes>SubClass). Ensuite les propriétés de la classe sont accessibles dans la palette d'info. En fait, il y a juste 2 types de propriétés: les outlets et les actions. Les outlets sont des références à des éléments d'interface et les actions sont des méthodes qu'on définira dans le code et qui seront déclenchées directement par les éléments d'interface. Pour Calculator, c'est simple, on ne crée ni outlet ni action, logique puisque ce n'est pas lui le controlleur de l'interface. Par contre, pour IController, on va créer deux outlets et 7 actions. Vous pouvez les appeller n'importe comment mais moi j'ai choisi "calculator" et "displayField" pour les outlets, dont il ne faut pas oublier de préciser la classe là où IB avait mis "id" par défaut (choisir respectivement "Calculator" et "NSTextField" dans les pop-ups). Ensuite viennent les actions: "add", "substract", "div", "mult", "reset", "Equals", "Numeric". j'espère que les noms que j'ai donné sont assez explicites?
Maintenant, il nous faut encore écrire le corps des fonctions et également définir la classe "Buffer". Tout cela va se faire dans PB. Mais avant pour rendre les fichiers accessibles, on sélectionne chacune de nos deux classes et on choisit "Create files for?" dans le menu "Classes".
Validez simplement le dialogue qui s'ensuit et vous devriez voir apparaitre les fichiers correspondants dans PB.
Pour IController, c'est simple, on a qu'a remplir les squelettes des fonctions qui sont déjà créés. Seulement on ne va pas le faire maintenant parcequ'il vaut mieux commencer par les classes qui sont totalement indépendantes avant de s'attaquer aux liens view-controller.
Pour créer la classe Buffer, c'est très simple, on choisit "New File" dans le menu File de PB.
Le fonctionnement théorique
Notre programme va donc se baser sur deux buffers qui contiennent chacun un nombre. Le premier contiendra la saisie de l'utilisateur jusqu'à ce qu'il demande une opération (addition, soustraction?). A ce moment là, on note quelquepart l'opération que le buffer attend et on remplit le second. Si l'utilisateur clique alors sur égal ou sur une autre opération, on effectue l'opération initialement demandée et le second buffer peut donc être réinitialisé et réutilisé pour la nouvelle opération.
Commençons donc par écrire la classe Buffer?
La classe "Buffer"
C'est donc une classe assez simple puisqu'elle ne contient qu'un nombre. Seulement on a dit qu'il fallait noter quelquepart quelle opération effectuer avec le second buffer (l'ajouter au premier, le soustraire? ?). On va donc trouver un moyen pour que chaque buffer aie un état, pour savoir ce qu'il attend. Pour cela, on va créer un nouveau type de données qui pourra prendre des valeurs excplicites: WaitingForNumberA, WaitingForNumberS, WaitingForNumberM, WaitingForNumberD, WaitingNewNumber, WaitingForNumberToAppend. Les quatres premiers servent à ce que le buffer sache l'opération qu'il attend et les deux autres servent au remplissage du buffer. WaitingNewNumber est l'état initial du buffer. Pour créer ce type de donnée, on va utiliser le mot-clé typedef. Il s'utilise ainsi:
typedef definitiondutype nomdunouveautype;
On pourra appeler le nouveau type BufferState (ou autre chose si ça vous chante?). Pour sa définition, on va utiliser une "enum". Le mot-clé enum sert à définir une suite de constantes. Il s'utilise ainsi:
enum CONSTS {CONST1, CONST2, ?};
le compilateur attribue automatiquement une valeur entière à chaque constante: il commence par 0 et ajoute 1 àchaque fois. Ainsi, ici CONST1 vaut 0, CONST2 1? On peut aussi lui demander explicitement d'attribuer une valeur différente:
enum CONSTS {CONST1 = 15, CONST2 = 36, CONST3};
Ici, CONST3 vaut 37.
typedef et enum sont souvent utilisés ensemble pour créer un type de donnée qui peut prendre pour valeur une des constantes définies. Souvent, la valeur entière qui leur est attribuée ne sert pas au programmeur, c'est d'ailleurs notre cas. La définition de notre type s'écrit donc:
typedef enum {WaitingForNumberA, WaitingForNumberS, WaitingForNumberM, WaitingForNumberD, WaitingNewNumber, WaitingForNumberToAppend, WaitingForDecimalToAppend} BufferState;
Cette définition se place dans le fichier Buffer.h, juste avant le "@interface".
Maintenant, on peut définir notre classe proprement dite. On commence par les données membres: un "float val;" (qui contient la valeur du buffer) et un "BufferState state;". Pour toutes les déclarations de variables, on donne toujours le type de la variable, puis son nom. Par contre, dans un "@interface", on en peut pas donner de valeur par défaut. La seule chose qu'on fait ici, c'est juste dire de quels types de données la classe sera composée. Pour pallier à cela, on peut soit créer une méthode d'instantiation qui permet de spécifier les valeurs initiales, soit faire un override de la méthode "init" standard. J'en profite ici pour insister sur la différence entre une classe et un objet: un objet est une instance de classe. La classe en elle même ne peut rien faire de concret, elle est juste le modèle sur lequel pourront être fabriqués des objets qui auront exactement les mêmes caractéristiques. La seule différence entre deux objets d'une même classe, c'est la valeur des données membres, pas leur nature.
Bref, toutes les classes dérivées de NSObject ont une méthode init (c'est l'équivalent du constructeur en C++). On va la surcharger pour définir une initialisation spécifique à la classe: pour l'instant on ne fait que l'annoncer et toujours dans le "@interface", on ajoute une ligne "- (id)init;". C'est ce qu'on appelle un prototype de fonction, comme je l'ai expliqué dans mon dernier article: on annonce qu'il y aura dans le code une fonction init qui renverra un "id". Le moins devant veut dire que c'est une fonction d'objet, par opposition aux fonctions de classes, plus rares, précédées d'un signe + (la plupart du temps, elles servent seulement à instantier la classe)
Maintenant, il ne nous reste plus qu'a écrire le prototype des accesseurs et des mutateurs: notre classe a deux données membres et il faut qu'on puisse obtenir leurs valeurs et les modifier. Bien sur, il existe un moyen pour y accéder directement mais ce serait contraire à un des principes de base de la programmation orientée objet: l'encapsulation (pour information, les trois grands principes de la POO sont l'héritage, le polymorphisme et l'encapsulation). Ce principe dit que pour chaque classe, l'accès aux données membres doit être réservé à la classe elle même: si une autre classe a besoin de les manipuler, elle fait appel à des fonctions prévues à cet effet, appelées accesseurs pour celles qui renvoient les valeurs des donnes membres et mutateurs pour celles qui les modifient. Bien sur le code de ces fonctions est extrêmement simple. Et pour en revenir à leurs prototypes, ce sont le suivants:
- (void)setState: (BufferState)newstate;
- (void)setValue: (float)newval;
- (float)getValue;
- (BufferState)getState;
Comme on pouvait s'y attendre, les mutateurs ne renvoient rien et prennent un argument et les accesseurs renvoient le type de la donnée membre correspondante. C'est tout pour l'interface du Buffer, on peut donc passer à son implementation.
On peut commencer par recopier les prototypes des fonctions définies dans l' "@interface". Un copier-coller fait tout à fait l'affaire. Ensuite, pour chaque fonction, on efface le point-virgule qui termine la ligne, on fait un retour-chariot, accolade ouvrante, retour-chariot, accolade fermante. Il reste encore à remplir ces fonctions. Commencons par init. C'est facile, il faut tout simplement définir val et state. Seulement comme on fait un override de la méthode de la superclasse (la classe parente quoi, c-a-d la classe dont celle-ci est dérivée), il ne faut pas oublier de l'appeler. Cela nous donne pour init:
- (id)init
{
self = [super init];
val = 0;
state = WaitingNewNumber;
return self;
}
Maintenant, les mutateurs:
- (void)setState: (BufferState)newstate
{
state = newstate;
}
- (void)setValue: (float)newval
{
val = newval;
}
Et les accesseurs:
- (float)getValue
{
return val;
}
- (BufferState)getState
{
return state;
}
Voila, comme vous le remarquez, l'implementation de cette classe est très simple, puisqu'elle ne propose pas d'autres fonctions que de stocker ensemble deux types de données. Et en effet, on aurait pu faire autrement, c-a-d relier ces deux types de données dans une "struct", sans avoir besoin de créer de classe mais mon but ici était de vous présenter cet aspect de la POO et on peut toujours avoir besoin d'ajouter des fonctions plus tard.
Une fois tout cela fait, on peut s'attaquer à la classe Calculator. Et je vous préviens tout de suite, ça va être un peu plus compliqué?
La Classe Calculator
Commencons par les données membres: c'est très simple, on a deux buffers, buffer1 et buffer2, ce qui nous donne:
@interface Calculator : NSObject
{
Buffer ]buffer1;
Buffer [buffer2;
}
Seulement, la classe Buffer n'est définie nul part, il faut préciser au compilateur ce que c'est et lui dire de compiler ses fichiers également. Ici, on va juste l'informer que c'est bien une classe en rajoutant avant le @interface la ligne:
@class Buffer;
Faire cela ne suffit pas: il faut aussi dire au compilateur quel fichier contient la définition de cette classe. Pour cela, on rajoute une ligne en tout début du fichier Calculator.m:
#import "Buffer.h"
Cette commande import est une directive à l'intention du préprocesseur. Remarquez d'ailleurs que chaque fichier .m importe son propre header (.h).
Dans le noms des buffers, l'astérisque représente un pointeur, c-a-d une adresse de zone de mémoire. Dans la pratique, on se sert de pointeur dès lors que l'on utilise pas un des types de base (float, int, char?) ou un type défini par typedef?
Seulement, ces pointeurs ne contiennent pour l'instant rien. Il faudra donc leur donner l'adresse de buffers lors du lancement.
Comme tout à l'heure, on va donc "overrider" la méthode init. On aura aussi besoin d'une fonction reinit, qui sera utilisée lorsque l'utilisateur cliquera sur le bouton "C". Vous remarquerez qu'on commence déjà à se rapprocher un peu de l'interface? Ces méthodes s'écrivent:
- (id)init;
- (void)reinit;
Ensuite, on aura besoin de quatres fonctions pour définir l'attente des différentes opérations pour le buffer1. En effet, le buffer2 n'aura jamais besoin d'attendre une opération quelconque car à ce mopment là, on effectuera l'opération précédente en attente et le buffer2 pourra être réutilisé. Relisez lentement si vous avez pas tout compris? ;)
Ces quatres méthodes donnent donc:
- (void)waitForNTA;
- (void)waitForNTS;
- (void)waitForNTM;
- (void)waitForNTD;
Je pense que vous comprendrez à quoi correspond chacune grâce à son nom.
On va aussi avoir besoin de méthodes pour accéder aux résultats des différentes opérations. On peut en faire une qui donne toujours la valeur du buffer1 et une autre qui donne la valeur du buffer en cours de modification:
- (float)getValueOfCurrentEditingBuffer;
- (float)getResult;
On a encore besoin d'une méthode pour effectuer l'opération en attente, du contenu de buffer2 vers celui de buffer1:
- (void)execB2toB1;
Et enfin, une méthode pour gérer l'envoi de nombres par l'utilisateur (savoir si c'est toujours le même nombre que l'utilisateur est en train d'entrer?) et une autre pour effectuer l'opération en attente et être prêt à une nouvelle opération (méthode appellée lorsque l'utilisateur clique sur égal):
- (void)handleNumber: (float)num;
- (void)endOp;
Voila, l'interface de Calculator est terminée, il ne reste plus qu'à écrire son code? et c'est là qu'est le plus gros du travail?
Commençons par l'init, c'est assez simple, il suffit d'instantier deux buffers:
- (id)init
{
self = [super init];
buffer1 = [[Buffer alloc] init];
buffer2 = [[Buffer alloc] init];
return self;
}
La technique alloc-init est celle qu'on utilise la plupart du temps pour associer un pointeur à son objet. La méthode alloc est une méthode de classe puisque le receveur du message est bien Buffer, la classe, et non pas le nom d'un objet buffer, variable.
On peut aussi écrire facilement
getResult
, ce qui donne:- (float)getResult
{
return [buffer1 getValue];
}
Tout simplement?
Toujours dans les méthodes faciles, il y a encore reinit:
- (void)reinit
{
[buffer1 setValue:0];
[buffer1 setState:WaitingNewNumber];
[buffer2 setValue:0];
[buffer2 setState:WaitingNewNumber];
}
Maintenant, un tout petit peu plus compliqué, getCurrentEditingBuffer:
- (float)getValueOfCurrentEditingBuffer
{
return ((([buffer1 getState] == WaitingNewNumber) || ([buffer1 getState] == WaitingForNumberToAppend)) ? [buffer1 getValue] : [buffer2 getValue] ) ;
}
Alors ici, il faut que j'explique: on utilise un opérateur ternaire. L'opérateur ternaire evalue une condition A, effectue B si la condition est vraie et C si elle est fausse, et le tout s'écrit:
A ? B : C
On pourrait aussi l'écrire de cette façon:
if (A) {
B
} else {
C
}
Comme vous le voyez, l'emploi d'un opérateur ternaire permet de gagner en concision. Ici, les actions a effectuer sont simples à comprendre, si le test est vrai, on renvoie la valeur de buffer1, sinon, celle de buffer2. On en déduit que ce test est en fait celui qui nous dit si buffer1 est le buffer en cours d'utilisation. Si c'est le cas, son statut ne peut prendre que les valeurs suivantes: WaitingNewNumber ou WaitingForNumberToAppend. C'est ce que détermine le test: la première paire de parenthèses encadre le test qui renvoie vrai si le statut est WaitingForNumberToAppend, non sinon. Le deuxième test fonctionne sur le même principe et le tout est relié grâce à un "ou" logique, "||".
Passons à nos quatres fonctions d'opérations. Si l'utilisateur clique sur un des boutons d'opérations,il faut à ce moment là que l'éventuelle opération en attente s'execute, que le state du buffer1 devienne celui qui convient à l'opération nouvellement demandée et que le buffer2 soit prêt à recevoir le deuxième acteur de cette opération. Cela nous donne:
- (void)waitForNTA
{
[self execB2toB1];
[buffer1 setState:WaitingForNumberA];
[buffer2 setState:WaitingNewNumber];
}
- (void)waitForNTS
{
[self execB2toB1];
[buffer1 setState:WaitingForNumberS];
[buffer2 setState:WaitingNewNumber];
}
- (void)waitForNTM
{
if ([buffer2 getValue] != 0) {
[self execB2toB1];
}
[buffer1 setState:WaitingForNumberM];
[buffer2 setState:WaitingNewNumber];
}
- (void)waitForNTD
{
if ([buffer2 getValue] != 0) {
[self execB2toB1];
}
[buffer1 setState:WaitingForNumberD];
[buffer2 setState:WaitingNewNumber];
}
Remarquez que dans le cas de la multiplication et de la division, on prend soin de vérifier que le buffer2 ne contient pas 0, pour éviter les déconvenues lors de la première opération sur le nombre (et même les autres pour la division).
Passons à la dernière méthode de ce genre, celle d'opération finale, soit le "égal", soit encore endOp:
- (void)endOp
{
if ([buffer2 getValue] != 0) {
[self execB2toB1];
}
[buffer2 setState:WaitingNewNumber];
[buffer2 setValue:0];
[buffer1 setState:WaitingNewNumber];
}
C'est exactement comme pour les précédentes sauf qu'après l'execution de cette méthode, le buffer1 contient encore le résultat mais sera rempli avec un nouveau nombre à la prochaine saisie de l'utilisateur.
Attaquons nous maintenant à execB2toB1:
- (void)execB2toB1
{
switch ([buffer1 getState] ) {
case WaitingForNumberA : [buffer1 setValue: ( [buffer1 getValue] + [buffer2 getValue] ) ];
break;
case WaitingForNumberS : [buffer1 setValue: ( [buffer1 getValue] - [buffer2 getValue] ) ];
break;
case WaitingForNumberD : [buffer1 setValue: ( [buffer1 getValue] / [buffer2 getValue] ) ;
break;
case WaitingForNumberM : [buffer1 setValue: ( [buffer1 getValue] ] [buffer2 getValue] ) ];
break;
default : break;
}
}
On utilise ici une structure qui permet d'examiner différentes possibilités de valeurs pour une variable, le switch. Je crois qu'on a déjà fait un article sur son utilisation donc je vais pas m'appesantir dessus. On examine juste les 4 possibilités d'états qui peuvent nous intéresser pour cette méthode (les autres cas servent à un autre moment) et on effectue l'opération appropriée.
Enfin, il ne reste plus qu'à écrire la méthode de gestion de l'entrée utilisateur de chiffres, handleNumber:
- (void)handleNumber: (float)num
{
switch ( [buffer1 getState] ) {
case WaitingNewNumber : [buffer1 setValue:num];
[buffer1 setState:WaitingForNumberToAppend];
break;
case WaitingForNumberToAppend : [buffer1 setValue: ( [buffer1 getValue] [ 10 + num) ];
break;
default : switch ( [buffer2 getState] ) {
case WaitingNewNumber : [buffer2 setValue:num];
[buffer2 setState:WaitingForNumberToAppend];
break;
case WaitingForNumberToAppend : [buffer2 setValue: ( [buffer2 getValue] ] 10 + num ) ];
break;
default : break;
}
break;
}
}
Là encore, on utilise un switch, sauf qu'on ne s'intéresse qu'aux possibilités qu'on a pas traitées précédemment. Si il se trouve que le buffer1 n'est pas celui en cours d'édition, on fait exactement la même chose sur le buffer2. Pour ajouter un chiffre à la suite d'un nombre, on ruse un peu: on multiplie ce nombre par 10 et on ajoute le chiffre? (ouais, bon, pas la peine d'être très rusé?;)
Et ben, vous voyez qu'on en a vu le bout? c'était pas si difficile que ça? mais bon maintenant ça va se compliquer quand même? non, je plaisante, le plus dur est derrière nous?
En effet, il ne nous reste plus qu'a paramétrer le controller de l'interface. Pour l'interface, il n'y a rien a faire: IB a déjà tout fait pour nous. On a plus qu'a remplir les squelettes de méthodes qu'il nous a gentiment placé dans IController.m.
La Classe IController
Maintenant on arrive vraiment au niveau de l'interface puisque toutes ces méthodes seront déclenchées par un clic de l'utilisateur. Lorsq'il cliquera sur l'un des quatre boutons d'opération, c'est facile, on fait appel à la méthode correspondante que l'on a définie dans Calculator et on affiche le resultat, ce qui nous donne:
- (IBAction)add: (id)sender
{
[calculator waitForNTA];
[displayField setFloatValue:[calculator getResult]];
}
- (IBAction)div: (id)sender
{
[calculator waitForNTD];
[displayField setFloatValue:[calculator getResult]];
}
- (IBAction)mult: (id)sender
{
[calculator waitForNTM];
[displayField setFloatValue:[calculator getResult]];
}
- (IBAction)substract: (id)sender
{
[calculator waitForNTS];
[displayField setFloatValue:[calculator getResult]];
}
setFloatValue est une méthode de la classe NSTextField, et vous pouvez donc avoir plus de renseignements sur cette classe dans le Framework AppKit, parcequ'on se sert presque toujours des NSTextField et c'est bien de les connaitre un peu.
Le comportement du bouton égal est très facile à programmer:
- (IBAction)Equals: (id)sender
{
[calculator endOp];
[displayField setFloatValue:[calculator getResult]];
}
Il nous faut encore assigner une action au bouton C:
- (IBAction)reset: (id)sender
{
[calculator reinit];
[displayField setIntValue:0];
}
Et maintenant, c'est presque fini, il n'y a plus qu'a définir Numeric:
- (IBAction)Numeric: (id)sender
{
[calculator handleNumber:[[sender title] floatValue]];
[displayField setFloatValue:[calculator getValueOfCurrentEditingBuffer]];
}
Vous avez sans doute remarqué que nous n'avons pas touché au fichier main.m et c'est normal, la plupart du temps pour des applications Cocoa, on le laisse tel quel, il sert juste à lancer l'application. C'est en fait la seule partie un peu plus procédurale du programme, tout le reste est très Orienté Objet.
Maintenant qu'on a créé toutes les classes qui vont nous servir, il faut encore les instantier. Retour à IB, dans le MainMenu.nib. Selectionnez tour à tour Calculator et IController et instantiez les. Les objets ainsi créés apparaissent logiquement dans l'onglet Instances. Il n'y a plus qu'a créer les connections: pour faire celles des deux outlets de IController, faites deux control-glisser-déposé de IController vers respectivement Calculator (l'objet) et le champ NSTextField. Ensuite pour assigner les actions, on fait encore des controls-clics en partant des boutons vers IControler. A chaque fois, choississez dans "target" l'action appropriée:
Tous les boutons de nombres déclenchent la même action; Numeric.
Sauvegardez votre MainMenu.nib.
Ca y est, c'est fini ! vous pouvez lancer la compilation dans PB et tester.
Ca marche (évidemment?) mais on peut encore améliorer quelques points de détails?
Le paufinage
On va maintenant s'occuper des petits détails qui font une application finie.
Commençons par l'icone. C'est très simple, on prend la zolie icone que nous a préparé tonton Grouiky, on l'appelle NSApplicationIcon et on l'ajoute au projet par simple glisser-déposer. Tout le reste est automatique.
On peut aussi s'occuper des menus: il faut en effacer quelques-uns. Reportez vous au MainMenu.nib téléchargé pour savoir lequels garder.
Ensuite, on peut encore éditer le InfoPlist.strings, toujours dans PB.
Le mien me donne:
CFBundleName = "Basic Calculator";
CFBundleShortVersionString = "Basic Calculator version 1.0";
CFBundleGetInfoString = "Basic Calculator version 1.0, Copyright 2002 Corentin Herbert, Mac4Ever.com.";
NSHumanReadableCopyright = "Copyright 2002 Corentin Herbert, Mac4Ever.com.";
Ces chaînes seront affichées dans la fenêtre de lecture des infos du Finder ou dans la AboutBox de l'application.
Et non, je n'ai pas honte de signer un torchon pareil ! ;)
Mais si vous voulez mettre votre nom à la place?
Le mot de la fin?
Bon, j'espere que grâce à ce tutoriel, vous aurez compris les techniques de base de programmation Cocoa. Je vous conseille aussi le tutoriel "CurencyConverter", que vous trouverez avec la documentation installée par les devTools. Bien sûr, ici, on a fait une application très simple mais on a la base pour l'améliorer si on veut. Vous pourez par exemple ajouter une mémoire, d'autres opérations? (ou aussi localiser le logiciel)
Si l'envie vous en prend, envoyez-moi un mail, je pourrais écrire un article pour expliquer les améliorations