- Programmation windows : chapitre 1 -

Voici le premier chapitre d'une serie de tutoriaux sur la programmation sous Windows. Les pre-requis pour pour pouvoir suivre correctement ces petits documents sont juste une connaissance correcte du C. Je ferais un petit apparté pour présenter un peu quelques concepts du C++ puisque beaucoup de ces concepts se retrouvent sous Windows.



Introduction :

Tout d'abord, pour ce qui est de la programmation sous Windows, plusieurs manieres de faire sont possibles, plus ou moins accessibles et rapides a maitriser :

  • La programmation sous Windows en utilisant les MFCs : les MFCs ( ou Microsoft Fundation Classes ) sont des ensembles de classes ( pour avoir des explications sur ce qu'est une classe voir a la fin de ce chapitre ) de base permettant de programmer plus ou moins aisement sous Windows. Un des avantages de ces classes est qu'elles permettent de developper une application assez rapidement grace a un ensemble d'outil aidant a la mise en place du code general. Par contre, elles demandent une bonne connaissance du C++, sont entierement proprietaires ( Microsoft oblige ) et surtout sont assez lourdes et compliquees a BIEN comprendre ( Je me demande d'ailleurs si le temps soit disant gagne en les utilisant n'est pas perdu a parfaire la maitrise qu'on en a ).

  • La programmation dite "SDK Win32" qui utilise le SDK ( Software Development Kit ou Kit de Developpement Logiciel ) de Windows. Malgré quelques vagues notions de C++, la solution offerte par le SDK est plus "propre" et plus facile a controler au niveau du code final obtenu mais il faut dans ce cas tout faire soit meme. Il s'agit en fait d'un ensemble de fonctions de type C qui repondent directement a des besoins precis ( un SDK quoi ! ).


  • J'ai choisi d'utiliser la deuxieme solution tout simplement parce que c'est la plus simple. Je ferai peut etre un article ou deux sur la programmation utilsant les MFCs, on verra.
    Mais entrons donc dans le vif du sujet.

    1. La programmation sous Windows : un point de vue different.

    Contrairement a DOS qui est un environement mono-tache, mono-utlisateur par excellence, Windows depuis la version 95 est un vrai OS 32 bits et multi-tache pouvant ainsi gerer plusieurs "taches" simultanement.
    Tiens faisons un petit arret sur la notion de "tache" a proprement parler. Une tache est un flot d'execution associé a un certain nombre de structures de base. Regardons ca de maniere generale tout d'abord. Lorsque vous lancez un programme quelconque, un processus est cree a partir de ce programme par le systeme d'exploitation. Un processus est une structure d'execution comprennant en gros :

  • Le code de votre programme,
  • La pile de donnee de votre programme,
  • Les donnees propres a votre programme ( variables, ... ).
  • Et d'autres informations ( droits, ... ),


  • Chaque processus possede son propre espace adressable et ignore completement celui du processus voisin par differentes méthodes de protection mémoire ( MMU, ... ) sur lesquelles je ne veux pas m'etendre ici vu que ce n'est pas le sujet. Windows gère ainsi un grand nombre de ces processus simultanément et leur donne sucessivement, selon une politique d'ordonnancement donnée, le droit à s'éxécuter L'UN APRES L'AUTRE sur le processeur. Et oui ! Multi-tâche ne veut pas dire "environement où les programmes s'éxécutent en parallèle et SIMULTANEMENT les uns par rapport aux autres". En effet, le nombre de processeurs etant en général limité, Windows et les OS multitaches en général, ne peuvent que SIMULER un environement vraiment multitâche en sautant très vite d'un processus à l'autre pour que le processeur puisse executer le code correspondant. Le processus de passage d'un processus a l'autre est appele "changement de contexte" (context switching) ou le mot contexte designe en fait le contexte d'execution du code associé au processus ( defini par les structure ci-dessus : pile de donnée, donnée du processus, etc ... ). Ce changement de contexte, qui arrive à chaque fois que Windows decide de faire s'executer un autre processus que celui qui est actuellement éxécute par le processeur, est en général assez lourd en perte de temps. Il demande de toute facon toujours au moins un minimum de temps pour que le nouveau contexte soit chargé ( pour vous donner une idee, sous QNX qui est un OS temps reel, ce que Windows n'est pas, les temps de changement de context sont de l'ordre de quelques dizaines de micro-secondes ).

    Passons maintenant à la notion de thread, un Thread est un "processus léger" en ce sens qu'il est ... plus léger qu'un processus normal :). Plus sérieusement, un thread est un flot d'éxécution interne à un processus, ... oula ca devient un peu farfelu. Précisons un peu les choses, je vous ai dis qu'un processus possèdait son propre espace adressable complètement indépendant de celui des processus voisins. Et bien, un Thread s'éxécute dans l'espace adressable de son processus "père" et possède, par contre, sa propre pile d'éxécution. Qu'est ce que ça veut dire ? Et bien un thread a acces à TOUTES les variables du processus qui lui a donné naissance, ce que ne peut pas faire un processus avec un processus voisin. Un processus peut ainsi consister en l'execution de un ou plusieurs threads pouvant partager les memes variables. Un processus est ainsi constitué au moins d'un thread d'éxécution : le thread primaire. Et une tâche est, dans ce contexte, un thread. Voila, en gros, on y reviendra plus en profondeur ( en regardant plus en détail l'ordonnancement de ces threads et les différentes politiques proposées par Windows ) plus tard dans un tutorial prochain.
    Un petit schéma pour appuyer ces quelques phrases :




    Voila ce qu'est une tâche ! Et un processus ( un programme plus sa pile, etc .. ) n'est rien d'autre qu'un ou plusieurs thread d'execution !

    Voila pour ce petit aparté que je trouvais nécessaire pour fixer les idées concernant ce qu'était un environement multitâche. Maintenant retour à la programmation Windows à proprement parler !


    2. Windows : des messages, des messages et des messages !!!

    Sous DOS, il etait habituel de concevoir des programmes lienaires dont on maitrisait l'ordre d'execution, et bien sous Windows, et sous n'importe quel environnement graphique, la maitrise de l'ordre d'execution d'une application est moindre. Elle depend en effet des solicitations exterieures. Il va donc falloir adapter notre maniere de programmer a ce type d'environnements. On appel ca de la programmation evenementielle car notre programme va devoir REAGIR a des EVENEMENTS exterieurs. Nous allons ainsi indiquer ce que nous voulons que notre application fasse lorsque l'utilisateur appui sur tel bouton ou sur tel case a cocher mais il faut bien garder en tete le fait que nous n'avons aucune maitrise de l'instant auquel ces bouts de codes associes vont etre executes. Il y a une vieille image que j'aime bien permettant de pas trop mal fixer les idees sur cette notion, elle est l'oeuvre de Shaun Dore : dans un programme classique si nous voulons servir une tasse de cafe, nous allons prendre une tasse, puis y verser du cafe, puis prendre une cuillere, puis y ajouter du sucre etc ... pour un style de programmation evenementielle nous allons amener un plateau avec dessus une tasse, une cuillere, du sucre etc ... et nous allons laisser faire l'utilisateur et REAGIR a ses actions. Comme nous l'avons vu plus haut, Windows doit gérer plusieurs dizaines de Threads et gérer leur temps d'éxécution sur le processeur pour donner l'illusion qu'ils s'éxécutent tous en meme temps. Maintenant, toutes les applications associé a ces Threads ne sont pas isolées dans une bulle tout seul dans leur coin. Elles doivent prendre en compte les solicitations exterieures ! Quels types de solicitations me direz vous ? Et bien puisque Windows est un environement graphique les clics de souris, les deplacement de fenetres, les minimalisations/maximisation de fenetres, etc ...
    Tous ces messages sont gérés en interne et envoyés directement à leur destinataires par Windows. Vu qu'on va travailler avec des fenêtres ( environnement graphique oblige ) nous aurons à nous occuper des differents messages qu'on peut nous adresser et de leur gestion. On verra ca plus en détail dans la partie qui va concerner les fenêtres.

    Bon trève de blabla, passons à la pratique !


    3. Mon premier programme Windows : "Hello World".

    Tout programme windows possède une fonction WinMain ( qui remplace la fonction main() du C classique ). Cette fonction va être le point d'entrée de votre programme lors de l'execution de celui ci. Elle possède tout comme le point d'entrée classique en C ( int main( int argc, char **argv ) ), un certain nombre de paramètres :

    int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd );

  • HINSTANCE hInstance : hInstance est un handler ( un identificateur, une ID propre ) de votre application de type HINSTANCE. Il va etre extremement important puisqu'il va identifier votre application parmi les autres.
  • HINSTANCE hPrevInstance : est un parametre obsolete herite de Window 3.1 ( Win16 ) ou chaque application devait etre lancee avec une application "Mere".
  • LPSTR lpCmdLine : il s'agit d'un pointeur sur la ligne de commande ( nom du programme exclu ) de votre programme sous forme de chaine de caracteres terminee par un '\0' habituel.
  • int nShowcmd : nous donne des informations sur la maniere dont la fenetre doit etre visualisee. On ne s'en sert pas pour l'instant.

  • Le APIENTRY précèdent le WinMain est obligatoire et indique a Windows que pour la fonction qui suit l'ordre de passage des parametres est inverse par rapport a l'ordre classique. Voila pour le WinMain().

    Maintenant, voici le fameux Hello World :

    #include < windows.h >
    #include < windowsx.h >

    int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd )
    {
    MessageBox(NULL, "Hello World !!", "Mon programme", MB_OK);
    return(0);
    }



    Voila !!! La fonction MessageBox(...) est une fonction qui vous affiche une boite de dialogue de base contenant un message defini pour vous meme. Le premier paramètre doit etre un handle ( un identificateur ) de la fenêtre mère de cette petite boite. On met NULL ici pour dire que la notre est orpheline et autonome.
    Les headers inclus au début de ce programme seront à inclure à chaque programme ( le premier étant indispensable, le deuxième contenant quelques macros, ... utiles ) Voila, voila.

    On a fait un peut le tour de quelques notions de base sous Windows. Le deuxième chapitre sera consacré à la gestion des fenêtres ( création, messages, ... ). On fera joujou avec quoi.
    Biensur pour toutes remarques, questions, ... emails !




    ANNEXE 1 : La notation hongroise

    Vous avez surement du remarquer les notations utilisees pour les variables du WinMain du genre : lpCmdLine, hInstance, ... Et bien, ces notations ne sont pas innocentes et correspondent a une logique d'uniformisation de la notation des variables sous Windows. Lorsqu'un programme et ses sources doivent etre partagés par un grand nombre de personnes, il est evident qu'il faut pouvoir s'y retrouver facilement pour eviter, en plus de passer du temps a comprendre la structure du code et ce qu'il fait effectivement, de perdre du temps supplementaire en se perdant dans des variables aux noms abstraits.
    Voici un exemple de ce type de programme :

    // mes variables globales
    int x,y,k;
    char *buffer;
    HINSTANCE i;

    .....

    void MyFunction(int u, int v, char *buf)
    {
    buffer = buf;
    x = u;
    k++;
    }

    Ce programme est illisible !!! Les noms de variables ne veulent rien dire, on ne sait pas ce qu'on manipule, ... Dans le cas, de code enormes du type de celui de Windows mettre au point une convention d'ecriture est une obligation et une necessite.
    Cette convention a ete mise au point par un Monsieur qui a donne son nom a cette notation. Grossierement, chaque variable est prefixee d'une sorte d'identificateur predefini nous renseignant sur son type, voici une liste des plus utilisees :

  • "lp" : long pointer designe un pointeur,
  • "sz" : designe une chaine de caractere terminee par un '\0',
  • "h" : designe une variable de type handler sur un objet,
  • "w" : designe une variable de type Word ( 16 bits ),
  • "dw" : designe une variable de type Double Word ( 32 bits ),
  • etc, etc ...

    Il convient de ne pas trop abuser de cette notation car les variables et le code en general deviennent tres vite lourd ce qui s'eloigne du but premier de cette notation mais de s'en inspirer pour rendre ses programmes plus lisibles. ( un article sur les problemes de lisibilite/organisation du code viendra plus tard )
    Voici le programme ci-dessus ecrit avec une notation de type hongroise :

    // mes variables globales
    int g_x, g_y, g_k;
    char *g_lpBuffer;
    HINSTANCE g_hInst;

    .....

    void MyFunction(int u, int v, char *lpBuf)
    {
    g_lpBuffer = lpBuf;
    g_x = u;
    g_k++;
    }

    Voila, meme si c'est pas parfait, c'est deja un peu plus lisible que le premier.



    ANNEXE 2 : quelques principes du C++ et de la programmation objet

    Je vais presenter, ici, quelques principes objets et du C++.
    La programmation objet correspond a des besoins de structuration et de conceptualisation devenu necessaires dans de tres nombreux domaines. Il permet une gestion et une implementation complete d'abstraction donnee qu'il seraient tres complique et fouilli d'organiser en temps normal. Je ne suis pas tres clair je le sais mais vous allez bientot comprendre.
    Prenons l'exemple d'un vehicule quelconque. Un vehicule possede un certain nombre d'attributs tel que la couleur, le nombre de portes, etc ... Bon, ensuite avec un vehicule donne, il est possible de faire beaucoup de choses du genre freiner, accelerer, tourner, etc ...
    Nous sommes, ici, en presence d'un concept : le concept de vehicule associe a ses caracteristiques et a un certain nombres de choses qu'on peut faire avec. Ces deux derniers ensembles definissent ce qu'est un vehicule dans sa globalite. Il n'y a pas besoin d'ne savoir plus. A partir du moment ou on connait ces caracteristiques et ces actions possibles il nous est possible de le construire ce vehicule.
    Bon, maintenant d'un point de vue programmation, comment est ce qu'on representerait un vehicule si jamais on devait traiter des donnees de ce type dans un langage non objet. En C, par exemple, un vehicule pourrait etre defini comme ceci :

    
    struct vehicule 
    {   
    	// les caracteristiques de notre vehicule
       	int 	m_couleur;
       	int 	m_nombrePortes;
       	bool 	bABS;
       	....
    };
    
    // les actions possibles avec un vehicule
    void Accelerer( struct vehicule );
    void Freiner( struct vehicule );
    
    Comme on le voit ci-dessus, il n'y a aucune relation logique entre la declaration de la structure de type vehicule avec ses caracteristiques et les actions qu'on peut faire avec. Ensuite, il n'y a aucun mecanisme natifs permettant de gerer plus profondement ce type d'objets. En effet, un vehicule c'est vaste ... un camion et une voiture de course n'ont pas les memes caracteristiques et ne peuvent pas faire les memes choses. Il faudrait donc si on veut pouvoir creer a la foid des formules 1 et des camions ajouter dans les caracteristiques et dans les actions les caracteristiques propres a chaque nouveau type de vehicule. Ce qui serait super lourd !!! Il faudrait prevoir TOUTES les caracteristiques de chaque nouveau type de vehicule, et TOUTES les actions ...

    Le C++ et les langages objets sont prevus pour ca. Pour gerer ce type de concepts abtraits ( ou qui peuvent l'etre ) non prevu nativement par le langage ( du genre float, ... ).
    Il est ainsi possible de reunir directement les actions possibles et les caracteristiques de chaque concept a travers ce qu'on appelle : une Classe.
    Une classe est la definition d'un concept nouveau. Notre classe vehicule serait ainsi :
    
    class CVehicule 
    {
    public:
    	// les caracteristiques de notre vehicule
    	int 	m_couleur;
    	int 	m_nombrePortes;
       	bool 	m_bABS;
       	....
    
    	// les actions possibles avec un vehicule
       	void 	Accelerer( struct vehicule );
       	void 	Freiner( struct vehicule );
       	....
    }; 
    
    

    On a ainsi reuni les caracteristiques et les methodes. Maintenant, a travers divers mecanismes , un langage objet donne des possibilites de specialiser une classe pour creer, par exemple, une classe CCamion, une classe CFormule1, ... grace a un mecanisme appelé héritage , d'adapter des actions ( ou methodes en vocabulaire objet ) generales en fonction de l'objet utilise, etc ...

    Voila pour une vue globale tres tres rapides du C++ et des langages objets.


    NOTE: remarquez les notations utilisees pour les noms de classe ( CVehicule, CCamion, ... ) et pour les variables/fonctions membres ( m_couleur, ... ) qu'on retrouve pas mal dans les codes et qu'il est bon d'utiliser ( dans le meme esprit que la notation hongroise ) dans votre code pour le nom d'une classe.

    A++

    --
    Document ecrit par ABREU Alexandre : wiss1976@yahoo.fr
    Libre reproduction et diffusion autorisée - modifications interdites sans autorisation de l'auteur.

    Suivant