IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

La programmation Win32 en Virtual Pascal avec OWL


précédentsommairesuivant

VII. GDI

Pour varier les plaisirs, nous allons à présent aborder une partie plus amusante du tutoriel : la gestion de l'interface graphique.

Ceux d'entre vous qui ont pratiqué la programmation sous MS-DOS savent qu'une des plus grosses difficultés vient de la nécessaire compatibilité des programmes avec le hardware. Or, une fois que l'on s'aventure hors des sentiers balisés par les fonctions du DOS et du BIOS, on s'expose à la quasi-absence de standardisation.
Windows affranchit le programmeur de tous les tracas liés à la gestion du hardware. Chaque constructeur fournit les pilotes nécessaires à la gestion de son matériel et les programmes Windows s'adressent non plus directement au hardware mais bien à l'API qui, elle, se charge des tâches de ce niveau.

Pour tout ce qui concerne ce qui doit être dessiné, que ce soit sur un écran ou sur une imprimante, Windows fournit un panel de fonctions regroupées dans la bibliothèque GDI (pour Graphics Device Interface).

VII-A. Contexte de périphérique

Le principe premier du travail avec GDI est l'utilisation de contextes de périphérique (DC pour Device Context). Un DC est un objet d'interface lié à un périphérique. Pour un programme, il n'y a aucune différence entre un contexte de périphérique lié à un écran et un contexte de périphérique lié à une imprimante. Vous pouvez d'ores et déjà considérer que l'essentiel des notions que nous allons voir nous serviront lorsque nous aborderons l'impression sous Windows !

Le nombre de contextes de périphérique que Windows peut fournir est limité. Tout DC demandé à Windows doit être libéré le plus rapidement possible

VII-B. La méthode PAINT : le programme CERCLE.PAS

Au niveau de l'affichage à l'écran, le travail de Windows est de faire en sorte que tous les éléments graphiques soient correctement dessinés. Au besoin, Windows répare, nettoie, réaffiche les fenêtres, icônes, menus, images, textes, etc.

Chaque application doit mettre à la disposition de Windows toutes les instructions nécessaires pour redessiner ses éléments graphiques

Ce principe est important : non seulement une application doit être capable de dessiner ses éléments à l'écran une première fois, mais elle doit aussi être capable de les redessiner chaque fois que Windows le lui demandera !

Dans la bibliothèque OWL, c'est le rôle assigné à la méthode Paint de l'objet tWindow : elle est chargée de redessiner à tout instant l'entièreté de la zone client de la fenêtre.

Au premier abord, ce principe peut paraître peu efficace : une fenêtre qui, après la plus infime modification, se redessine complètement. En fait pas tant que cela car, à ce principe, vient se greffer un autre : le clipping. En fait, Windows limitera au strict minimum la portion de fenêtre dont l'affichage doit être mis à jour. Un programme OWL n'a pas à se soucier de la manière dont Windows doit travailler : on se contente, dans la méthode Paint, de dessiner toute la zone client; dans la pratique, grâce au clipping, seule la partie visible sera redessinée par le système.

Voyons à présent la déclaration de la méthode Paint :

 
Sélectionnez
Procedure PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct); virtual;
  • PaintDC est le handle du contexte de périphérique à utiliser
  • PaintInfo est une structure que nous ne verrons pas en détail (voyez le type PAINTSTRUCT dans le SDK)

La méthode Paint fournit automatiquement le contexte de périphérique dans lequel il faut dessiner la fenêtre, PaintDC, ce qui est bien pratique.

Le contexte de périphérique PaintDC n'a pas à être libéré par le programme : les routines OWL s'en chargent toutes seules en temps opportun

Rien de tel qu'un exemple pratique pour aider à comprendre : nous allons réaliser un minuscule programme qui affiche un cercle et met en évidence, à l'aide d'un bip sonore, chaque appel de la méthode Paint.

 
Sélectionnez
Program CERCLE;

(* Mise en évidence du fonctionnement de la méthode tWindow.Paint.

   Réalisé par Alcatîz pour Developpez.com - 27-08-2006 *)


Uses Windows,    (* API Win32 *)
     OWindows;   (* Objets OWL *)


Type pFenetrePrincipale = ^tFenetrePrincipale;
     tFenetrePrincipale = Object(tWindow)
                            Procedure PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
                               virtual;
                               (* Dessin d'un cercle et bip sonore *)
                          end;

     tProgramme = Object(tApplication)
                    Procedure INITMAINWINDOW; virtual;
                  end;


(* ----- Méthodes de l'objet tFenetrePrincipale ----- *)

Procedure tFenetrePrincipale.PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
(* Dessin d'un cercle et bip sonore *)
Begin
  Ellipse(PaintDC,10,10,100,100);
  MessageBeep(mb_IconExclamation);
End;


(* ----- Méthodes de l'objet tProgramme ----- *)

Procedure tProgramme.INITMAINWINDOW;
(* Allocation de la fenêtre principale du programme *)
Begin
  MainWindow := New(pFenetrePrincipale,INIT(Nil,'La m'#233'thode tWindow.Paint'));
End;


Var Programme : tProgramme;


(* ----- Programme principal ----- *)

Begin
  Programme.INIT('Cercle');
  Programme.RUN;
  Programme.DONE;
End.

Si vous compilez et exécutez ce petit programme, vous pouvez constater auditivement que la méthode Paint est appelée très souvent : lorsque vous redimensionnez la fenêtre, lorsque vous restaurez sa taille après l'avoir réduite en icône, lorsqu'elle a été totalement ou partiellement masquée par une autre fenêtre... Par contre, elle n'est pas exécutée lorsque vous déplacez la fenêtre ou lorsque vous ouvrez le menu système en cliquant sur l'icône dans la barre de titre. Car, dans ces cas, pour des raisons de performances, Windows effectue lui-même une sauvegarde et une restauration de la zone à redessiner.

Nous allons étudier la fonction Ellipse car beaucoup de fonctions de GDI sont calquées sur elle :

 
Sélectionnez
Function Ellipse (DC : hDC; X1, Y1, X2, Y2 : Integer) : Bool;

Toutes les fonctions de dessin demandent comme paramètre le handle du contexte de périphérique

DC est le handle du contexte de périphérique dans lequel l'ellipse doit être dessinée. Il s'agit présentement de PaintDC, qui est fourni par OWL.

(X1,Y1) sont les coordonnées du point haut-gauche et (X2,Y2) celles du point bas-droite du rectangle dans lequel s'inscrit l'ellipse. Comme nous voulions un cercle, nous avons dessiné une ellipse inscrite dans un carré de coordonnées (10,10) à (100,100).
Ne cherchez pas de fonction Circle, il n'y en a pas ;-)

Généralement, la valeur booléenne de retour de la fonction n'est pas testée. Il pourrait y avoir une erreur si le contexte de périphérique était invalide mais c'est souvent au niveau de la création de celui-ci qu'une erreur est détectée.

Pour terminer, parlons deux minutes de la fonction MessageBeep car elle peut être bien utile pour déboguer rapidement un programme : au lieu de poser un point d'arrêt pour vérifier qu'une routine est bien appelée, on peut se contenter d'un bip sonore en début de routine. Le paramètre de MessageBeep ne vous est sûrement pas inconnu : mb_IconExclamation a déjà été rencontré lorsque nous avons abordé la fonction MessageBox. Selon la constante mb_IconXXX utilisée, l'avertissement sonore sera différent. Notez que la valeur FFFFFFFFh provoque un bip du haut-parleur interne mais que cela ne fonctionne pas toujours.

Dans notre programme, l'ellipse est dessinée à l'aide des objets de dessin sélectionnés par défaut dans le contexte de périphérique : un pinceau pour le fond et un crayon pour la bordure. Nous allons voir comment créer des crayons et des pinceaux personnalisés.

VII-B-1. Crayons et pinceaux

VII-B-1-a. Création d'un crayon

La fonction GDI CreatePen permet de créer un crayon :

 
Sélectionnez
Function CreatePen (Style, Width : Integer; Color : tColorRef) : hPen;

Le paramètre Style définit l'aspect du crayon : ligne pleine, pointillés, etc. Voici un aperçu des possibilités :

Constante Aspect
ps_Solid Ligne pleine
ps_Dash Ligne composée de tirets
ps_Dot Ligne pointillée
ps_DashDot Alternance de tirets et de points
ps_DashDotDot Alternance tiret - point - point
ps_Null Ligne invisible
ps_InsideFrame Ligne pleine d'épaisseur supérieure à 1, utilisée pour le tracé de formes géométriques, dont l'épaisseur sera dessinée à l'intérieur de la forme pour ne pas dépasser les limites de celle-ci


Width indique l'épaisseur de la mine du crayon. Une valeur nulle assure une épaisseur de 1 pixel, quelle que soit la résolution de l'écran. Notez qu'une épaisseur supérieure à 1 ne peut être appliquée à une ligne composée de points et/ou de tirets.

Pour terminer, Color est la couleur du crayon. Le type tColorRef est en fait un LongInt issu de la composition des trois couleurs primaires : rouge, vert et bleu. La macro RGB permet de créer très facilement une couleur; elle attend trois valeurs comprises entre 0 et 255, correspondant respectivement à l'intensité de rouge (Red), vert (Green) et bleu (Blue). (0,0,0) donnera du noir et (255,255,255) donnera du blanc !

La valeur retournée par CreatePen est le handle du crayon créé.

VII-B-1-b. Création d'un pinceau

A la différence des crayons, qu'une unique fonction permet de créer (avec un paramètre d'aspect), il existe pour les pinceaux deux fonctions : CreateSolidBrush pour les pinceaux unis et CreateHatchBrush pour les pinceaux à motifs. Voici leur déclaration :

 
Sélectionnez
Function CreateSolidBrush (Color : tColorRef) : hBrush;
Function CreateHatchBrush (Style : LongInt; Color : tColorRef) : hBrush;

Le paramètre Color est absolument identique à celui que nous venons de voir avec la fonction CreatePen.

Le paramètre Style de la fonction CreateHatchBrush définit un motif :

Constante Aspect
hs_Horizontal Hachures horizontales
hs_Vertical Hachures verticales
hs_Cross Quadrillage horizontal et vertical
hs_FDiagonal Hachures diagonales de haut-gauche vers bas-droite (\)
hs_BDiagonal Hachures diagonales de bas-gauche vers haut-droite (/)
hs_DiagCross Quadrillage diagonal (X)


La valeur retournée par ces deux fonctions est le handle du pinceau créé.

VII-B-1-c. Sélection des outils de dessin dans le DC

Une fois que les outils de dessin sont créés, il faut demander à Windows de les utiliser pour dessiner. Pour ce faire, on les sélectionne dans le contexte de périphérique. Il faut utiliser la fonction SelectObject :

 
Sélectionnez
Function SelectObject (DC : hDC; GDIObj : hGDIObj) : hGDIObj;

Cette fonction est à usage multiple, c'est-à-dire qu'elle permet de sélectionner tous les types d'objets dans un contexte de périphérique. Elle attend comme paramètre le handle de l'objet à sélectionner et retourne le handle de l'objet qui était sélectionné jusqu'alors, ce qui est très important.

Avant de libérer un contexte de périphérique, il faut le restaurer dans son état initial

C'est primordial, sous peine de s'exposer à des instabilités d'affichage ultérieures. D'où l'intérêt de récupérer le handle renvoyé par SelectObject, afin de pouvoir resélectionner l'objet initial avant de libérer le DC.

VII-B-1-d. Destruction des outils de dessin

Lorsque Windows crée un objet GDI comme un crayon ou un pinceau, il alloue de la mémoire dans le tas global. C'est au programme - et non au système, comme on pourrait le croire - de s'assurer que les objets créés sont détruits.
Une autre fonction à usage multiple, DeleteObject, permet de supprimer des objets GDI de tous types :

 
Sélectionnez
Function DeleteObject (GDIObject : tHandle) : Bool;

Le seul paramètre de cette fonction est le handle de l'objet à détruire.

Très important : il ne faut jamais détruire un objet encore sélectionné dans un DC

Il faut donc toujours effectuer les opérations dans cet ordre :

  1. Désélectionner l'objet avec SelectObject, en resélectionnant l'objet sélectionné précédemment
  2. Détruire l'objet avec DeleteObject
VII-B-1-e. Illustration : amélioration de la méthode PAINT

Pour bien fixer les idées, nous allons améliorer la méthode Paint de notre programme CERCLE.PAS en définissant l'aspect de la bordure et du fond du cercle :

 
Sélectionnez
Procedure tFenetrePrincipale.PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
(* Dessin d'un cercle et bip sonore *)
Var AncienCrayon, Crayon : hPen;
    AncienPinceau, Pinceau : hBrush;
Begin
  (* Création des outils de dessin *)
  Crayon := CreatePen(ps_Solid,3,RGB(255,0,0));
  AncienCrayon := SelectObject(PaintDC,Crayon);
  Pinceau := CreateSolidBrush(RGB(128,128,128));
  AncienPinceau := SelectObject(PaintDC,Pinceau);
  (* Dessin *)
  Ellipse(PaintDC,10,10,100,100);
  (* Destruction des outils de dessin *)
  SelectObject(PaintDC,AncienPinceau);
  DeleteObject(Pinceau);
  SelectObject(PaintDC,AncienCrayon);
  DeleteObject(Crayon);
  (* Bip sonore *)
  MessageBeep(mb_IconExclamation);
End;

Tout ce que nous venons de voir s'y trouve :

  • La création d'un crayon rouge d'épaisseur 3 à l'aide de CreatePen
  • La sélection de ce crayon dans le DC à l'aide de SelectObject, en sauvegardant le handle de l'ancien crayon
  • La création d'un pinceau uni avec CreateSolidBrush
  • La sélection du pinceau dans le DC
  • Le dessin du cercle, qui n'a pas changé
  • La désélection du crayon et du pinceau, en resélectionnant les anciens outils avec SelectObject
  • La destruction du crayon et du pinceau et du crayon avec DeleteObject

Cette séquence d'opérations, avec bien entendu toutes sortes de variantes, sera une constante dans tous nos programmes.

Je vous encourage à expérimenter différents motifs de pinceaux et de crayons, en essayant les différentes valeurs données plus haut.

VII-C. Dessin hors d'une méthode PAINT : le programme ONDOYANT.PAS

Le dessin d'une fenêtre dans une méthode Paint est bien commode mais ce n'est pas la seule manière possible de produire des graphismes à l'écran. Elle est même inutilisable dans certains cas.

Nous allons réaliser un programme qui dessine des lignes ondoyantes de couleur sur un fond noir :

Image non disponible

Le programme n'affichera simultanément qu'une centaine de lignes, donc se chargera d'effacer les lignes les plus anciennes avant d'afficher les lignes les plus récentes. Les couleurs sont choisies aléatoirement mais, pour des raisons esthétiques, il n'y a de changement de couleur que toutes les 10 lignes. La direction et la taille des lignes sont choisies de manière à donner l'impression qu'elles rebondissent sur les bords de la fenêtre.

Voici le source du programme ONDOYANT.PAS :

 
Sélectionnez
Program ONDOYANT;

(* Animation de lignes ondoyantes.

   Réalisé par Alcatîz pour Developpez.com - 29-08-2006 *)


{$R ONDOYANT.RES}


Uses Windows,    (* API Win32 *)
     OWindows;   (* Objets OWL *)



Const IdTimer = 11;

      IndiceMax = 100;     (* Nombre de lignes simultanées *)
      VitesseTimer = 50;   (* Nombre de ms du timer *)

      dx1 : LongInt = 4;
      dy1 : LongInt = 10;
      dx2 : LongInt = 3;
      dy2 : LongInt = 9;


{$I ONDOYANT.INC}


Type tLigne = Record
                x1, y1, x2, y2 : LongInt;
                Couleur : tColorRef;
              end;
     tTabLignes = Array [0..IndiceMax] of tLigne;

     pFenetrePrincipale = ^tFenetrePrincipale;
     tFenetrePrincipale = Object(tWindow)
                            PinceauFond : hBrush;
                            TabLignes : tTabLignes;
                            IndiceGeneral : LongInt;
                            Effacer : LongBool;
                            Constructor INIT (aParent : pWindowsObject; aTitle : pChar);
                               (* Création pinceau fond - Initialisation des champs *)
                            Procedure SETUPWINDOW;
                               virtual;
                               (* Mise en route du timer *)
                            Function GETCLASSNAME : pChar;
                               virtual;
                               (* Retourne le nom de classe de la fenêtre *)
                            Procedure GETWINDOWCLASS (var aWndClass : tWndClass);
                               virtual;
                               (* Fond noir pour la fenêtre *)
                            Procedure NOUVELLELIGNE (DC : hDC;
                                                     var Coord, Intervalle : LongInt;
                                                     CoordMax : LongInt;
                                                     var Couleur : tColorRef);
                               (* Nouvelle coordonnée, nouvel intervalle et nouvelle couleur *)
                            Procedure DESSINLIGNE (DC : hDC; i : Integer);
                               (* Dessine ou efface la ligne d'indice i *)
                            Procedure WMTIMER (var Msg : tMessage);
                               virtual wm_First + wm_Timer;
                               (* Dessin de 10 lignes *)
                            Procedure WMDESTROY (var Msg : tMessage);
                               virtual wm_First + wm_Destroy;
                               (* Destruction du timer *)
                            Destructor DONE;
                               virtual;
                               (* Destruction du pinceau pour le fond *)
                          end;

     tProgramme = Object(tApplication)
                    Procedure INITMAINWINDOW;
                       virtual;
                       (* Allocation de la fenêtre principale du programme *)
                  end;


(* ----- Méthodes de l'objet tFenetrePrincipale ----- *)

Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
(* Création du pinceau noir pour le fond - Initialisation des champs *)
Var h : hBitmap;
    i : LongInt;
Begin
  tWindow.INIT(aParent,aTitle);
  (* Création du pinceau pour le fond *)
  h := LoadBitmap(hInstance,pChar(id_FondNoir));
  if h <> 0
     then
       begin
         PinceauFond := CreatePatternBrush(h);
         DeleteObject(h);
       end;
  (* Initialisation des champs *)
  for i := 0 to IndiceMax do
    TabLignes[i].x1 := -1;
  IndiceGeneral := 0;
  Effacer := False;
End;

Procedure tFenetrePrincipale.SETUPWINDOW;
(* Mise en route du timer *)
Begin
  tWindow.SETUPWINDOW;
  SetTimer(hWindow,IdTimer,VitesseTimer,Nil);
End;

Function tFenetrePrincipale.GETCLASSNAME : pChar;
(* Retourne le nom de classe de la fenêtre *)
Begin
  GETCLASSNAME := 'Ondoyant';
End;

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Fond noir pour la fenêtre *)
Begin
  tWindow.GETWINDOWCLASS(aWndClass);
  aWndClass.hbrBackground := PinceauFond;
End;

Procedure tFenetrePrincipale.NOUVELLELIGNE (DC : hDC;
                                            var Coord, Intervalle : LongInt;
                                            CoordMax : LongInt;
                                            var Couleur : tColorRef);
(* Détermination nouvelle coordonnée, nouvel intervalle et nouvelle couleur *)
Var Total : LongInt;   (* Calcul nouvelle coordonnée *)
    Signe : LongInt;   (* Signe à appliquer au calcul nouvel intervalle *)
Begin
  Total := Coord + Intervalle;
  if (Total < 0) or (Total > CoordMax)
     then
       begin
         (* Calcul du signe à appliquer *)
         if Intervalle >= 0
            then
              Signe := -1
            else
              Signe := 1;
         (* Nouvel intervalle *)
         Intervalle := Signe * (3 + Random(12));
         (* Nouvelle couleur *)
         repeat
           Couleur := RGB(Random(256),Random(256),Random(256));
         until Couleur <> GetBkColor(DC);
       end
     else
       Coord := Total;
End;

Procedure tFenetrePrincipale.DESSINLIGNE (DC : hDC; i : Integer);
(* Dessine ou efface la ligne d'indice i *)
Var AncienCrayon, Crayon : hPen;
    AncienneROP : LongInt;
Begin
  with TabLignes[i] do
    begin
      Crayon := CreatePen(ps_Solid,1,Couleur);
      AncienCrayon := SelectObject(DC,Crayon);
      AncienneROP := SetROP2(DC,r2_XORPen);
      MoveTo(DC,x1,y1);
      LineTo(DC,x2,y2);
      SelectObject(DC,AncienCrayon);
      DeleteObject(Crayon);
      SetROP2(DC,AncienneROP);
    end;
End;

Procedure tFenetrePrincipale.WMTIMER (var Msg : tMessage);
(* Dessin de 10 lignes *)
Var DC : hDC;                 (* Contexte d'affichage *)
    ClientRect : tRect;       (* Taille de la zone client *)
    i : LongInt;              (* Indice dans le tableau *)
    AncienIndice : LongInt;   (* Sauvegarde de l'indice général *)
Begin
  DC := GetDC(hWindow);
  GetClientRect(hWindow,ClientRect);
  for i := 1 to 10 do
    begin
      AncienIndice := IndiceGeneral;
      if IndiceGeneral = IndiceMax - 1
         then
           begin   (* Le tour du tableau est fait *)
             IndiceGeneral := 0;
             Effacer := True;
           end
         else
           Inc(IndiceGeneral);
      if Effacer
         then   (* Effacement par dessin avec opération XOR *)
           DESSINLIGNE(DC,IndiceGeneral);
      TabLignes[IndiceGeneral] := TabLignes[AncienIndice];
      (* Création d'une nouvelle ligne *)
      with TabLignes[IndiceGeneral] do
        begin
          NOUVELLELIGNE(DC,x1,dx1,ClientRect.Right,Couleur);
          NOUVELLELIGNE(DC,y1,dy1,ClientRect.Bottom,Couleur);
          NOUVELLELIGNE(DC,x2,dx2,ClientRect.Right,Couleur);
          NOUVELLELIGNE(DC,y2,dy2,ClientRect.Bottom,Couleur);
        end;
      (* Dessin de la nouvelle ligne *)
      DESSINLIGNE(DC,IndiceGeneral);
    end;
  ReleaseDC(hWindow,DC);
End;

Procedure tFenetrePrincipale.WMDESTROY (var Msg : tMessage);
(* Destruction du timer *)
Begin
  KillTimer(hWindow,IdTimer);
  tWindow.WMDESTROY(Msg);
End;

Destructor tFenetrePrincipale.DONE;
(* Destruction du pinceau pour le fond *)
Begin
  DeleteObject(PinceauFond);
  tWindow.DONE;
End;


(* ----- Méthodes de l'objet tProgramme ----- *)

Procedure tProgramme.INITMAINWINDOW;
(* Allocation de la fenêtre principale du programme *)
Begin
  MainWindow := New(pFenetrePrincipale,INIT(Nil,'Lignes ondoyantes'));
End;


(* ----- Variables globales ----- *)

Var Programme : tProgramme;


(* ----- Programme principal ----- *)

Begin
  Randomize;
  Programme.INIT('Ondoyant');
  Programme.RUN;
  Programme.DONE;
End.

Détaillons notre programme.

VII-C-1. Ressources

La seule ressource utilisée par le programme est une image bitmap monochrome de 64 X 64 pixels, entièrement noire. Elle servira à peindre le fond de la fenêtre, comme nous l'avons déjà vu dans le programme FONDEMAR.PAS.
L'identificateur de la bitmap, id_FondNoir, se trouve dans le fichier include ONDOYANT.INC, lié au source par la directive

 
Sélectionnez
{$I ONDOYANT.INC}

La ressource compilée sous forme binaire se trouve, elle, dans le fichier ONDOYANT.RES, lié à notre programme par la directive

 
Sélectionnez
{$R ONDOYANT.RES}

Tout ceci n'est qu'un rappel de ce qui a déjà été vu au Chapitre VI.
C'est ainsi que :

  • Le pinceau noir pour le fond est créé dans la méthode Init
  • Ce pinceau est affecté à la classe de fenêtre dans la méthode GetWindowClass
  • Le pinceau est détruit dans le destructeur Done

VII-C-2. Timer

Pour ses graphismes animés, notre programme utilise une horloge, un timer. Windows peut mettre à la disposition des applications un nombre restreint d'horloges réglables en millisecondes. Toutefois, la précision de ces horloges est relativement faible; c'est dû à la manière dont le système gère le multitâche. Mais pour notre programme, il n'y a pas besoin d'une grande précision.

Nous allons donc créer un timer et lui demander d'envoyer un message wm_Timer à la fenêtre principale du programme à chaque tic d'horloge. C'est dans une méthode virtuelle indexée WMTIMER que nous allons dessiner les lignes ondoyantes.

La création du timer s'effectue dans la méthode SetupWindow :

 
Sélectionnez
Procedure tFenetrePrincipale.SETUPWINDOW;
(* Mise en route du timer *)
Begin
  tWindow.SETUPWINDOW;
  SetTimer(hWindow,IdTimer,VitesseTimer,Nil);
End;

Pourquoi dans cette méthode ? Parce que la fonction SetTimer a besoin du handle de la fenêtre à laquelle les messages wm_Timer sont destinés. Le champ hWindow d'un objet tWindow contient son handle.

La méthode SetupWindow est le premier endroit où le champ hWindow d'une fenêtre est défini

C'est le rôle premier de SetupWindow : permettre de configurer une fenêtre avant qu'elle soit affichée, en ayant déjà son handle à disposition.

Détaillons à présent la fonction SetTimer :

 
Sélectionnez
Function SetTimer (Wnd : hWnd; IdEvent, Elapse : UInt; TimerFunc : tFNTimerProc) : UInt;
  • Wnd est le handle de la fenêtre à laquelle les messages wm_Timer sont destinés
  • IdEvent est l'identificateur du timer
  • Elapse est la fréquence des tics d'horloge, en millisecondes
  • TimerFunc est l'adresse d'une fonction de rappel (technique que nous n'utilisons pas ici, donc Nil)

Dans la déclaration des constantes de notre programme, nous avons défini IdTimer comme identificateur pour le timer. Le rôle de l'identificateur du timer est de permettre, éventuellement, de différencier les messages wm_Timer envoyés par plusieurs timers créés simultanément.

Il ne faut surtout pas oublier de détruire le timer avant la fin du programme !

Un bon endroit pour détruire le timer est une méthode WMDESTROY :

Alors que la méthode SetupWindow est le premier endroit où le handle hWindow est défini, WMDESTROY est le dernier endroit où celui-ci est valide, dans le processus de destruction de la fenêtre

La fonction utilisée pour détruire un timer est KillTimer. Il faut bien sûr l'appeler avant d'appeler la méthode WMDESTROY de l'objet ancêtre tWindow :

 
Sélectionnez
Procedure tFenetrePrincipale.WMDESTROY (var Msg : tMessage);
(* Destruction du timer *)
Begin
  KillTimer(hWindow,IdTimer);
  tWindow.WMDESTROY(Msg);
End;

VII-C-3. Dessin des lignes

Comme nous l'avons déjà dit, c'est dans la méthode WMTIMER que les lignes sont dessinées. Nous les dessinons par lots de 10 de couleur identique; vous pouvez bien sûr changer ce nombre.

Voici la méthode WMTIMER :

 
Sélectionnez
Procedure tFenetrePrincipale.WMTIMER (var Msg : tMessage);
(* Dessin de 10 lignes *)
Var DC : hDC;                 (* Contexte d'affichage *)
    ClientRect : tRect;       (* Taille de la zone client *)
    i : LongInt;              (* Indice dans le tableau *)
    AncienIndice : LongInt;   (* Sauvegarde de l'indice général *)
Begin
  DC := GetDC(hWindow);
  GetClientRect(hWindow,ClientRect);
  for i := 1 to 10 do
    begin
      AncienIndice := IndiceGeneral;
      if IndiceGeneral = IndiceMax - 1
         then
           begin   (* Le tour du tableau est fait *)
             IndiceGeneral := 0;
             Effacer := True;
           end
         else
           Inc(IndiceGeneral);
      if Effacer
         then   (* Effacement par dessin avec opération XOR *)
           DESSINLIGNE(DC,IndiceGeneral);
      TabLignes[IndiceGeneral] := TabLignes[AncienIndice];
      (* Création d'une nouvelle ligne *)
      with TabLignes[IndiceGeneral] do
        begin
          NOUVELLELIGNE(DC,x1,dx1,ClientRect.Right,Couleur);
          NOUVELLELIGNE(DC,y1,dy1,ClientRect.Bottom,Couleur);
          NOUVELLELIGNE(DC,x2,dx2,ClientRect.Right,Couleur);
          NOUVELLELIGNE(DC,y2,dy2,ClientRect.Bottom,Couleur);
        end;
      (* Dessin de la nouvelle ligne *)
      DESSINLIGNE(DC,IndiceGeneral);
    end;
  ReleaseDC(hWindow,DC);
End;

Vous voyez tout d'abord qu'un handle de contexte de périphérique est demandé à Windows. En effet, alors que la méthode Paint que nous avons vue fournit elle-même un handle de DC (PaintDC), ici nous devons le demander au système pour pouvoir dessiner à l'écran. La fonction GetDC ne nécessite que le handle de la fenêtre comme paramètre et retourne le handle du contexte de périphérique :

 
Sélectionnez
Function GetDC (Wnd : hWnd) : hDC;

Dans un souci de pureté, il conviendrait à ce niveau de tester la valeur de retour de GetDC et de mettre fin à l'application si elle est nulle :

 
Sélectionnez
  DC := GetDC(hWindow);
  if DC <> 0
     then
       begin
         ...
       end
     else
       begin
         MessageBox(hWindow,'Plus de ressources syst'#232'me','Erreur',mb_IconHand or mb_Ok);
         CloseWindow;
       end;

Cela signifierait que Windows n'a plus de contexte de périphérique disponible, ce qui est rare mais pas impossible. D'ailleurs, l'effondrement total du système serait proche...

La méthode CloseWindow, héritée de l'objet ancêtre tWindowsObject, détruit la fenêtre principale et met fin à l'application.

Il ne faut pas confondre la méthode CloseWindow avec la fonction de l'API du même nom qui, elle, sert à minimiser une fenêtre

Nous avons déjà insisté sur la nécessité de libérer un contexte de périphérique dès que possible, vu que Windows en dispose en nombre limité. Donc, à la fin de la méthode WMTIMER, nous restituons le DC à l'aide de la fonction ReleaseDC :

 
Sélectionnez
Function ReleaseDC (Wnd : hWnd; DC : hDC) : Integer;
  • Wnd est le handle de la fenêtre
  • DC est le handle du contexte de périphérique obtenu par GetDC

Rappelons encore une fois qu'avant d'être libéré, le DC soit être restauré dans son état initial. C'est bien ce que nous nous sommes attachés à faire dans notre méthode DESSINLIGNE.


Mais revenons à la manière dont fonctionne notre programme. Dans la boucle qui effectue les 10 itérations, vous pouvez voir que le champ Effacer, déclaré dans l'objet tFenetrePrincipale et initialisé à False dans le constructeur de la fenêtre, devient définitivement True dès que le tour des 100 éléments du tableau des lignes a été effectué au moins une fois. Cela entraîne que, à partir du moment où 100 lignes se trouvent à l'écran, il faudra en effacer une pour en dessiner une autre, de manière à ne jamais dépasser les 100 lignes simultanément affichées.

Or, justement, pour effacer une ligne vous voyez que l'on appelle la méthode DESSINLIGNE ?!?
Eh bien oui, cette méthode permet d'effacer une ligne en la redessinant. Regardons-la de plus près :

 
Sélectionnez
Procedure tFenetrePrincipale.DESSINLIGNE (DC : hDC; i : Integer);
(* Dessine ou efface la ligne d'indice i *)
Var AncienCrayon, Crayon : hPen;
    AncienneROP : LongInt;
Begin
  with TabLignes[i] do
    begin
      Crayon := CreatePen(ps_Solid,1,Couleur);
      AncienCrayon := SelectObject(DC,Crayon);
      AncienneROP := SetROP2(DC,r2_XORPen);
      MoveTo(DC,x1,y1);
      LineTo(DC,x2,y2);
      SelectObject(DC,AncienCrayon);
      DeleteObject(Crayon);
      SetROP2(DC,AncienneROP);
    end;
End;

C'est la fonction GDI SetROP2 qui permet indifféremment de dessiner ou effacer une ligne. ROP est l'abréviation de Raster OPeration. Raster, qui signifie trame, fait référence à la trame de pixels de l'écran.
SetROP2 permet d'appliquer une opération logique entre la couleur à appliquer à un pixel et la couleur actuelle de ce pixel à l'écran. En utilisant l'opération r2_XORPen, si les pixels de l'écran sont de la couleur du fond alors les pixels de la ligne prendront la couleur désirée et, inversément, si les pixels ont déjà la couleur de la ligne, ils reprendront la couleur du fond.

L'opération r2_XORPen est une manière très intéressante d'effacer un élément dessiné à l'écran

Voici un tableau de toutes les opérations logiques applicables par SetROP2 :

Constante Résultat à l'écran Opération logique
r2_NOP Aucune opération - pixels inchangés Ecran := Ecran
r2_Black Pixels noirs Ecran := 0
r2_White Pixels blancs Ecran := 1
r2_Not Inversion de la couleur des pixels Ecran := NOT Ecran
r2_CopyPen Application de la couleur du crayon Ecran := Crayon
r2_NotCopyPen Inverse de la couleur du crayon Ecran := NOT Crayon
r2_XORPen Couleur de l'écran XOR couleur du crayon Ecran := Ecran XOR Crayon
r2_NotXORPen Inverse du résultat de r2_XORPen Ecran := NOT(Ecran XOR Crayon)
r2_MaskPen Couleur commune au pinceau et à l'écran Ecran := Ecran AND Crayon
r2_NotMaskPen Inverse du résultat de r2_MaskPen Ecran := NOT(Ecran AND Crayon)
r2_MaskNotPen Couleur commune à l'écran et à l'inverse de la couleur du crayon Ecran := Ecran AND (NOT Crayon)
r2_MaskPenNot Couleur commune au crayon et à l'inverse de la couleur de l'écran Ecran := (NOT Ecran) AND Crayon
r2_MergePen Fusion des couleurs du pinceau et de l'écran Ecran := Ecran OR Crayon
r2_NotMergePen Inverse du résultat de r2_MergePen Ecran := NOT(Ecran OR Crayon)
r2_MergeNotPen Fusion de la couleur de l'écran et de l'inverse de la couleur du crayon Ecran := Ecran OR (NOT Crayon)
r2_MergePenNot Fusion de la couleur du crayon et de l'inverse de la couleur de l'écran Ecran := (NOT Ecran) OR Crayon


Un seul conseil : expérimentez ces différentes opérations !

Important : dans le but de restaurer le contexte de périphérique dans son état initial avant de le libérer, il faut absolument sauvegarder l'opération logique initiale au moment de l'appel de SetROP2. C'est le but de la variable locale AncienneROP.


Il nous reste encore à parler de la manière dont le programme détermine la taille de la fenêtre, afin de pouvoir donner l'impression que les lignes rebondissent sur ses bordures :

 
Sélectionnez
Procedure tFenetrePrincipale.WMTIMER (var Msg : tMessage);
(* Dessin de 10 lignes *)
Var DC : hDC;                 (* Contexte d'affichage *)
    ClientRect : tRect;       (* Taille de la zone client *)
    ...
Begin
  DC := GetDC(hWindow);
  GetClientRect(hWindow,ClientRect);
  ...

La fonction GetClientRect permet de connaître la taille de la zone client de la fenêtre, c'est-à-dire la zone intérieure comprise entre la barre de titre (s'il n'y a pas de menu) ou le menu et les bordures de la fenêtre. Le résultat de GetClientRect est retourné sous la forme d'une structure de type tRect :

 
Sélectionnez
Type tPoint = Record
                X : Long;
                Y : Long;
              end;
     tRect = Record
               case Integer of
                 0 : (Left, Top, Right, Bottom : Integer);
                 1 : (TopLeft, BottomRight : tPoint);
             end;
  • Left est l'abscisse du point extrême haut-gauche du rectangle
  • Top est son ordonnée
  • Right est l'abscisse du point extrême bas-droite du rectangle
  • Bottom est son ordonnée

Les champs Right et Bottom de la variable locale ClientRect sont passés comme paramètres à la méthode NOUVELLELIGNE, qui est chargée de créer les coordonnées et la couleur d'une nouvelle ligne.

VII-C-4. Amélioration du programme ONDOYANT pour imiter un screen-saver

Mine de rien, il ne faudrait pas grand chose pour que notre programme ressemble à un screen-saver ! Alors, nous allons lui ajouter quelques fonctionnalités :

  • Lui faire remplir tout l'écran
  • Arrêter son exécution dès qu'on touche au clavier ou à la souris

Bien qu'il s'agisse d'un programme Windows conventionnel, la structure d'un vrai screen-saver doit répondre à plusieurs spécifications bien précises qui sortent du cadre de ce tutoriel : sa vitesse et sa résolution doivent être paramétrables, il doit pouvoir être visible dans une petite fenêtre d'aperçu, etc.

Quoi qu'il en soit, après avoir modifié le programme ONDOYANT, nous pourrons le renommer en .SCR et l'utiliser comme screen-saver.

Tout d'abord, pour lui faire prendre tout l'écran, sans que soient visibles la barre de titre ni la moindre bordure, nous allons fixer nous-même la taille et la position de la fenêtre principale. Car, jusqu'à présent, nous avons laissé au système le soin de positionner et dimensionner toutes nos fenêtres.

Dans le constructeur Init de la fenêtre principale, nous allons donner des valeurs aux champs X, Y, W et H du champ Attr de l'objet descendant de tWindow. Rappelez-vous, nous avons déjà abordé le champ Attr dans le chapitre sur les ressources.
X et Y étant les coordonnées du point haut-gauche de la fenêtre, il est logique de leur donner une valeur nulle - puisque le 1er pixel tout en haut de l'écran a les coordonnées (0,0). Pour donner à la fenêtre la largeur (W) et la hauteur (H) de l'écran, il nous faut déterminer ces valeurs au moyen de la fonction GetSystemMetrics :

 
Sélectionnez
Function GetSystemMetrics (Index : Integer) : Integer;

Comme son nom l'indique, cette fonction retourne des mesures d'éléments du système. Elle attend un index et retourne la mesure qui correspond à cet index. Pour connaître la largeur et la hauteur de l'écran, il faut utiliser respectivement comme index sm_CXScreen et sm_CYScreen. Vous pouvez consulter le SDK pour voir tous les index possibles; il y en a plusieurs dizaines et vous constaterez que GetSystemMetrics permet d'obtenir des renseignements aussi divers que le type de boot ou le nombre de boutons de la souris.

Modifions le constructeur de la fenêtre principale :

 
Sélectionnez
Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
(* Style & taille - Pinceau pour le fond - Initialisation des champs *)
Var h : hBitmap;
    i : LongInt;
Begin
  tWindow.INIT(aParent,aTitle);
  (* Taille de la fenêtre *)
  Attr.X := 0;
  Attr.Y := 0;
  Attr.W := GetSystemMetrics(sm_CXScreen);
  Attr.H := GetSystemMetrics(sm_CYScreen);
  (* Création du pinceau pour le fond *)
  ...
End;

Recompilez le source ainsi modifié et exécutez-le : la fenêtre prend bien toute la largeur et toute la hauteur de l'écran mais la barre de titre et les bordures sont encore visibles. Donc, ce n'est pas encore terminé : la dernière modification à apporter à la fenêtre est de lui choisir un style particulier.

Si vous êtes un utilisateur habituel de Windows, vous n'avez certainement pas manqué de remarquer que toutes les fenêtres de tous les programmes ne sont pas absolument identiques : certaines ont une barre de titre et d'autres pas, certaines ont une bordure épaisse et d'autres une bordure fine ou même pas de bordure du tout, certaines peuvent être déplacées et dimensionnées par l'utilisateur alors que d'autres pas, etc, etc. Toutes ces différences sont dues à l'utilisation d'attributs de style différents.

Mis à part certains attributs s'excluant mutuellement, les attributs de style peuvent être combinés au moyen d'une opération logique or.

Dans le cas qui nous occupe, la combinaison des attributs de style ws_Popup (pour une fenêtre simple sans barre de titre ni bordure) et ws_Visible (pour une fenêtre visible, vous aviez deviné) correspond exactement à notre attente.
Modifions donc une dernière fois le constructeur de la fenêtre principale :

 
Sélectionnez
Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
(* Style & taille - Pinceau pour le fond - Initialisation des champs *)
Var h : hBitmap;
    i : LongInt;
Begin
  tWindow.INIT(aParent,aTitle);
  (* Style et taille de la fenêtre *)
  Attr.Style := ws_Popup or ws_Visible;
  Attr.X := 0;
  Attr.Y := 0;
  Attr.W := GetSystemMetrics(sm_CXScreen);
  Attr.H := GetSystemMetrics(sm_CYScreen);
  (* Création du pinceau pour le fond *)
  ...
End;

Si vous compilez et exécutez le programme, vous voyez que c'est gagné : il prend l'entièreté de l'écran !

La dernière étape à effectuer est de faire en sorte que le programme s'arrête dès qu'une touche est pressée ou dès que l'on touche à la souris, comme un vrai screen-saver. Avec ce que nous avons déjà vu, vous devriez déjà être capable de deviner comment réaliser cela : tout simplement en créant des méthodes virtuelles indexées qui vont réagir aux messages du clavier et de la souris. Pour fermer le programme, nous appellerons la méthode CloseWindow :

 
Sélectionnez
Procedure tFenetrePrincipale.WMKEYDOWN (var Msg : tMessage);
(* Fermeture de la fenêtre quand touche pressée *)
Begin
  CloseWindow;
End;

Procedure tFenetrePrincipale.WMMOUSEMOVE (var Msg : tMessage);
(* Fermeture de la fenêtre quand mouvement de souris *)
Begin
  CloseWindow;
End;

Procedure tFenetrePrincipale.WMLBUTTONDOWN (var Msg : tMessage);
(* Fermeture de la fenêtre quand clic de souris *)
Begin
  CloseWindow;
End;
VII-C-4-a. Attributs de style de fenêtre

Le sujet est suffisamment important pour que nous prenions le temps de détailler tous les attributs de styles de fenêtres. Notez que le nom de tous les attributs commence par le mnémonique ws_, qui correspond à Window Style.

Attribut Explication
ws_Popup Fenêtre simple sans barre de titre ni bordure
ws_Child Fenêtre enfant, ne convient pas pour créer une fenêtre principale
ws_Overlapped / ws_Tiled Fenêtre avec barre de titre et bordures
ws_DlgFrame Fenêtre sans barre de titre et avec les mêmes bordures que les boîtes de dialogue
ws_Visible Fenêtre visible dès sa création
ws_Border Fenêtre avec bordures fines
ws_Caption Fenêtre avec barre de titre et bordures fines
ws_SysMenu Fenêtre avec menu système (doit être combiné avec ws_Caption)
ws_MinimizeBox Fenêtre avec bouton de réduction (doit être combiné avec ws_SysMenu)
ws_MaximizeBox Fenêtre avec bouton d'agrandissement (doit être combiné avec ws_SysMenu)
ws_SizeBox / ws_ThickFrame Fenêtre avec bouton de redimensionnement (doit être combiné avec ws_SysMenu)
ws_ClipChildren Le dessin du contenu de la fenêtre ne s'effectue que dans les zones non recouvertes par les fenêtres enfants
ws_ClipSiblings Assure que le dessin dans une fenêtre enfant de la fenêtre n'affectera pas le contenu des autres fenêtres enfants
ws_Disabled Fenêtre inactive (ne répond à aucune action de l'utilisateur)
ws_Group Premier élément d'un groupe de contrôles
ws_TabStop Contrôle accessible par la touche TAB
ws_HScroll Fenêtre avec barre de défilement horizontale
ws_VScroll Fenêtre avec barre de défilement verticale
ws_Iconic / ws_Minimize Fenêtre minimisée
ws_Maximize Fenêtre agrandie
ws_PopupWindow Fenêtre avec les attributs ws_Popup, ws_Border et ws_SysMenu
ws_ChildWindow Fenêtre avec l'attribut ws_Child
ws_OverlappedWindow / ws_TiledWindow Fenêtre avec les attributs ws_Overlapped, ws_Caption, ws_SysMenu, ws_ThickFrame, ws_MinimizeBox et ws_MaximizedBox

VII-D. Dessin de formes par cliquer-glisser de souris : le programme FORMES.PAS

Poursuivons notre visite du domaine très étendu de l'interface GDI. Nous allons à présent réaliser un programme qui permet, un peu comme l'utilitaire Paint de Windows, de dessiner des formes géométriques (lignes, rectangles, ellipses) :

Image non disponible

Dans notre programme, les figures géométriques seront dessinées à la souris : la position de la figure sera fixée par un clic du bouton gauche de la souris et la taille ainsi que la forme seront déterminées par le mouvement de la souris jusqu'au relâchement du bouton gauche.

Voici le source du programme :

 
Sélectionnez
Program FORMES;

(* Dessin de formes géométriques par cliquer-glisser de souris.

   Réalisé par Alcatîz pour Developpez.com - 17-09-2006 *)


{$R FORMES.RES}


Uses Windows,    (* API Win32 *)
     OWindows;   (* Objets OWL *)


{$I FORMES.INC}


Type pForme = ^tForme;
     tForme = Record
                x1, y1, x2, y2 : LongInt;   (* Coordonnées *)
                Forme : LongInt;            (* Type de forme *)
                pSuiv : pForme;             (* Adresse forme suivante *)
              end;

     pFenetrePrincipale = ^tFenetrePrincipale;
     tFenetrePrincipale = Object(tWindow)
                            BoutonEnfonce : LongBool;
                            FormeCourante : LongInt;
                            x1, y1, x2, y2 : LongInt;
                            pTeteFormes, pQueueFormes : pForme;
                            Constructor INIT (aParent : pWindowsObject; aTitle : pChar);
                               (* Chargement menu - Initialisation champs - Cochage menu *)
                            Function GETCLASSNAME : pChar;
                               virtual;
                               (* Retourne le nom de classe de la fenêtre *)
                            Procedure GETWINDOWCLASS (var aWndClass : tWndClass);
                               virtual;
                               (* Définition du curseur de classe de la fenêtre *)
                            Procedure DESSIN_FORME (DC : hDC);
                               (* Dessin d'une forme aux coordonnées courantes *)
                            Procedure PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
                               virtual;
                               (* Dessin de toutes les formes de la liste *)
                            Procedure DESTRUCTION_LISTE;
                               (* Destruction de la liste chaînée des formes *)
                            Procedure WMCOMMAND (var Msg : tMessage);
                               virtual wm_First + wm_Command;
                               (* Réponse aux messages de commandes *)
                            Procedure WMLBUTTONDOWN (var Msg : tMessage);
                               virtual wm_First + wm_LButtonDown;
                               (* Bouton gauche : début du dessin *)
                            Procedure WMLBUTTONUP (var Msg : tMessage);
                               virtual wm_First + wm_LButtonUp;
                               (* Relâchement bouton : fin du dessin et ajout à la liste *)
                            Procedure WMMOUSEMOVE (var Msg : tMessage);
                               virtual wm_First + wm_MouseMove;
                               (* Mouvement de souris : dessin intermédiaire *)
                            Destructor DONE;
                               virtual;
                               (* Destruction liste chaînée des formes en fin de programme *)
                          end;

     tProgramme = Object(tApplication)
                   Procedure INITINSTANCE;
                      virtual;
                      (* Chargement de la table d'accélérateurs *)
                    Procedure INITMAINWINDOW;
                       virtual;
                       (* Allocation de la fenêtre principale du programme *)
                  end;


(* ----- Méthodes de l'objet tFenetrePrincipale ----- *)

Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
(* Chargement du menu - Initialisation des champs - Cochage du menu *)
Begin
  tWindow.INIT(aParent,aTitle);
  (* Chargement du menu *)
  Attr.Menu := LoadMenu(hInstance,pChar(id_MenuPrincipal));
  (* Initialisation des champs *)
  BoutonEnfonce := False;
  FormeCourante := cm_Rectangle;
  pTeteFormes := Nil;
  pQueueFormes := Nil;
  (* Cochage de la commande de menu de dessin de rectangle *)
  CheckMenuItem(Attr.Menu,cm_Rectangle,mf_ByCommand or mf_Checked);
End;

Function tFenetrePrincipale.GETCLASSNAME : pChar;
(* Retourne le nom de classe de la fenêtre *)
Begin
  GETCLASSNAME := 'Formes';
End;

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Définition du curseur de classe de la fenêtre *)
Begin
  tWindow.GETWINDOWCLASS(aWndClass);
  aWndClass.hCursor := LoadCursor(0,idc_Cross);
End;

Procedure tFenetrePrincipale.DESSIN_FORME (DC : hDC);
(* Dessin d'une forme aux coordonnées courantes *)
Begin
  case FormeCourante of
    cm_Rectangle : Rectangle(DC,x1,y1,x2,y2);
    cm_Ellipse : Ellipse(DC,x1,y1,x2,y2);
    cm_Ligne : begin
                 MoveToEx(DC,x1,y1,Nil);
                 LineTo(DC,x2,y2);
               end;
  end;
End;

Procedure tFenetrePrincipale.PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
(* Dessin de toutes les formes de la liste *)
Var x1Sauve, y1Sauve, x2Sauve, y2Sauve, FormeSauve : Integer;
    p : pForme;
    AncienPinceau : hBrush;
    AncienneROP : LongInt;
Begin
  (* Sauvegarde des champs *)
  x1Sauve := x1; 
  y1Sauve := y1; 
  x2Sauve := x2; 
  y2Sauve := y2; 
  FormeSauve := FormeCourante;
  (* Sélection d'un pinceau invisible, choix ROP *)
  AncienPinceau := SelectObject(PaintDC,GetStockObject(Hollow_Brush));
  AncienneROP := SetROP2(PaintDC,r2_Not);
  (* Dessin de toutes les formes de la liste *)
  p := pTeteFormes;
  while p <> Nil do
    begin
      x1 := p^.x1; 
      y1 := p^.y1; 
      x2 := p^.x2; 
      y2 := p^.y2; 
      FormeCourante := p^.Forme;
      DESSIN_FORME(PaintDC);
      p := p^.pSuiv;
    end;
  (* Restauration du DC dans son état d'origine *)
  SetROP2(PaintDC,AncienneROP);
  SelectObject(PaintDC,AncienPinceau);  
  (* Restauration des champs *)  
  x1 := x1Sauve; 
  y1 := y1Sauve; 
  x2 := x2Sauve; 
  y2 := y2Sauve; 
  FormeCourante := FormeSauve;
End;

Procedure tFenetrePrincipale.DESTRUCTION_LISTE;
(* Destruction de la liste chaînée des formes *)
Var pSuivant : pForme;
Begin
  pQueueFormes := pTeteFormes;
  while pQueueFormes <> Nil do
    begin
      pSuivant := pQueueFormes^.pSuiv;
      Dispose(pQueueFormes);
      pQueueFormes := pSuivant;
    end;
  pTeteFormes := Nil;
  pQueueFormes := Nil;
End;

Procedure tFenetrePrincipale.WMCOMMAND (var Msg : tMessage);
(* Réponse aux messages de commandes *)
Begin
  case LoWord(Msg.wParam) of
    cm_Rectangle..cm_Ligne : if not BoutonEnfonce
                                then   (* Changement de forme *)
                                  begin
                                    CheckMenuItem(Attr.Menu,FormeCourante,mf_ByCommand or mf_Unchecked);
                                    FormeCourante := LoWord(Msg.wParam);
                                    CheckMenuItem(Attr.Menu,FormeCourante,mf_ByCommand or mf_Checked);
                                  end;
    cm_Supprimer : if not BoutonEnfonce
                      then   (* Suppression de la liste des formes *)
                        begin
                          InvalidateRect(hWindow,Nil,True);
                          DESTRUCTION_LISTE;
                        end;
  else
    tWindow.WMCOMMAND(Msg);
  end;
End;

Procedure tFenetrePrincipale.WMLBUTTONDOWN (var Msg : tMessage);
(* Clic gauche : début du dessin *)
Begin
  x1 := LoWord(Msg.lParam);
  y1 := HiWord(Msg.lParam);
  x2 := x1;
  y2 := y1;
  (* Capture de la souris jusqu'à la fin du dessin *)
  SetCapture(hWindow);
  (* Indicateur de bouton de souris enfoncé *)
  BoutonEnfonce := True;
End;

Procedure tFenetrePrincipale.WMMOUSEMOVE (var Msg : tMessage);
(* Mouvement de souris : dessin intermédiaire *)
Var DC : hDC;
    AncienPinceau : hBrush;
    AncienneROP : LongInt;
Begin
  if BoutonEnfonce
     then
       begin
         (* Obtention du DC, sélection pinceau invisible, choix ROP *)
         DC := GetDC(hWindow);
         AncienPinceau := SelectObject(DC,GetStockObject(Hollow_Brush));
         AncienneROP := SetROP2(DC,r2_Not);
         (* Effacement de la forme actuelle *)
         DESSIN_FORME(DC);
         (* Nouveau dessin *)
         x2 := LoWord(Msg.lParam);
         y2 := HiWord(Msg.lParam);
         DESSIN_FORME(DC);
         (* Libération du DC *)
         SetROP2(DC,AncienneROP);
         SelectObject(DC,AncienPinceau);
         ReleaseDC(hWindow,DC);
       end;
End;

Procedure tFenetrePrincipale.WMLBUTTONUP (var Msg : tMessage);
(* Relâchement bouton gauche : fin du dessin et ajout à la liste *)
Var DC : hDC;
    AncienPinceau : hBrush;
    AncienneROP : LongInt;
    p : pForme;
Begin
  if BoutonEnfonce
     then
       begin
         (* Obtention du DC, sélection pinceau invisible, choix ROP *)
         DC := GetDC(hWindow);
         AncienPinceau := SelectObject(DC,GetStockObject(Hollow_Brush));
         AncienneROP := SetROP2(DC,r2_Not);
         (* Effacement de la forme actuelle *)
         DESSIN_FORME(DC);
         (* Dessin de la forme définitive *)         
         DESSIN_FORME(DC);
         (* Libération du DC *)
         SetROP2(DC,AncienneROP);
         SelectObject(DC,AncienPinceau);
         ReleaseDC(hWindow,DC);
         (* Libération de la souris capturée *)
         ReleaseCapture;
         (* Mise à jour indicateur bouton de souris enfoncé *)
         BoutonEnfonce := False;
         (* Ajout de la forme dans la liste chaînée *)
         New(p);
         if p <> Nil
            then
              begin
                p^.x1 := x1; 
                p^.y1 := y1; 
                p^.x2 := x2; 
                p^.y2 := y2;
                p^.Forme := FormeCourante;
                p^.pSuiv := Nil;
                if pTeteFormes = Nil 
                   then 
                     pTeteFormes := p 
                   else 
                     pQueueFormes^.pSuiv := p;
                pQueueFormes := p;
              end;
       end;
End;

Destructor tFenetrePrincipale.DONE;
(* Destruction de la liste chaînée des formes en fin de programme *)
Begin
  DESTRUCTION_LISTE;
  tWindow.DONE;
End;


(* ----- Méthodes de l'objet tProgramme ----- *)

Procedure tProgramme.INITINSTANCE;
(* Chargement de la table d'accélérateurs *)
Begin
  tApplication.INITINSTANCE;
  hAccTable := LoadAccelerators(hInstance,pChar(id_Accel));
End;

Procedure tProgramme.INITMAINWINDOW;
(* Allocation de la fenêtre principale du programme *)
Begin
  MainWindow := New(pFenetrePrincipale,INIT(Nil,'Dessin de formes'));
End;


(* ----- Variables globales ----- *)

Var Programme : tProgramme;


(* ----- Programme principal ----- *)

Begin
  Programme.INIT('Formes');
  Programme.RUN;
  Programme.DONE;
End.

VII-D-1. Ressources

Comme vous le voyez sur le screenshot ci-dessus, notre programme contient un menu et une table d'accélérateurs (dont les combinaisons de touches apparaissent dans le menu). Il n'y a aucune difficulté particulière dans la création de ces deux ressources - ne vous préoccupez pas pour l'instant de la marque de cochage de la commande Rectangle. Vous pouvez bien sûr toujours vous référer au chapitre consacré à la création de ressources.

Voici le source du fichier FORMES.RC :

 
Sélectionnez
#include "formes.inc"

id_MenuPrincipal MENU 
{
 POPUP "&Commandes"
 {
  MENUITEM "&Rectangle\tAlt-R", cm_Rectangle
  MENUITEM "&Ellipse\tAlt-E", cm_Ellipse
  MENUITEM "&Ligne\tAlt-L", cm_Ligne
  MENUITEM SEPARATOR
  MENUITEM "&Supprimer tout\tSuppr", cm_Supprimer
  MENUITEM SEPARATOR
  MENUITEM "&Quitter", cm_Exit
 }
}
id_Accel ACCELERATORS 
{
 "r", cm_Rectangle, ASCII, ALT
 "e", cm_Ellipse, ASCII, ALT
 "l", cm_Ligne, ASCII, ALT
 VK_DELETE, cm_Supprimer, VIRTKEY
}

Contenu du fichier FORMES.INC :

 
Sélectionnez
const
        cm_Rectangle     = 101;
        cm_Ellipse       = 102;
        cm_Ligne         = 103;
        cm_Supprimer     = 104;
        cm_Exit          = 24340;
        id_MenuPrincipal = 1;
        id_Accel         = 2;

VII-D-2. Curseur du stock de Windows

Afin d'indiquer à l'utilisateur que le programme attend un clic de souris pour dessiner, nous allons remplacer le curseur par défaut (la bonne vieille flèche) par un curseur plus adapté, une croix (+). En plus, il sera plus facile avec ce curseur de positionner la souris avec précision à l'écran.

Dans le chapitre consacré aux ressources, nous avons vu comment créer un curseur personnalisé. Mais ici nous n'en avons pas besoin car Windows possède déjà en stock le curseur qu'il nous faut !

 
Sélectionnez
Function tFenetrePrincipale.GETCLASSNAME : pChar;
(* Retourne le nom de classe de la fenêtre *)
Begin
  GETCLASSNAME := 'Formes';
End;

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Définition du curseur de classe de la fenêtre *)
Begin
  tWindow.GETWINDOWCLASS(aWndClass);
  aWndClass.hCursor := LoadCursor(0,idc_Cross);
End;

Dans une fonction de chargement de ressource (LoadIcon, LoadCursor...), l'utilisation de 0 comme handle d'instance donne accès aux objets du stock de Windows

En utilisant 0 comme handle d'instance (au lieu de la variable hInstance, que nous avons utilisée jusqu'à présent), nous pouvons donc charger un curseur du stock de Windows. L'identificateur de la croix que nous allons utiliser est idc_Cross.

Voici les curseurs disponibles dans le stock de Windows :

Identificateur Curseur
idc_Arrow Image non disponible
idc_Wait Image non disponible
idc_AppStarting Image non disponible
idc_Cross Image non disponible
idc_IBeam Image non disponible
idc_No Image non disponible
idc_UpArrow Image non disponible
idc_SizeWE Image non disponible
idc_SizeNS Image non disponible
idc_SizeNWSE Image non disponible
idc_SizeNESW Image non disponible
idc_Size / idc_SizeAll Image non disponible

VII-D-3. Cochage du menu

Notre programme permettra à l'utilisateur de choisir trois types de formes géométriques : rectangle, ellipse ou ligne droite. Au démarrage du programme, la forme sélectionnée sera le rectangle et donc la commande de menu correspondante sera cochée. Le type de forme en cours sera stocké dans un champ de la fenêtre principale, FormeCourante. Et quoi de plus simple que d'utiliser directement l'identificateur de commande ? Dans le constructeur de la fenêtre, le champ FormeCourante recevra donc la commande cm_Rectangle.

Le cochage d'une commande de menu se fait à l'aide de la fonction CheckMenuItem :

 
Sélectionnez
Function CheckMenuItem (Menu : hMenu; IDCheckItem, Check : UInt) : DWord;
  • Menu est le handle du menu
  • IdCheckItem permet d'identifier la commande à cocher
  • Check est l'option de cochage à appliquer

Lorsque nous chargeons une ressource de type menu, dans le constructeur de notre fenêtre principale, nous avons vu que le handle retourné par la fonction LoadMenu est stocké dans le champ Attr.Menu de l'objet descendant de tWindow. C'est donc ce handle qui sera passé comme paramètre Menu de la fonction CheckMenuItem.

Les deux autres paramètres sont en quelque sorte liés : la valeur de IdCheckItem dépendra de celle de Check. Je m'explique : Check est une combinaison logique (or) entre l'option de cochage à appliquer (mf_Checked pour cocher la commande ou mf_Unchecked pour la décocher) et un paramètre permettant de déterminer si IdCheckItem identifie la commande du menu à (dé)cocher ou bien représente la position de cette commande dans le menu. Ainsi, si Check est une combinaison or de mf_Checked et mf_ByCommand, cela signifie que le paramètre IdCheckItem contient l'identificateur de la commande qu'il faut cocher tandis que, si Check est la combinaison de mf_Checked et mf_ByPosition, cela signifie que IdCheckItem contient le n° d'ordre de la commande à cocher dans le menu.

Dans le présent programme, nous utilisons mf_ByCommand :

 
Sélectionnez
CheckMenuItem(Attr.Menu,cm_Rectangle,mf_ByCommand or mf_Checked);

Dans le corps du programme, chaque fois que l'utilisateur changera de type de forme géométrique à dessiner, nous devrons décocher la commande courante et cocher la nouvelle commande. Cette action aura lieu en réponse à une des commandes de choix de figure.

Nous utiliserons une méthode WMCOMMAND pour répondre aux commandes, comme dans le programme CHGFOND créé dans le chapitre précédent :

 
Sélectionnez
Procedure tFenetrePrincipale.WMCOMMAND (var Msg : tMessage);
(* Réponse aux messages de commandes *)
Begin
  case LoWord(Msg.wParam) of
    cm_Rectangle..cm_Ligne : if not BoutonEnfonce
                                then   (* Changement de forme *)
                                  begin
                                    CheckMenuItem(Attr.Menu,FormeCourante,mf_ByCommand or mf_Unchecked);
                                    FormeCourante := LoWord(Msg.wParam);
                                    CheckMenuItem(Attr.Menu,FormeCourante,mf_ByCommand or mf_Checked);
                                  end;
  else
    tWindow.WMCOMMAND(Msg);
  end;
End;

Rappelons que nous avons décidé de stocker la forme courante dans le champ FormeCourante de la fenêtre principale. Nous décochons donc d'abord la commande en passant ce champ comme paramètre IdCheckItem de la fonction CheckMenuItem, avec comme paramètre Check la combinaison mf_ByCommand or mf_Unchecked; ensuite, la nouvelle commande, qui se trouve dans le mot de poids faible du champ wParam du message de commande, est copiée dans le champ FormeCourante; enfin, nous cochons la nouvelle commande.

VII-D-4. Dessin d'une forme

Penchons-nous tout d'abord sur la méthode qui sera chargée de dessiner une forme :

 
Sélectionnez
Procedure tFenetrePrincipale.DESSIN_FORME (DC : hDC);
(* Dessin d'une forme aux coordonnées courantes *)
Begin
  case FormeCourante of
    cm_Rectangle : Rectangle(DC,x1,y1,x2,y2);
    cm_Ellipse : Ellipse(DC,x1,y1,x2,y2);
    cm_Ligne : begin
                 MoveToEx(DC,x1,y1,Nil);
                 LineTo(DC,x2,y2);
               end;
  end;
End;

Très simple, cette procédure aura besoin des éléments suivants :

  • Un contexte de périphérique
  • Le type de forme à dessiner
  • Les coordonnées de la forme

Le contexte de périphérique, DC, sera fourni par la procédure appelante. Le type de forme à dessiner est déjà stocké dans le champ FormeCourante de la fenêtre principale. Par contre, les coordonnées de début (x1,y1) et de fin (x2,y2), nous n'en avons pas encore parlé : la manière la plus simple sera de les déclarer eux aussi comme champs de l'objet fenêtre principale.

Parmi les fonctions GDI de dessin utilisées, nous avons déjà abordé Ellipse plus haut dans ce chapitre. La fonction Rectangle, dont je ne vous fais pas l'affront de vous expliquer à quoi elle sert, est calquée sur Ellipse : ses paramètres sont rigoureusement identiques.

Pour dessiner une ligne, nous utilisons le couple de fonctions MoveToEx et LineTo :

 
Sélectionnez
Function MoveToEx (DC : hDC; X, Y : Integer; OldPos : pPoint) : Bool;
Function LineTo (DC : HDC; X, Y : Integer) : Bool;

Pour ceux qui ont travaillé avec l'unité Graph de Turbo Pascal, l'analogie est évidente avec les procédures MoveTo et LineTo.

MoveToEx fixe les coordonnées du point d'origine de la forme. LineTo dessine tout simplement une ligne à partir de ce point d'origine jusqu'aux coordonnées qui lui sont passées comme paramètres. Comme toutes les fonctions de dessin GDI, ces deux fonctions nécessitent comme paramètre un handle de contexte de périphérique (DC).

MoveToEx peut retourner les coordonnées de l'ancien point d'origine dans une structure de type tPoint; comme nous n'en avons pas besoin dans notre programme, nous transmettons Nil comme adresse.
Le type tPoint contient tout simplement un couple abscisse/ordonnée :

 
Sélectionnez
Type pPoint = ^tPoint;
     tPoint = Record
                X : Long;
                Y : Long;
              end;

Les fonctions GDI qui dessinent une forme incluent le point d'origine du dessin mais excluent le point de destination

Cette remarque n'est pas sans importance. Par exemple, si l'on utilise la fonction Rectangle :

 
Sélectionnez
Rectangle(DC,10,10,100,100);

Le rectangle dessiné à l'écran aura comme point d'origine (10,10) mais aura en réalité comme point de destination (99,99) !
Il y a une raison assez logique à cela : si l'on doit dessiner une série de formes successives, le point de destination d'une forme devient automatiquement le point d'origine de la forme suivante. Les formes ne se chevauchent donc pas.

Dans le cas de la fonction LineTo, le point de destination devient automatiquement le nouveau point d'origine pour le dessin suivant. Par exemple :

 
Sélectionnez
MoveToEx(DC,10,10);
LineTo(DC,100,100);
LineTo(DC,200,50);

Avant le premier LineTo, le point d'origine est fixé à (10,10) par la fonction MoveToEx. Le LineTo dessine une ligne de (10,10) à (99,99) et le point d'origine devient automatiquement (100,100) sans qu'il y ait besoin de réexécuter un MoveToEx. Le second LineTo, lui, dessine une ligne de (100,100) à (199,51) et le point d'origine devient (200,50)... et ainsi de suite.

VII-D-5. Clic gauche : début du dessin et capture de la souris

Le dessin d'une forme débutera lorsque l'utilisateur aura cliqué avec le bouton gauche de la souris dans la zone client de la fenêtre. Cet événement sera géré dans une méthode virtuelle indexée WMLBUTTONDOWN :

 
Sélectionnez
Procedure tFenetrePrincipale.WMLBUTTONDOWN (var Msg : tMessage);
(* Clic gauche : début du dessin *)
Begin
  x1 := LoWord(Msg.lParam);
  y1 := HiWord(Msg.lParam);
  x2 := x1;
  y2 := y1;
  (* Capture de la souris jusqu'à la fin du dessin *)
  SetCapture(hWindow);
  (* Indicateur de bouton de souris enfoncé *)
  BoutonEnfonce := True;
End;

Que fait cette méthode ? Tout d'abord, elle stocke les coordonnées du début de la forme dans les champs x1 et y1 de l'objet fenêtre principale. Comme la forme a une taille nulle pour l'instant, les champs x2 et y2 seront identiques à x1 et y1.

Ensuite, elle capture la souris.

Capturer la souris entraîne que tous les messages de la souris arrivent à la fenêtre qui l'a capturée, même si la souris a quitté sa zone client

Pourquoi faire cela ? Tout simplement parce que si, une fois que le dessin d'une forme a commencé, la souris quitte la zone client de la fenêtre, nous ne saurons pas si le bouton gauche a été relâché. En nous assurant que tous les messages de la souris nous parviendront, nous sommes certains de pouvoir détecter quand le bouton gauche est relâché et donc quand le dessin de la forme en cours est terminé.

Le fait de capturer la souris entraîne que celle-ci est inutilisable en dehors de la fenêtre de notre application. Nous n'allons donc pas la capturer en permanence mais plutôt limiter sa capture à la durée du dessin d'une forme :

  • Capture de la souris au début du dessin (clic gauche de la souris)
  • Fin de la capture à la fin du dessin (quand le bouton est relâché)

Les deux fonctions qui permettent de capturer et libérer la souris sont très simples :

 
Sélectionnez
Function SetCapture (Wnd : hWnd) : hWnd;
Function ReleaseCapture : Bool;

SetCapture ne nécessite comme paramètre que le handle de la fenêtre capturante, qui recevra donc tous les messages de la souris. ReleaseCapture, qui libère la souris, n'a besoin d'aucun paramètre.

Pour en finir avec le début du dessin d'une forme, nous devons parler d'un dernier champ : BoutonEnfonce. Lorsque la souris se trouve dans la zone client de la fenêtre de notre programme, celle-ci reçoit des messages wm_MouseMove chaque fois que la souris est déplacée. Or, une fois que le dessin a débuté, quand le bouton gauche de la souris a été enfoncé, à chaque mouvement de la souris la forme est redessinée, et cela jusqu'à ce que le bouton gauche soit relâché.
Mais que se passe-t-il si la souris se déplace dans la zone client de la fenêtre sans qu'un dessin soit en cours ? Eh bien, si nous ne testons pas que le bouton gauche est enfoncé, nous risquons de dessiner des formes quand il ne le faut pas. Le champ BoutonEnfonce, de type booléen, est donc mis à True au début du dessin et à False lorsque le dessin est terminé.

VII-D-6. Mouvements de souris : dessins intermédiaires

Une fois que le dessin a débuté, l'utilisateur n'aura qu'à déplacer la souris pour modifier la taille et la forme de la figure géométrique. Il faut qu'il voie la figure changer à l'écran : notre programme va donc la redessiner en permanence jusqu'à ce que l'utilisateur la fixe une fois pour toutes en relâchant le bouton de la souris.

Tous les dessins intermédiaires se feront dans une méthode WMMOUSEMOVE, en réponse à chaque message de mouvement de la souris wm_MouseMove :

 
Sélectionnez
Procedure tFenetrePrincipale.WMMOUSEMOVE (var Msg : tMessage);
(* Mouvement de souris : dessin intermédiaire *)
Var DC : hDC;
    AncienPinceau : hBrush;
    AncienneROP : LongInt;
Begin
  if BoutonEnfonce
     then
       begin
         (* Obtention du DC, sélection pinceau invisible, choix ROP *)
         DC := GetDC(hWindow);
         AncienPinceau := SelectObject(DC,GetStockObject(Hollow_Brush));
         AncienneROP := SetROP2(DC,r2_Not);
         (* Effacement de la forme actuelle *)
         DESSIN_FORME(DC);
         (* Nouveau dessin *)
         x2 := LoWord(Msg.lParam);
         y2 := HiWord(Msg.lParam);
         DESSIN_FORME(DC);
         (* Libération du DC *)
         SetROP2(DC,AncienneROP);
         SelectObject(DC,AncienPinceau);
         ReleaseDC(hWindow,DC);
       end;
End;
VII-D-6-a. Pinceau invisible - Objets du stock de Windows

Nous le savons déjà, les fonctions GDI utilisent le crayon et le pinceau sélectionnés dans le contexte de périphérique pour dessiner respectivement les contours et l'intérieur des formes. Dans notre programme, nous ne voulons voir que les contours; pour ne pas dessiner l'intérieur des formes, nous allons sélectionner un pinceau invisible ! Or, dans son stock, Windows possède le pinceau qu'il nous faut.

Pour aller chercher un objet du stock, on utilise GetStockObject :

 
Sélectionnez
Function GetStockObject (Index : Integer) : hGDIObj;

Cette fonction ne demande qu'un index et retourne le handle de l'objet demandé.

Voici les index des principaux objets disponibles dans le stock de Windows :

Index Objet
Black_Brush Pinceau noir
DkGray_Brush Pinceau gris foncé
Gray_Brush Pinceau gris moyen
LtGray_Brush Pinceau gris clair
White_Brush Pinceau blanc
Hollow_Brush / Null_Brush Pinceau invisible
Black_Pen Crayon noir
White_Pen Crayon blanc
Null_Pen Crayon invisible
System_Font Police de caractères par défaut (style Windows 3.x)
System_Fixed_Font Police de caractères à largeur fixe par défaut (obsolète)
Ansi_Var_Font Police de caractères système
Ansi_Fixed_Font Police de caractères système à largeur fixe



Oubliez pour l'instant les polices de caractères, nous les aborderons plus tard.

VII-D-6-b. Coordonnées de la souris

Le point d'origine de la forme à dessiner a été fixé au moment où l'utilisateur a pressé le bouton gauche de la souris. Ses coordonnées sont stockées dans les champs x1 et y1 de l'objet fenêtre principale. Le point extrême, par contre, correspond aux coordonnées actuelles de la souris. Celles-ci sont transmises dans le champ lParam du message wm_MouseMove : le mot de poids faible (LoWord) est l'abscisse et le mot de poids fort (HiWord) est l'ordonnée. Les coordonnées de la souris seront stockées dans les champs x2 et y2 de l'objet fenêtre principale.

VII-D-6-c. Effacement et dessin

Dans notre programme, nous ne définissons aucune couleur pour les formes à dessiner : nous nous contentons d'inverser la couleur du fond de la fenêtre. Pour ce faire, nous utilisons la fonction SetROP2 que nous avons étudiée dans le programme ONDOYANT.PAS et nous utilisons l'opération logique r2_Not.

Voici donc une idée d'exercice pour vous : modifier le programme en ajoutant des couleurs pour les formes !

A chaque fois qu'un message wm_MouseMove sera reçu par la fenêtre principale, donc à chaque mouvement de souris, nous devrons effacer le dernier dessin intermédiaire avant d'en dessiner un nouveau avec les nouvelles coordonnées de la souris.

Tout comme dans le programme ONDOYANT.PAS, nous utilisons la fonction SetROP2 pour effacer une forme en la redessinant. C'est très facile puisque les formes sont dessinées en inversant la couleur du fond : en la réinversant, on retrouve la couleur du fond.

VII-D-7. Relâchement du bouton gauche : fin du dessin et libération de la souris

La forme en cours est définitivement fixée lorsque l'utilisateur relâche le bouton gauche de la souris. A ce moment, le dernier dessin intermédiaire est effacé, le dessin définitif est effectué et la souris capturée est libérée.

Le message que reçoit la fenêtre lorsque le bouton gauche de la souris est relâché est wm_LButtonUp; les opérations ci-dessus seront donc exécutées dans une méthode virtuelle indexée WMLBUTTONUP :

 
Sélectionnez
Procedure tFenetrePrincipale.WMLBUTTONUP (var Msg : tMessage);
(* Relâchement bouton gauche : fin du dessin et ajout à la liste *)
Var DC : hDC;
    AncienPinceau : hBrush;
    AncienneROP : LongInt;
Begin
  if BoutonEnfonce
     then
       begin
         (* Obtention du DC, sélection pinceau invisible, choix ROP *)
         DC := GetDC(hWindow);
         AncienPinceau := SelectObject(DC,GetStockObject(Hollow_Brush));
         AncienneROP := SetROP2(DC,r2_Not);
         (* Effacement de la forme actuelle *)
         DESSIN_FORME(DC);
         (* Dessin de la forme définitive *)
         DESSIN_FORME(DC);
         (* Libération du DC *)
         SetROP2(DC,AncienneROP);
         SelectObject(DC,AncienPinceau);
         ReleaseDC(hWindow,DC);
         (* Libération de la souris capturée *)
         ReleaseCapture;
         (* Mise à jour indicateur bouton de souris enfoncé *)
         BoutonEnfonce := False;
       end;
End;

Vous voyez que l'indicateur d'enfoncement du bouton gauche de la souris, BoutonEnfonce, est également mis à jour : les mouvements de souris ultérieurs n'entraîneront aucun dessin inopportun de forme.

VII-D-8. Mémorisation des formes

Lorsque nous avons entamé le présent chapitre, nous avons insisté sur le fait qu'un programme Windows doit mettre à la disposition du système toutes les instructions nécessaires pour redessiner son contenu. Dans le cas présent, nous allons garder en mémoire toutes les formes dessinées par l'utilisateur, pour permettre à Windows de les redessiner en cas de besoin. Pour ce faire, notre programme tiendra à jour une liste chaînée dont chaque élément contiendra les informations nécessaires au dessin d'une forme. Oh, il n'y a en fait pas besoin de beaucoup de renseignements :

  • Le type de forme (rectangle, ellipse ou ligne)
  • Les coordonnées de début (x1,y1)
  • Les coordonnées de fin (x2,y2)

La déclaration de type de la liste chaînée sera toute simple :

 
Sélectionnez
Type pForme = ^tForme;
     tForme = Record
                x1, y1, x2, y2 : LongInt;   (* Coordonnées *)
                Forme : LongInt;            (* Type de forme *)
                pSuiv : pForme;             (* Adresse forme suivante *)
              end;

L'endroit où, fort logiquement, une nouvelle forme sera ajoutée à la liste chaînée est l'endroit où celle-ci est définitivement fixée, c'est-à-dire la méthode WMLBUTTONUP :

 
Sélectionnez
Procedure tFenetrePrincipale.WMLBUTTONUP (var Msg : tMessage);
(* Relâchement bouton gauche : fin du dessin et ajout à la liste *)
Var ...
    p : pForme;
Begin
  if BoutonEnfonce
     then
       begin
         ...
         (* Ajout de la forme dans la liste chaînée *)
         New(p);
         if p <> Nil
            then
              begin
                p^.x1 := x1;
                p^.y1 := y1;
                p^.x2 := x2;
                p^.y2 := y2;
                p^.Forme := FormeCourante;
                p^.pSuiv := Nil;
                if pTeteFormes = Nil
                   then
                     pTeteFormes := p
                   else
                     pQueueFormes^.pSuiv := p;
                pQueueFormes := p;
              end;
       end;
End;

Pour que Windows puisse redessiner toutes les formes de la liste, nous mettons à sa disposition la méthode Paint :

 
Sélectionnez
Procedure tFenetrePrincipale.PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
(* Dessin de toutes les formes de la liste *)
Var x1Sauve, y1Sauve, x2Sauve, y2Sauve, FormeSauve : Integer;
    p : pForme;
    AncienPinceau : hBrush;
    AncienneROP : LongInt;
Begin
  (* Sauvegarde des champs *)
  x1Sauve := x1;
  y1Sauve := y1;
  x2Sauve := x2;
  y2Sauve := y2;
  FormeSauve := FormeCourante;
  (* Sélection d'un pinceau invisible, choix ROP *)
  AncienPinceau := SelectObject(PaintDC,GetStockObject(Hollow_Brush));
  AncienneROP := SetROP2(PaintDC,r2_Not);
  (* Dessin de toutes les formes de la liste *)
  p := pTeteFormes;
  while p <> Nil do
    begin
      x1 := p^.x1;
      y1 := p^.y1;
      x2 := p^.x2;
      y2 := p^.y2;
      FormeCourante := p^.Forme;
      DESSIN_FORME(PaintDC);
      p := p^.pSuiv;
    end;
  (* Restauration du DC dans son état d'origine *)
  SetROP2(PaintDC,AncienneROP);
  SelectObject(PaintDC,AncienPinceau);
  (* Restauration des champs *)
  x1 := x1Sauve;
  y1 := y1Sauve;
  x2 := x2Sauve;
  y2 := y2Sauve;
  FormeCourante := FormeSauve;
End;

La technique utilisée pour le dessin est rigoureusement identique à celle des méthodes WMMOUSEMOVE et WMLBUTTONUP. La liste chaînée des formes est entièrement passée en revue et chacune d'elles est redessinée.

L'utilisateur a la possibilité d'effacer à tout moment toutes les formes qui ont été dessinées jusque là, soit en sélectionnant le menu Commandes --> Supprimer tout, soit en pressant la touche Suppr. Dans les deux cas, le message de commande cm_Supprimer est envoyé à la fenêtre du programme, qui y répond au travers de la méthode WMCOMMAND :

 
Sélectionnez
Procedure tFenetrePrincipale.WMCOMMAND (var Msg : tMessage);
(* Réponse aux messages de commandes *)
Begin
  case LoWord(Msg.wParam) of
    cm_Supprimer : if not BoutonEnfonce
                      then   (* Suppression de la liste des formes *)
                        begin
                          InvalidateRect(hWindow,Nil,True);
                          DESTRUCTION_LISTE;
                        end;
  else
    tWindow.WMCOMMAND(Msg);
  end;
End;

L'effacement de la fenêtre est effectué par la fonction InvalidateRect, qui a été vue dans le chapitre sur la création des ressources. Parallèlement, il ne faudra plus que Windows redessine les formes qui viennent d'être effacées; la liste chaînée des formes est donc purgée par la méthode DESTRUCTION_LISTE.

Important : cette même méthode devra également impérativement être appelée à la fin du programme, pour détruire la liste chaînée des formes ! C'est dans le destructeur de la fenêtre principale que ce dernier nettoyage aura lieu :

 
Sélectionnez
Destructor tFenetrePrincipale.DONE;
(* Destruction de la liste chaînée des formes en fin de programme *)
Begin
  DESTRUCTION_LISTE;
  tWindow.DONE;
End;

VII-E. Affichage d'images : le programme SHOWIMG.PAS

Nous allons à présent réaliser un petit programme qui va charger un fichier BMP et l'afficher. L'utilisateur pourra régler le zoom de l'affichage et expérimenter toutes les opérations logiques possibles que l'on peut réaliser sur une image :

Image non disponibleImage non disponibleImage non disponible

Voici le source du programme :

 
Sélectionnez
Program SHOWIMG;

(* Affichage d'un fichier image.

   Réalisé par Alcatîz pour Developpez.com - 07-10-2006 *)


{$R SHOWIMG.RES}


Uses Strings,    (* Chaînes AZT *)
     Windows,    (* API Win32 *)
     CommDlg,    (* Dialogues standards *)
     OWindows;   (* Objets OWL *)


Const NomApplication = 'ShowImg';

{$I SHOWIMG.INC}


Type tAspects = Array [cm_SrcCopy..cm_Whiteness] of DWord;

     pFenetrePrincipale = ^tFenetrePrincipale;
     tFenetrePrincipale = Object(tWindow)
                            hImage : hBitmap;
                               (* Handle de l'image à afficher *)
                            DonneesImage : tBitmap;
                               (* Caractéristiques image à afficher *)
                            Zoom : LongInt;
                               (* Facteur de zoom actuel *)
                            Aspect : LongInt;
                               (* Aspect actuel *)
                            OFNFiltre : Array [0..79] of Char;
                               (* Filtre du dialogue GetOpenFileName *)
                            OFNDossier : Array [0..Max_Path] of Char;
                               (* Dossier démarrage dialogue GetOpenFileName *)
                            Constructor INIT (aParent : pWindowsObject; aTitle : pChar);
                               (* Chargement menu - Initialisation champs - Cochage 100 % *)
                            Procedure PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
                               virtual;
                               (* Affichage de l'image *)
                            Procedure WMCOMMAND (var Msg : tMessage);
                               virtual wm_First + wm_Command;
                               (* Réponse aux messages de commandes *)
                            Destructor DONE;
                               virtual;
                               (* Destruction de l'image *)
                          end;

     tProgramme = Object(tApplication)
                    Procedure INITMAINWINDOW;
                       virtual;
                       (* Allocation de la fenêtre principale du programme *)
                  end;

                  
Const Aspects : tAspects = (SrcCopy,SrcPaint,SrcAnd,SrcInvert,SrcErase,NotSrcCopy,
                            NotSrcErase,MergeCopy,MergePaint,PatCopy,PatPaint,
                            PatInvert,DstInvert,Blackness,Whiteness);


(* ----- Méthodes de l'objet tFenetrePrincipale ----- *)

Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
(* Chargement du menu - Initialisation des champs - Cochage commande zoom 100 % *)
Var i : DWord;   (* Indice dans le filtre *)
Begin
  tWindow.INIT(aParent,aTitle);
  (* Chargement du menu *)
  Attr.Menu := LoadMenu(hInstance,pChar(id_MenuPrincipal));
  (* Initialisation des champs *)
  hImage := 0;
  Zoom := cm_100pc;
  Aspect := cm_SrcCopy;
  StrCopy(OFNFiltre,'Bitmaps (*.bmp)|*.bmp|');
  for i := 0 to StrLen(OFNFiltre) do
    if OFNFiltre[i] = '|'
       then
         OFNFiltre[i] := #0;
  OFNDossier[0] := #0;
  (* Cochage des menus zoom et aspect *)
  CheckMenuItem(Attr.Menu,cm_100pc,mf_ByCommand or mf_Checked);
  CheckMenuItem(Attr.Menu,cm_SrcCopy,mf_ByCommand or mf_Checked);
End;

Procedure tFenetrePrincipale.PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
(* Affichage de l'image *)
Var MemDC : hDC;                (* DC compatible *)
    AncienneBitmap : hBitmap;   (* Ancienne bitmap sélectionnée dans MemDC *)
    l, h : LongInt;             (* Largeur et hauteur image à l'écran *)
    Rect : tRect;               (* Coordonnées de la zone client *)
Begin
  if hImage <> 0
     then
       begin
         MemDC := CreateCompatibleDC(PaintDC);
         AncienneBitmap := SelectObject(MemDC,hImage);
         case Zoom of
           cm_50pc : begin   (* Zoom 50 % *)
                       l := DonneesImage.bmWidth div 2;
                       h := DonneesImage.bmHeight div 2;
                     end;
           cm_100pc : begin   (* Zoom 100 % *)
                        l := DonneesImage.bmWidth;
                        h := DonneesImage.bmHeight;
                      end;
           cm_200pc : begin   (* Zoom 200 % *)
                        l := DonneesImage.bmWidth * 2;
                        h := DonneesImage.bmHeight * 2;
                      end;
           cm_Ajuster : begin   (* Ajuster à la taille fenêtre *)
                          GetClientRect(hWindow,Rect);
                          l := Rect.Right;
                          h := Rect.Bottom;
                        end;
         end;
         StretchBlt(PaintDC,0,0,l,h,MemDC,0,0,DonneesImage.bmWidth,DonneesImage.bmHeight,Aspects[Aspect]);
         SelectObject(MemDC,AncienneBitmap);
         DeleteDC(MemDC);
       end;  
End;

Procedure tFenetrePrincipale.WMCOMMAND (var Msg : tMessage);
(* Réponse aux messages de commandes *)
Var OFN : tOpenFileName;                        (* Structure du dialogue GetOpenFileName *)
    Chemin : Array [0..Max_Path] of Char;       (* Résultat du dialogue *)
    NomFichier : Array [0..Max_Path] of Char;   (* Nom seul du fichier *)
    Titre : Array [0..Max_Path + 12] of Char;   (* Titre de la fenêtre *)
Begin
  case LoWord(Msg.wParam) of
    cm_FileOpen : begin   (* Ouverture d'un fichier image *)
                    FillChar(OFN,SizeOf(OFN),0);
                    (* Taille de la structure *)
                    OFN.lStructSize := SizeOf(OFN);
                    (* Handle fenêtre parent *)
                    OFN.hWndOwner := hWindow;
                    (* Filtre d'affichage des fichiers *)
                    OFN.lpstrFilter := OFNFiltre;
                    OFN.nFilterIndex := 1;
                    (* Résultat retourné par le dialogue *)
                    Chemin[0] := #0;
                    OFN.lpstrFile := Chemin;
                    OFN.nMaxFile := SizeOf(Chemin);
                    (* Nom seul du fichier retourné par le dialogue *)
                    OFN.lpstrFileTitle := NomFichier;
                    OFN.nMaxFileTitle := SizeOf(NomFichier);
                    (* Dossier de démarrage *)
                    if StrLen(OFNDossier) > 0
                       then
                         OFN.lpstrInitialDir := OFNDossier
                       else
                         OFN.lpstrInitialDir := Nil;
                    (* Flags *)
                    OFN.Flags := ofn_PathMustExist or ofn_FileMustExist or ofn_HideReadOnly;
                    (* Affichage du dialogue *)
                    if GetOpenFileName(OFN)
                       then
                         begin
                           (* Sauvegarde du dossier pour prochaine fois *)
                           StrCopy(OFNDossier,Chemin);
                           OFNDossier[OFN.nFileOffset] := #0;
                           (* Destruction de l'image actuelle *)
                           if hImage <> 0
                              then
                                DeleteObject(hImage);
                           (* Chargement de la nouvelle image *)
                           hImage := LoadImage(0,Chemin,Image_Bitmap,0,0,lr_LoadFromFile);
                           if hImage <> 0
                              then
                                begin
                                  (* Titre de la fenêtre *)
                                  StrCopy(Titre,NomApplication);
                                  StrCat(Titre,' - [');
                                  StrCat(Titre,NomFichier);
                                  StrCat(Titre,']');
                                  SetWindowText(hWindow,Titre);
                                  (* Caractéristiques de l'image *)
                                  GetObject(hImage,SizeOf(DonneesImage),@DonneesImage);
                                  (* Affichage de l'image *)
                                  InvalidateRect(hWindow,Nil,True);
                                end
                              else   (* Erreur *)
                                begin
                                  (* Titre de la fenêtre *)
                                  SetWindowText(hWindow,NomApplication);
                                  (* Message d'erreur *)
                                  MessageBox(hWindow,'Impossible de charger l''image','Erreur',mb_IconHand or mb_Ok);
                                end;  
                         end;
                  end;
    cm_50pc..cm_Ajuster : begin   (* Réglage du zoom *)
                            if Zoom <> LoWord(Msg.wParam)
                               then
                                 begin
                                   CheckMenuItem(Attr.Menu,Zoom,mf_ByCommand or mf_Unchecked);
                                   Zoom := LoWord(Msg.wParam);
                                   CheckMenuItem(Attr.Menu,Zoom,mf_ByCommand or mf_Checked);
                                   InvalidateRect(hWindow,Nil,True);
                                 end;
                          end;
    cm_SrcCopy..cm_Whiteness : begin   (* Changement d'aspect *)
                                 CheckMenuItem(Attr.Menu,Aspect,mf_ByCommand or mf_Unchecked);
                                 Aspect := LoWord(Msg.wParam);
                                 CheckMenuItem(Attr.Menu,Aspect,mf_ByCommand or mf_Checked);
                                 InvalidateRect(hWindow,Nil,True);
                               end;                   
  else
    tWindow.WMCOMMAND(Msg);
  end;
End;

Destructor tFenetrePrincipale.DONE;
(* Destruction de l'image *)
Begin
  if hImage <> 0
     then
       DeleteObject(hImage);
  tWindow.DONE;     
End;


(* ----- Méthodes de l'objet tProgramme ----- *)

Procedure tProgramme.INITMAINWINDOW;
(* Allocation de la fenêtre principale du programme *)
Begin
  MainWindow := New(pFenetrePrincipale,INIT(Nil,NomApplication));
End;


(* ----- Variables globales ----- *)

Var Programme : tProgramme;


(* ----- Programme principal ----- *)

Begin
  Programme.INIT(NomApplication);
  Programme.RUN;
  Programme.DONE;
End.

VII-E-1. Ressources et identificateurs

Rien de transcendant à se mettre sous la dent au niveau des ressources : un menu.

Voici le source du fichier SHOWIMG.RC :

 
Sélectionnez
#include "showimg.inc"
id_MenuPrincipal MENU 
{
 POPUP "&Fichier"
 {
  MENUITEM "&Ouvrir", cm_FileOpen
  MENUITEM SEPARATOR
  MENUITEM "&Quitter", cm_Exit
 }
 POPUP "&Zoom"
 {
  MENUITEM "&50 %", cm_50pc
  MENUITEM "&100 %", cm_100pc
  MENUITEM "&200 %", cm_200pc
  MENUITEM SEPARATOR
  MENUITEM "&Ajuster à la fenêtre", cm_Ajuster
 }
 POPUP "&Aspect"
 {
  MENUITEM "SrcCopy", cm_SrcCopy
  MENUITEM "SrcPaint", cm_SrcPaint
  MENUITEM "SrcAnd", cm_SrcAnd
  MENUITEM "SrcInvert", cm_SrcInvert
  MENUITEM "SrcErase", cm_SrcErase
  MENUITEM "NotSrcCopy", cm_NotSrcCopy
  MENUITEM "NotSrcErase", cm_NotSrcErase
  MENUITEM "MergeCopy", cm_MergeCopy
  MENUITEM "MergePaint", cm_MergePaint
  MENUITEM "PatCopy", cm_PatCopy
  MENUITEM "PatPaint", cm_PatPaint
  MENUITEM "PatInvert", cm_PatInvert
  MENUITEM "DstInvert", cm_DstInvert
  MENUITEM "Blackness", cm_Blackness
  MENUITEM "Whiteness", cm_Whiteness
 }
}

Et à présent le fichier SHOWIMG.INC :

 
Sélectionnez
const
        cm_FileOpen      = 24330;
        cm_Exit          = 24340;
        id_MenuPrincipal = 1;
        id_Accel         = 2;
        cm_100pc         = 102;
        cm_200pc         = 103;
        cm_50pc          = 101;
        cm_Ajuster       = 104;
        cm_SrcCopy       = 201;
        cm_SrcPaint      = 202;
        cm_SrcAnd        = 203;
        cm_SrcInvert     = 204;
        cm_SrcErase      = 205;
        cm_NotSrcCopy    = 206;
        cm_NotSrcErase   = 207;
        cm_MergeCopy     = 208;
        cm_MergePaint    = 209;
        cm_PatCopy       = 210;
        cm_PatPaint      = 211;
        cm_PatInvert     = 212;
        cm_DstInvert     = 213;
        cm_Blackness     = 214;
        cm_Whiteness     = 215;

VII-E-2. Dialogue standard d'ouverture de fichier

Windows met à la disposition du programmeur une panoplie de dialogues standards, que l'on retrouve dans de très nombreuses applications : dialogues d'ouverture ou d'enregistrement de fichier, dialogues de choix de couleur ou de police de caractères, dialogue de recherche de texte, dialogue d'impression.
Les fonctions de création des dialogues standards sont déclarés dans l'unité CommDlg de Virtual Pascal.

Dans le présent programme, nous avons besoin du dialogue d'ouverture de fichier et de la fonction qui le crée, GetOpenFileName :

 
Sélectionnez
Function GetOpenFileName (var OpenFile : tOpenFilename) : Bool;

Un seul paramètre pour cette fonction, de type tOpenFileName :

 
Sélectionnez
type pOpenFileName = ^tOpenFileName;
     tOpenFileName = Packed record
                       lStructSize : DWord;
                       hwndOwner : hWnd;
                       hInstance : hInst;
                       lpstrFilter : pChar;
                       lpstrCustomFilter : pChar;
                       nMaxCustFilter : DWord;
                       nFilterIndex : DWord;
                       lpstrFile : pChar;
                       nMaxFile : DWord;
                       lpstrFileTitle : pChar;
                       nMaxFileTitle : DWord;
                       lpstrInitialDir : pChar;
                       lpstrTitle : pChar;
                       Flags : DWord;
                       nFileOffset : SmallWord;
                       nFileExtension : SmallWord;
                       lpstrDefExt : pChar;
                       lCustData : DWord;
                       lpfnHook : tOFNHookProc;
                       lpTemplateName : pChar;
                     end;

Windows permet beaucoup de souplesse dans l'utilisation de ses dialogues standards : on peut mettre le titre que l'on veut, on peut choisir de masquer ou d'afficher certains contrôles, on peut même en remplacer l'aspect ou le comportement général. Dans le cadre de ce tutoriel, nous n'allons pas exploiter toutes les possibilités du dialogue d'ouverture de fichier. Voici les champs qui nous intéressent :

  • lStructSize : c'est la taille totale de la structure
  • hWndOwner est le handle de la fenêtre qui exécute le dialogue
  • lpstrFilter est un ou plusieurs filtre(s) sur les extensions de fichiers
  • nFilterIndex est l'index du filtre à utiliser au démarrage
  • lpstrFile recevra le chemin complet du fichier choisi par l'utilisateur
  • nMaxFile est la taille du buffer lpstrFile
  • lpstrFileTitle recevra le nom seul du fichier choisi
  • nMaxFileTitle est la taille du buffer lpstrFileTitle
  • lpstrinitialDir est le dossier courant au démarrage du dialogue
  • Flags est une combinaison or de plusieurs options
  • nFileOffset recevra l'index du nom de fichier dans le buffer lpstrFile

Le dialogue d'ouverture de fichier est exécuté lorsque l'utilisateur sélectionne le menu Fichier --> Ouvrir. Nous avons affecté à cette commande l'identificateur cm_FileOpen, que nous avons listé avec tous les identificateurs standards de Windows.

L'ouverture de fichier aura lieu dans une méthode WMCOMMAND :

 
Sélectionnez
Procedure tFenetrePrincipale.WMCOMMAND (var Msg : tMessage);
(* Réponse aux messages de commandes *)
Var OFN : tOpenFileName;                        (* Structure du dialogue GetOpenFileName *)
    Chemin : Array [0..Max_Path] of Char;       (* Résultat du dialogue *)
    NomFichier : Array [0..Max_Path] of Char;   (* Nom seul du fichier *)
    Titre : Array [0..Max_Path + 12] of Char;   (* Titre de la fenêtre *)
Begin
  case LoWord(Msg.wParam) of
    cm_FileOpen : begin   (* Ouverture d'un fichier image *)
                    FillChar(OFN,SizeOf(OFN),0);
                    (* Taille de la structure *)
                    OFN.lStructSize := SizeOf(OFN);
                    (* Handle fenêtre parent *)
                    OFN.hWndOwner := hWindow;
                    (* Filtre d'affichage des fichiers *)
                    OFN.lpstrFilter := OFNFiltre;
                    OFN.nFilterIndex := 1;
                    (* Résultat retourné par le dialogue *)
                    Chemin[0] := #0;
                    OFN.lpstrFile := Chemin;
                    OFN.nMaxFile := SizeOf(Chemin);
                    (* Nom seul du fichier retourné par le dialogue *)
                    OFN.lpstrFileTitle := NomFichier;
                    OFN.nMaxFileTitle := SizeOf(NomFichier);
                    (* Dossier de démarrage *)
                    if StrLen(OFNDossier) > 0
                       then
                         OFN.lpstrInitialDir := OFNDossier
                       else
                         OFN.lpstrInitialDir := Nil;
                    (* Flags *)
                    OFN.Flags := ofn_PathMustExist or ofn_FileMustExist or ofn_HideReadOnly;
                    (* Affichage du dialogue *)
                    if GetOpenFileName(OFN)
                       then
                         begin
                           (* Sauvegarde du dossier pour prochaine fois *)
                           StrCopy(OFNDossier,Chemin);
                           OFNDossier[OFN.nFileOffset] := #0;
                           
                           { Chargement de l'image }

                         end;
                  end;
  else
    tWindow.WMCOMMAND(Msg);
  end;
End;

Pour le confort de l'utilisateur, nous décidons de mémoriser dans quel dossier le fichier est ouvert, de manière à lui proposer directement ce dossier lors de l'ouverture suivante. Nous déclarons à cet effet le champ OFNDossier dans l'objet fenêtre principale. Au sujet de la taille de cette chaîne, rappelez-vous que nous avons déjà utilisé la constante Max_Path dans le chapitre sur l'unité WinCRT.

Un autre champ, OFNFiltre, contient le filtre utilisé par le dialogue pour n'afficher que les fichiers d'un ou plusieurs type particuliers. En effet, notre programme ne sera capable de charger que des images bitmaps; il est donc inutile voire idiot de laisser l'utilisateur essayer de charger un fichier JPG, par exemple, si ce format n'est pas supporté ! La meilleure manière d'empêcher cela est de faire en sorte que seuls les fichiers d'extension BMP apparaissent dans le dialogue, donc de définir un filtre.

Le champ lpstrFilter de la structure tOpenFileName est d'un format très spécial : il s'agit d'une liste de couples de sous-chaînes séparées par des 0 (le caractère #0). Chaque couple est constitué d'une description, qui est affichée par le dialogue, et d'un filtre. Les deux sont également séparés par un 0. Le filtre peut être un nom de fichier ou un joker avec une extension et, s'il y en a plusieurs, ils sont séparés par un point-virgule. La chaîne doit se terminer par un double 0.

Dans notre programme, nous définissons le filtre une fois pour toutes dans le constructeur Init de la fenêtre principale :

 
Sélectionnez
  StrCopy(OFNFiltre,'Bitmaps (*.bmp)|*.bmp|');
  for i := 0 to StrLen(OFNFiltre) do
    if OFNFiltre[i] = '|'
       then
         OFNFiltre[i] := #0;

Il est difficile de jouer avec des caractères #0 dans les chaînes AZT, puisqu'il s'agit du caractère terminal. Pour contourner le problème, nous remplaçons les #0 par un autre caractère (ici, la barre verticale) et, in fine, nous faisons une boucle pour remplacer ce caractère par un 0.

Le filtre utilisé dans notre programme est évidemment très simple. Pour illustrer les explications ci-dessus, voici un exemple de filtre plus complexe composé de trois sous-filtres (fichier images, fichiers textes et... tous les fichiers) :

 
Sélectionnez
  StrCopy(OFNFiltre,'Fichiers images|*.bmp;*.pcx;*.jpg|Fichiers textes|*.txt|Tous les fichiers|*.*|');
  for i := 0 to StrLen(OFNFiltre) do
    if OFNFiltre[i] = '|'
       then
         OFNFiltre[i] := #0;


Une dernière chose à présent : la structure tOpenFileName admet toute une série de flags, que vous pouvez découvrir dans le SDK. Voici ceux que nous utilisons dans notre programme :

  • ofn_PathMustExist oblige de retourner un chemin valide
  • ofn_FileMustExist oblige de retourner un fichier existant
  • ofn_HideReadOnly cache une case à cocher inutile permettant d'ouvrir un fichier en lecture seule

VII-E-3. Chargement de l'image

Une fois que la fonction GetOpenFileName se termine avec succès (elle renvoie un booléen), le chemin complet du fichier à charger se trouve dans la variable locale Chemin.
Pour le chargement de l'image, nous allons profiter de la possibilité accordée par la fonction LoadImage d'aller chercher cette image dans un fichier - uniquement dans un fichier BMP. Voici la déclaration de cette fonction :

 
Sélectionnez
Function LoadImage (hInst : hInst; ImageName : pChar; ImageType : UInt; X, Y : Integer; Flags : UInt) : tHandle;
  • hInst est le handle d'instance du module qui contient l'image (0 pour un fichier)
  • ImageName est le nom de l'image ou, dans notre cas, le nom du fichier
  • ImageType identifie le type d'image : Image_Bitmap, Image_Cursor ou Image_Icon
  • X et Y sont la largeur et la hauteur, uniquement pour un curseur ou une icône
  • Flags permet de définir les options de chargement (lr_LoadFromFile dans notre cas)

La valeur retournée par la fonction est le handle de l'image, qui vaudra 0 si une erreur s'est produite. Nous écrivons donc dans notre programme :

 
Sélectionnez
                    if GetOpenFileName(OFN)
                       then
                         begin
                           (* Sauvegarde du dossier pour prochaine fois *)
                           StrCopy(OFNDossier,Chemin);
                           OFNDossier[OFN.nFileOffset] := #0;
                           (* Chargement de la nouvelle image *)
                           hImage := LoadImage(0,Chemin,Image_Bitmap,0,0,lr_LoadFromFile);
                           if hImage <> 0
                              then
                                begin

                                  { Affichage de l'image }
                                  
                                end
                              else   (* Erreur *)
                                MessageBox(hWindow,'Impossible de charger l''image','Erreur',mb_IconHand or mb_Ok);

Vous trouverez sans doute que le programme est très limité, puisqu'il ne permet de charger que des images au format BMP. C'est volontaire, puisque le but de ce chapitre est de se familiariser avec l'interface GDI. Rien ne vous empêche de développer des routines de chargement d'images dans d'autres formats !

VII-E-4. Titre de la fenêtre

La plupart des applications Windows qui permettent de charger des fichiers affichent le nom de ce fichier dans la barre de titre. Nous n'allons pas déroger à cette règle; c'est pourquoi nous récupérons le nom du fichier chargé dans la variable locale NomFichier, qui est passée comme paramètre lpstrFileTitle de la structure tOpenFileName. Dans la barre de titre, nous afficherons le nom du fichier entre crochets, à la suite du nom de l'application.

La fonction SetWindowText permet de définir le titre de la fenêtre :

 
Sélectionnez
Function SetWindowText (Wnd : hWnd; Str : pChar) : Bool;

Sont passés comme paramètres le handle de la fenêtre et le titre.

Donc, si le chargement de l'image s'est bien déroulé alors nous afficherons le nom du fichier entre crochets dans la barre de titre, sinon nous n'affichons que le nom de l'application :

 
Sélectionnez
                           hImage := LoadImage(0,Chemin,Image_Bitmap,0,0,lr_LoadFromFile);
                           if hImage <> 0
                              then
                                begin
                                  (* Titre de la fenêtre *)
                                  StrCopy(Titre,NomApplication);
                                  StrCat(Titre,' - [');
                                  StrCat(Titre,NomFichier);
                                  StrCat(Titre,']');
                                  SetWindowText(hWindow,Titre);
                                  
                                  { Affichage de l'image }
                                  
                                end
                              else   (* Erreur *)
                                begin
                                  (* Titre de la fenêtre *)
                                  SetWindowText(hWindow,NomApplication);
                                  (* Message d'erreur *)
                                  MessageBox(hWindow,'Impossible de charger l''image','Erreur',mb_IconHand or mb_Ok);
                                end;

VII-E-5. Affichage de l'image

Avant d'aborder le côté technique de l'affichage, parlons un peu des possibilités de l'utilisateur. D'une part, il peut choisir un niveau de zoom : 50%, 100%, 200% ou ajustage de l'image aux dimensions de la fenêtre. Il effectue ce choix dans le menu Zoom et ce choix est mémorisé en stockant l'identificateur de la commande choisie dans le champ Zoom de l'objet fenêtre principale.

D'autre part, il peut choisir dans le menu Aspect une opération logique à appliquer à l'image pour changer son aspect à l'écran L'affichage de l'image chargée en mémoire se fait dans une méthode Paint. Bien entendu, nous devons commencer par tester qu'il y a bien une image en mémoire, en regardant si le champ hImage, qui contient le handle de l'image courante, ne vaut pas 0.

Voici notre méthode Paint :

 
Sélectionnez
Procedure tFenetrePrincipale.PAINT (PaintDC : hDC; var PaintInfo : tPaintStruct);
(* Affichage de l'image *)
Var MemDC : hDC;                (* DC compatible *)
    AncienneBitmap : hBitmap;   (* Ancienne bitmap sélectionnée dans MemDC *)
    l, h : LongInt;             (* Largeur et hauteur image à l'écran *)
    Rect : tRect;               (* Coordonnées de la zone client *)
Begin
  if hImage <> 0
     then
       begin
         MemDC := CreateCompatibleDC(PaintDC);
         AncienneBitmap := SelectObject(MemDC,hImage);
         case Zoom of
           cm_50pc : begin   (* Zoom 50 % *)
                       l := DonneesImage.bmWidth div 2;
                       h := DonneesImage.bmHeight div 2;
                     end;
           cm_100pc : begin   (* Zoom 100 % *)
                        l := DonneesImage.bmWidth;
                        h := DonneesImage.bmHeight;
                      end;
           cm_200pc : begin   (* Zoom 200 % *)
                        l := DonneesImage.bmWidth * 2;
                        h := DonneesImage.bmHeight * 2;
                      end;
           cm_Ajuster : begin   (* Ajuster à la taille fenêtre *)
                          GetClientRect(hWindow,Rect);
                          l := Rect.Right;
                          h := Rect.Bottom;
                        end;
         end;
         StretchBlt(PaintDC,0,0,l,h,MemDC,0,0,DonneesImage.bmWidth,DonneesImage.bmHeight,Aspects[Aspect]);
         SelectObject(MemDC,AncienneBitmap);
         DeleteDC(MemDC);
       end;  
End;

précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005-2007 Jean-Luc Gofflot. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.