La programmation Win32 en Virtual Pascal avec OWL

Alcatîz

Ce tutoriel vous fournira les bases nécessaires pour créer des programmes Windows en Virtual Pascal à l'aide d'Object Windows Library (OWL), la bibliothèque sur laquelle ont été construites les premières versions de Delphi.

Si vous voulez comprendre les mécanismes intimes d'un programme Windows, ce cours complet est fait pour vous !

Commentez

- Titre : La programmation Win32 en Virtual Pascal avec OWL

- Auteur : Alcatîz

- Parution : 6 novembre 2005

- Mise à jour : 5 avril 2013

- Public visé :

- Licence : 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 et 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.

Sommaire

- I. Préliminaires

- I-A. Prérequis

- I-B. Tutoriels disponibles dans la section Pascal/Delphi de Developpez.com

- I-C. Compilateur et utilitaires nécessaires

- I-C-1. Compilateur

- I-C-2. Editeur de ressources

- I-C-3. Documentation

- I-D. Configuration de Virtual Pascal 2.1

- I-D-1. Sources de la bibiliothèque OWL

- I-D-2. Compilation de la RTL et de OWL

- I-D-3. Réglages des options

- I-D-3-a. Plateforme de destination

- I-D-3-b. Compilateur

- I-D-3-c. Editeur de liens

- I-D-3-d. Taille de l'éditeur pour un confort maximum

- I-D-3-e. Vitesse de clignotement du curseur

- I-D-3-f. Coloration syntaxique

- I-E. Codes ANSI des caractères accentués

- II. Les chaînes à zéro terminal

- II-A. Tableaux de caractères et type pChar

- II-A-1. En résumé

- II-B. Opérations sur les chaînes à zéro terminal

- II-B-1. Assigner une valeur à une chaîne

- II-B-2. Initialiser une chaîne vide

- II-B-3. Tronquer une chaîne

- II-B-4. Déterminer la longueur d'une chaîne

- II-B-5. Copier une chaîne

- II-B-6. Comparer deux chaînes

- II-B-7. Déterminer la position d'une chaîne dans une autre

- II-B-8. Concaténer deux chaînes

- II-B-9. Conversion entre chaînes AZT et Strings

- II-B-10. Allocation dynamique de chaînes AZT

- III. Création de petits programmes avec l'unité WinCRT

- III-A. Déclarations

- III-B. Programme de démonstration : DIRTOT.PAS

- III-B-1. Variables

- III-B-2. L'unité Windows

- III-B-3. Initialisation de la fenêtre WinCRT

- III-B-4. La procédure récursive de recherche

- III-B-3-a. Affichage du nom de dossier

- III-B-3-b. Allocation dynamique de chaînes

- III-B-4-c. Changement de position du curseur

- III-B-4-d. Libération des chaînes dynamiques

- III-B-5. Terminaison du programme

- III-C. Téléchargement des sources des programmes de cette page

- IV. Notre premier programme OWL : MINIMUM.PAS

- IV-A. L'unité OWindows

- IV-B. L'objet tApplication

- IV-C. La méthode InitMainWindow

- IV-D. L'objet fenêtre tWindow

- IV-E. En résumé

- IV-F. Téléchargement des sources des programmes de cette page

- V. Réponses aux messages Windows

- V-A. Evénements et messages

- V-B. Structure d'un message

- V-C. Identificateurs de messages

- V-D. Réponse aux clics de souris : CLIC.PAS

- V-D-1. La zone client de la fenêtre

- V-D-2. Objet descendant de tWindow

- V-D-3. Méthodes virtuelles dynamiques indexées

- V-D-4. L'unité Windows

- V-D-5. La fonction MessageBox

- V-E. En résumé

- V-F. Téléchargement des sources des programmes de cette page

- VI. Création de ressources

- VI-A. Utilisation de Borland Resource Workshop 4.5

- VI-A-1. Plateforme de destination et génération des identificateurs

- VI-A-2. Création du projet

- VI-A-3. Ajout d'un fichier include

- VI-A-4. Création de bitmaps

- VI-A-4-a. Création complète d'une image

- VI-A-4-b. Utilisation d'une image existante

- VI-A-5. Création d'un menu

- VI-A-5-a. Identificateurs standards de commandes de menu

- VI-A-6. Création d'une table d'accélérateurs

- VI-A-6-a. Ajout des raccourcis dans les menus

- VI-A-7. Création d'une icône

- VI-A-8. Création d'un curseur

- VI-A-9. Attribution d'identificateurs aux ressources

- VI-A-10. Options de gestion des ressources en mémoire

- VI-A-11. Enregistrement et compilation du projet

- VI-A-11-a. Fichier de configuration

- VI-B. Utilisation des ressources dans un programme

- VI-B-1. Menu

- VI-B-2. Table d'accélérateurs

- VI-B-3. Icône

- VI-B-4. Curseur

- VI-B-5. Bitmap

- VI-B-6. En résumé

- VI-C. Le programme CHGFOND.PAS

- VI-C-1. Directives de compilation

- VI-C-2. Les bitmaps et les fonds de fenêtre

- VI-C-2-a. Définir un fond de fenêtre au démarrage du programme

- VI-C-2-b. Définir un fond de fenêtre en cours de programme

- VI-C-2-c. Déterminer le fond par défaut

- VI-C-3. Les autres ressources

- VI-C-4. Version avec une méthode WMCOMMAND

- VI-C-5. Téléchargement des sources des programmes de cette page

- VII. GDI

- VII-A. Contexte de périphérique

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

- VII-B-1. Crayons et pinceaux

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

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

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

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

- VII-B-1-e. Illustration : amélioration de la méthode PAINT

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

- VII-C-1. Ressources

- VII-C-2. Timer

- VII-C-3. Dessin des lignes

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

- VII-C-4-a. Attributs de style de fenêtre

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

- VII-D-1. Ressources

- VII-D-2. Curseur du stock de Windows

- VII-D-3. Cochage du menu

- VII-D-4. Dessin d'une forme

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

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

- VII-D-6-a. Pinceau invisible - Objets du stock de Windows

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

- VII-D-6-c. Effacement et dessin

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

- VII-D-8. Mémorisation des formes

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

- VII-E-1. Ressources et identificateurs

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

- VII-E-3. Chargement de l'image

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

- VII-E-5. Affichage de l'image

- VIII. Index

- IX. Abréviations

- X. Téléchargement des sources des programmes du tutoriel

I - Préliminaires

Avec les outils RAD (Rapid Application Development) actuels comme Delphi, il est devenu aisé de programmer un éditeur de texte complet en 5 minutes chrono. Au niveau rapport qualité/rendement/prix, c'est imbattable. Et voilà que j'arrive avec un tutoriel sur une bibliothèque issue des débuts de la programmation orientée objet sous Windows. Et je vous dis : voici une alternative intéressante à la programmation RAD ! Attention, je ne dis pas "alternative rentable", je dis bien "alternative intéressante".

Quel intérêt peut-il bien y avoir là-dedans ? Eh bien, OWL (Object Windows Library) est une bibliothèque d'objets génériques d'interface avec Windows. Un objet Windows, c'est aussi bien une fenêtre qu'une boîte de dialogue, qu'un bouton, une boîte liste, une barre de défilement, une police de caractères, un pinceau pour dessiner, etc. Grâce aux objets OWL, il est possible de modifier et/ou d'enrichir le comportement des objets Windows.

Une différence par rapport aux outils RAD est qu'un programme OWL ne nécessite pas de couche logicielle supplémentaire mais s'appuie directement sur le système, sur l'API (Application Programming Interface). OWL étant proche du système, elle permet donc de mieux comprendre le fonctionnement intime de Windows.

I-A - Prérequis

Bien que ce tutoriel soit très progressif, il demande une bonne maîtrise du langage Pascal. Il est en tout cas rédigé comme si vous étiez un(e) programmeur(euse) confirmé(e) issu(e) de Turbo Pascal.
Si vous provenez du monde de Delphi, vous serez sans doute dépaysé(e) par l'aspect non RAD du développement et par l'utilisation intensive de l'API Windows. Si vous êtes déjà rompu(e) à la programmation Windows classique, vous serez sans doute dépaysé(e) par l'aspect objets mais n'aurez aucun mal à digérer les notions relatives au fonctionnement interne de Windows. Enfin, si vous avez déjà travaillé avec l'interface TurboVision, vous retrouverez certaines notions, bien que le fonctionnement interne de OWL soit complètement différent.

I-B - Tutoriels disponibles dans la section Pascal/Delphi de Developpez.com

D'excellents cours peuvent vous aider à atteindre le niveau requis pour aborder le présent tutoriel :

- Cours complet Turbo Pascal 7 par Cyberzoïde

- Exercices corrigés en Turbo Pascal par Wormful_sickfoot et Giovanny Temgoua

- Introduction à la programmation orientée objet par Eric Sigoillot

- Strings et pChar en Delphi par Adrien Reboisson

I-C - Compilateur et utilitaires nécessaires

I-C-1 - Compilateur

Les sources que vous rencontrerez tout au long du tutoriel sont rédigés pour le compilateur Virtual Pascal 2.1 (build 279), entièrement compatible avec Borland Pascal for Windows 7.0.

Vous pouvez télécharger l'IDE complet de Virtual Pascal sur Developpez.com :

- Téléchargez Virtual Pascal

Il est possible que le portage de la bibliothèque OWL soit un jour également réalisé pour le compilateur Free Pascal.

I-C-2 - Editeur de ressources

Un bon éditeur de ressources visuel est également requis. Borland Resource Workshop 4.5 est un excellent éditeur, tant pour créer des ressources 16 bits que des ressources 32 bits. Cet outil accompagne d'ailleurs plusieurs environnements de développement comme Borland C++, Turbo C++ ou Delphi.

I-C-3 - Documentation

Pour terminer, il vous faut une solide documentation sur l'API Windows (Application Programming Interface). Internet regorge de ce genre de ressources mais vous pouvez vous contenter de télécharger le fichier hlp dont je me sers généralement :

- Téléchargez le fichier win32apihlp.zip

Mais LA référence incontournable reste la MSDN Microsoft.

I-D - Configuration de Virtual Pascal 2.1

I-D-1 - Sources de la bibiliothèque OWL

L'installation de Virtual Pascal ne devrait pas poser de problème particulier. Les sources de la bibliothèque OWL ne seront installés que si sont présents sur votre disque dur les fichiers sources de la RTL (Run-Time Library) de Borland Pascal for Windows 7.0. Ce compilateur est protégé par licence; Borland International / CodeGear ne le propose plus au téléchargement. Quoi qu'il en soit, si vous tenez absolument à vous le procurer, des licences sont toujours disponibles à la vente sur le Net. Mais rassurez-vous, les sources de la bibliothèque OWL ne sont pas indispensables pour pouvoir utiliser celle-ci.
Vous pouvez en télécharger la version compilée ci-dessous.

I-D-2 - Compilation de la RTL et de OWL

Deux "méta-unités" sont fournies avec Virtual Pascal, l'une pour compiler les sources de la RTL et l'autre pour compiler la bibliothèque OWL. La seconde n'est bien sûr disponible que si les sources de la bibliothèque OWL ont été installés (voir le paragraphe précédent). Il suffit de charger ces "méta-unités" dans l'éditeur puis de les compiler avec la commande Build du menu Compile.

Voici leur localisation :

- bldrtl.pas : dossier \Source\RTL - pour compiler la RTL

- buildowl.pas : dossier \Source\OWL - pour compiler la bibliothèque OWL

Si vous éprouvez des difficultés à installer et/ou compiler la RTL ou la bibliothèque OWL, ou si, pour cette dernière, les sources n'ont pas été installés, vous pouvez les télécharger ci-dessous et les copier dans le dossier \out.w32\units :

- Téléchargez le fichier owlunits.zip

I-D-3 - Réglages des options

I-D-3-a - Plateforme de destination

L'intégralité du tutoriel étant consacrée à la programmation Windows, il est évident que la plateforme de destination doit être Win32. Le choix de la plateforme se fait par le menu Compile --> Target platform :

I-D-3-b - Compilateur

Fort logiquement, les options du compilateur sont accessibles via le menu Options --> Compiler :

Vous ne devez pas considérer ces options comme définitivement figées. En effet, selon l'étape où vous vous trouvez dans le développement, vous pouvez régler différemment les options du compilateur. Vous disposez pour cela des trois boutons en bas et à gauche :

- Debug pendant la phase de débogage;

- Fastest pour générer votre exécutable final avec un maximum de peformances;

- Smallest pour générer votre exécutable final avec la plus petite taille possible.

Il est peu probable que le choix du plus petit exécutable possible soit une bonne solution car la réduction du code et des données se fait souvent au détriment des performances générales. Mais rien ne vous empêche bien sûr de tester les deux options.
Une fois votre choix fait, vous pouvez aussi modifier l'une ou l'autre option.

Il est très important que la syntaxe étendue (Extended syntax) soit activée. Sans trop anticiper sur le contenu du tutoriel, cela permettra notamment d'appeler des fonctions comme des procédures et d'utiliser des chaînes AZT (A Zéro Terminal) avec les routines classiques du Pascal.

I-D-3-c - Editeur de liens

Les options de l'éditeur de liens sont accessibles via le menu Options --> Linker :

Tout d'abord, les programmes que nous allons créer tout au long de ce tutoriel devront tourner sous l'interface graphique de Windows. Il faut donc sélectionner GUI application (pour Graphics User Interface).
L'option Generate location info reste cochée tant que vous êtes au stade du débogage. Lorsque vous générerez votre exécutable final, vous pourrez la décocher.

Le reste des options peut être laissé par défaut.

I-D-3-d - Taille de l'éditeur pour un confort maximum

L'IDE (Integrated Development Environment, en français environnement de développement intégré) de Virtual Pascal se compose d'un éditeur complet en mode texte et d'un debugger intégré.
Par défaut, l'éditeur tient dans une fenêtre de 80 * 25 caractères, ce qui est un peu court.

Une astuce permet d'augmenter la taille de l'éditeur :

Tout d'abord, allez cocher l'option Keep original dans le menu Options --> Environnement --> Preferences :

Ce faisant, vous forcez Virtual Pascal à restituer automatiquement au démarrage le nombre de lignes et de colonnes courant. Sauvez les options avant de quitter : menu Options --> Save options.

Ensuite, créez un petit script de lancement (appelé par exemple VP.cmd).

Si vous travaillez sous Windows 95/98/Me :

@Echo off
MODE CON COLS=132 LINES=70
C:
CD \Program Files\Vp21\Bin.w32
VP

Si vous travaillez sous Windows NT/2000/XP :

@Echo off
C:
CD \WINNT\System32
MODE CON COLS=132 LINES=70
C:
CD \Program Files\Vp21\Bin.w32
VP

Bien entendu, il se peut que vous ayez à spécifier un autre dossier de lancement de Virtual Pascal ou que vous souhaitiez configurer l'éditeur avec une autre résolution. Il suffit d'exécuter une fois le script de commande pour que Virtual Pascal enregistre la taille de l'éditeur et la restitue automatiquement à l'avenir.

Pour terminer, éditez le raccourci de Virtual Pascal :

1. Options d'affichage : fenêtre

2. Taille du curseur : moyenne

3. Options d'édition : décochez le tout (sinon la souris sera inopérante)

I-D-3-e - Vitesse de clignotement du curseur

Le curseur de texte de l'éditeur est parfois difficile à voir lorsque l'on scrolle dans la fenêtre. Pour améliorer sa visibilité, il suffit de régler le clignotement du curseur sur une vitesse plus élevée (Panneau de configuration de Windows --> Clavier --> Vitesse de clignotement du curseur).

I-D-3-f - Coloration syntaxique

Vous pouvez définir vous-même les couleurs utilisées dans l'éditeur de Virtual Pascal. Le fait de pouvoir aisément différencier les commentaires, les chaînes de caractères, les inclusions de code Assembleur, etc, procure un confort non négligeable.
Pour accéder aux définitions de couleurs : Options --> Environment --> Colors --> Syntax.

I-E - Codes ANSI des caractères accentués

Le texte de l'éditeur de Virtual Pascal 2.1 est codé en ASCII. Cela pose un problème lorsque l'on définit des chaînes de caractères destinées à être affichées dans un environnement Windows, qui attend un codage ANSI : les caractères accentués ne s'affichent pas correctement.
Par contre, le codage ASCII est parfait pour un programme console.

Tout au long de ce tutoriel, nous coderons les caractères accentués à l'aide de la directive # :

StrCopy(Chaine,'Les caract'#232'res accentu'#233's sont p'#233'nibles '#224' utiliser');

Voici un tableau reprenant les codes des caractères accentués les plus usités :

Caractère

Code ANSI

à

#224

â

#226

ä

#228

ç

#231

è

#232

é

#233

ê

#234

ë

#235

î

#238

ï

#239

ô

#244

ö

#246

ù

#249

û

#251

ü

#252

©

#169

®

#174

°

#176

II - Les chaînes à zéro terminal

Les chaînes à zéro terminal (AZT en français, ASCIIZ en anglais) sont la norme dans l'API Windows. Comme vous allez en déguster à toutes les sauces tout au long du tutoriel, il est essentiel que vous en maîtrisiez la représentation en mémoire ainsi que les subtilités du langage Pascal qui les concernent.
Une bonne approche de la problématique des chaînes à zéro terminal est déjà à votre disposition : Les Strings et pChar en Delphi par Adrien Reboisson.

II-A - Tableaux de caractères et type pChar

Une chaîne AZT est un tableau (à 1 dimension, un vecteur donc) de caractères terminé par 0.

Le 0 terminal est le caractère #0 (dont le code ANSI vaut 0), à ne pas confondre avec le caractère '0' (dont le code ANSI vaut 48) !

Voici la représentation d'une chaîne AZT en mémoire :

|'B'|'o'|'n'|'j'|'o'|'u'|'r'| 0 |

Théoriquement, une chaîne AZT n'a aucune limitation de taille, si ce n'est celle de la mémoire du système.
Sa déclaration est celle d'un tableau de caractères :

Var Chaine : Array [0..10] of Char;

La variable Chaine permet de stocker 10 caractères + le zéro terminal.

En C, il n'y a aucune différence entre un tableau en mémoire et un pointeur vers ce tableau. En Pascal, si ! Sauf pour les chaînes AZT. Un type particulier de pointeur, pChar, permet l'adressage des chaînes AZT et le passage de chaînes AZT comme paramètres aux fonctions de l'API Windows.

1ère notion à retenir : Tableau de caractères et pointeur sur un tableau de caractères sont équivalents.

Il est donc tout-à-fait correct d'écrire les deux instructions suivantes :

Var Chaine : Array [0..10] of Char;
pChaine : pChar;
Begin
(* Les 2 instructions suivantes font la même chose : *)
pChaine := Chaine;
pChaine := @Chaine;
End.

Ces deux instructions font exactement la même chose : assigner à pChaine l'adresse de Chaine.

Autre illustration de l'équivalence entre tableau de caractères et pointeur pChar :

Var Chaine : Array [0..10] of Char;
pChaine : pChar;
Caractere : Char;
Begin
pChaine := Chaine;
(* Les 2 instructions suivantes font la même chose : *)
Caractere := Chaine[6];
Caractere := pChaine[6];
End.

Toujours selon le même principe, voici un exemple de passage d'une chaîne AZT comme paramètre à une procédure :

Var Chaine : Array [0..10] of Char;
pChaine : pChar;

Procedure TRAITEMENT (p : pChar);
Begin
{ Traitement }
End;

Begin
pChaine := Chaine;
(* Les 2 instructions suivantes font la même chose : *)
TRAITEMENT(Chaine);
TRAITEMENT(pChaine);
End.

2ème notion à retenir : Les constantes tableaux de caractères sont elles-aussi équivalentes aux pointeurs pChar.
3ème notion à retenir : Les paramètres de type chaîne AZT sont passés aux procédures et fonctions par adresse.

Nous pouvons donc aussi passer une constante tableau de caractères comme paramètre à une procédure :

TRAITEMENT('hello');

Cette dernière instruction stocke la chaîne de caractères dans le segment de données du programme et passe son adresse à la procédure. Il est important de bien comprendre ce principe.

Il se passe exactement la même chose lorsqu'on assigne une constante tableau de caractères à notre pointeur pChar :

pChaine := 'tagada tsoin tsoin';

La chaîne est stockée dans le segment de données et son adresse est affectée à pChaine. La chaîne n'est donc aucunement recopiée.

II-A-1 - En résumé

- Tableau de caractères (Array of Char) et pointeur pChar sont équivalents;

- Constante tableau de caractères ('Bonjour') et pointeur pChar sont équivalents;

- Les paramètres de type chaîne AZT sont passés aux procédures et fonctions par adresse.

II-B - Opérations sur les chaînes à zéro terminal

L'unité Strings de Virtual Pascal propose pas mal de routines de gestion des chaînes AZT :

Unit Strings;

Interface

Function StrLen (Str : pChar) : Word;
(* Retourne la longueur d'une chaîne (0 terminal non compris) *)

Function StrCopy (Dest, Source : pChar) : pChar;
(* Copie Source dans Dest *)
Function StrECopy (Dest, Source : pChar) : pChar;
(* Copie Source dans Dest et retourne un pointeur sur le 0 terminal *)
Function StrLCopy (Dest, Source : pChar; MaxLen : Word) : pChar;
(* Copie MaxLen caractères de Source vers Dest *)
Function StrCat (Dest, Source : pChar) : pChar;
(* Ajoute Source à Dest *)
Function StrLCat (Dest, Source : pChar; MaxLen : Word) : pChar;
(* Ajoute Source à Dest dans la limite de MaxLen caractères *)
Function StrMove (Dest, Source : pChar; Count: Word) : pChar;
(* Transfère Count caractères de Source vers Dest *)

Function StrComp (Str1, Str2 : pChar) : Integer;
(* Compare deux chaînes *)
Function StrIComp (Str1, Str2 : pChar) : Integer;
(* Compare deux chaînes sans tenir compte des majuscules/minuscules *)
Function StrLComp (Str1, Str2 : pChar; MaxLen : Word) : Integer;
(* Compare deux chaînes sur une longueur de MaxLen caractères *)
Function StrLIComp (Str1, Str 2: pChar; MaxLen : Word) : Integer;
(* Compare deux chaînes sur une longueur de MaxLen caractères
sans tenir compte des majuscules/minuscules *)

Function StrPos (Str1, Str2 : pChar) : pChar;
(* Retourne l'adresse de la 1ère occurence de Str2 au sein de Str1 *)
Function StrScan (Str : pChar; Chr : Char) : pChar;
(* Retourne l'adresse de la 1ère occurence du caractère Chr dans Str *)
Function StrRScan (Str : pChar; Chr : Char) : pChar;
(* Retourne l'adresse de la dernière occurence du caractère Chr dans Str *)
Function StrEnd (Str : pChar) : pChar;
(* Retourne un pointeur sur le 0 terminal d'une chaîne *)

Function StrUpper (Str : pChar) : pChar;
(* Convertit une chaîne en majuscules *)
Function StrLower (Str : pChar) : pChar;
(* Convertit une chaîne en minuscules *)

Function StrPas (Str : pChar) : String;
(* Convertit une chaîne AZT en String *)
Function StrPCopy (Dest : pChar; Source : String) : pChar;
(* Copie une String dans une chaîne AZT *)

Function StrNew (Str : pChar) : pChar;
(* Alloue dynamiquement une copie de chaîne et renvoie son adresse *)
Procedure StrDispose (Str : pChar);
(* Désalloue une chaîne allouée par StrNew *)

Passons en revue les opérations les plus courantes. Plusieurs des routines présentées ci-après possèdent des variantes, que nous aborderons en temps voulu tout au long du tutoriel.

II-B-1 - Assigner une valeur à une chaîne

Pour assigner une valeur à une chaîne AZT, on utilise la fonction StrCopy :

StrCopy(Chaine,'Bonjour');

Physiquement, la chaîne 'Bonjour' est copiée dans la chaîne AZT. Il y a donc bien deux exemplaires de la chaîne à l'arrivée !

Il peut être tentant, par analogie avec les Strings, d'assigner une valeur à une chaîne AZT comme ceci :

Chaine := 'Bonjour';   (* Erreur de syntaxe ! *)

Mais le compilateur rejettera cette syntaxe ! Il faut employer StrCopy.

II-B-2 - Initialiser une chaîne vide

Pour initialiser une chaîne vide, c'est tout simple : il suffit qu'elle commence par le zéro terminal !

Chaine[0] := #0;

II-B-3 - Tronquer une chaîne

De la même manière, on peut tronquer une chaîne en y insérant un zéro terminal :

Chaine[5] := #0;

II-B-4 - Déterminer la longueur d'une chaîne

Pour déterminer la longueur d'une chaîne AZT, on utilise la fonction StrLen :

Longueur := StrLen(Chaine);

Cette fonction compte les caractères de la chaîne jusqu'au zéro terminal.

II-B-5 - Copier une chaîne

StrCopy sert également à copier une chaîne 1 dans une chaîne 2 :

StrCopy(Chaine2,Chaine1);

Retenez bien l'ordre des paramètres : destination avant source !
Après exécution de cette instruction, il y a bien deux exemplaires de la chaîne de départ.

Par contre (pour vous torturer un peu les méninges), combien y a-t-il d'exemplaires de la chaîne 1 après exécution de l'instruction suivante ?

Chaine2 := Chaine1;

Réponse : un seul exemplaire, bien sûr ! Cette instruction ne fait que copier l'adresse de la chaîne 1 dans la chaîne 2 et donc les deux pointeurs pointent sur la même chaîne en mémoire.

II-B-6 - Comparer deux chaînes

Pour comparer deux chaînes AZT, vous avez le choix entre StrComp (qui tient compte des majuscules et minuscules) et StrIComp (qui n'en tient pas compte).
La valeur retournée par ces deux fonctions peut être positive, négative ou nulle :

Var Chaine1 : Array [0..40] of Char;
Chaine2 : Array [0..30] of Char;
Resultat : Integer;

Resultat := StrIComp(Chaine1,Chaine2);
if Resultat > 0
then
WriteLn('Chaine1 > Chaine2')
else
if Resultat < 0
then
WriteLn('Chaine1 < Chaine2')
else
WriteLn('Chaine1 = Chaine2');

II-B-7 - Déterminer la position d'une chaîne dans une autre

Pour déterminer la position d'une chaîne dans une autre, on utilise StrPos.
Alors que la fonction Pos (pour les chaînes à attribut de longueur du Pascal) renvoie un indice, la fonction StrPos retourne un pointeur.

Un petit exemple :

Program POSITION;

Uses WinCRT,Strings;

Var Chaine : Array [0..40] of Char;
Extrait : Array [0..15] of Char;
Adresse : pChar;

Begin
StrCopy(Chaine,'Salut la compagnie !');
StrCopy(Extrait,'compagnie');
Adresse := StrPos(Chaine,Extrait);
WriteLn(Adresse);
WriteLn('Indice de l''extrait : ',Adresse - Chaine);
End.

Ne tenez pas compte pour l'instant de la déclaration de l'unité WinCRT : elle fera l'objet du chapitre suivant. Compilez et exécutez ce petit programme; vous obtiendrez quelque chose comme ceci :

Tout cela mérite quelques explications.
Tout d'abord, la variable Adresse retournée par l'instruction

Adresse := StrPos(Chaine,Extrait);

porte bien son nom puisqu'elle contient l'adresse de la chaîne 'compagnie' dans la chaîne 'Salut la compagnie !'. Pour bien le prouver, l'instruction

WriteLn(Adresse);

affiche à l'écran : compagnie !, c'est-à-dire à partir de l'adresse Adresse jusqu'au zéro terminal.

Enfin, notre dernière instruction soustrait l'adresse de la chaîne de départ à l'adresse trouvée par StrPos pour donner l'indice de l'extrait. Non, il n'y a pas d'erreur : l'indice du 1er caractère est 0 et non 1 comme pour les chaînes à attribut de longueur : l'indice affiché est bien 9 et non 10.

II-B-8 - Concaténer deux chaînes

Pour concaténer deux chaînes AZT, il existe l'instruction StrCat :

Var Chaine1 : Array [0..40] of Char;
Chaine2 : Array [0..30] of Char;

StrCat(Chaine1,Chaine2);

L'instruction ci-dessus ajoute le contenu de Chaine2 à Chaine1.

Que se passe-t-il si le contenu des deux chaînes mis bout à bout dépasse la taille limite de Chaine1 ? Un beau plantage à courte échéance !

Retenez bien ceci : la plupart des routines de l'unité Strings n'effectuent aucune vérification de débordement !

Il incombe donc au programmeur de prendre ses précautions. Dans le cas présent, une de ces précautions peut être de choisir la variante StrLCat :

Var Chaine1 : Array [0..40] of Char;
Chaine2 : Array [0..30] of Char;

StrLCat(Chaine1,Chaine2,SizeOf(Chaine1) - 1);

Traduite en langage courant, cette instruction signifie : "ajouter à Chaine1 le contenu de Chaine2 en veillant à ne pas dépasser la taille maximale de Chaine1".

II-B-9 - Conversion entre chaînes AZT et Strings

Il est possible de convertir une chaîne AZT en String à l'aide de la fonction StrPas :

Program CONVERS1;

Uses Strings;

Var s : String [7];
c : Array [0..7] of Char;

Begin
StrCopy(c,'Bonjour');
s := StrPas(c);
End.

La conversion inverse se fait avec la fonction StrPCopy :

Program CONVERS2;

Uses Strings;

Var s : String [7];
c : Array [0..7] of Char;

Begin
s := 'Bonjour';
StrPCopy(c,s);
End.

II-B-10 - Allocation dynamique de chaînes AZT

L'allocation dynamique de chaînes AZT permet de tirer parti de la quantité de mémoire disponible dans le tas global de Windows, bien supérieure à la capacité d'un segment de données ou d'un segment de pile.
La fonction StrNew permet d'allouer une chaîne AZT :

Var p : pChar;

p := StrNew('Allocation dans le tas');

Cette instruction alloue l'espace nécessaire dans le tas, y copie la chaîne et retourne l'adresse du bloc alloué. Il y a donc deux copies de la chaîne : une dans le segment de données et l'autre dans le tas.

Le principe de l'allocation dans le tas est économe en mémoire car n'est alloué que l'espace nécessaire pour stocker une copie de chaîne avec le zéro terminal.
Illustration :

Var Chaine : Array [0..255] of Char;
p : pChar;

Begin
StrCopy(Chaine,'Allocation dans le segment de donn'#233'es');
p := StrNew(Chaine);
WriteLn(p);
StrDispose(p);
End.

StrNew alloue dans le tas l'espace nécessaire pour stocker non pas 255 caractères (+ le zéro terminal) mais seulement 36 caractères (+ le zéro terminal) !
Notez que si la longueur de la chaîne est nulle ou s'il n'y a plus assez de mémoire dans le tas pour allouer la chaîne, StrNew renverra Nil.
Remarquez que p peut parfaitement être utilisé avec l'instruction WriteLn pour afficher la chaîne allouée dans le tas.

La fonction qui permet de libérer dans le tas l'espace alloué à une chaîne AZT est StrDispose.

III - Création de petits programmes avec l'unité WinCRT

L'unité WinCRT permet d'utiliser une fenêtre Windows un peu comme un écran DOS; elle est toutefois très limitée et ne pourra raisonnablement servir que pour faire de tout petits programmes. Elle mérite cependant que nous nous y attardions un peu.

Il ne faut pas confondre la fenêtre générée par l'unité WinCRT avec la console de Windows ! Il ne s'agit nullement ici d'émulation de MS-DOS mais bien d'un programme Windows à part entière !

III-A - Déclarations

Voici la déclaration des variables, constantes et routines de l'unité WinCRT :

Const
WindowOrg : tPoint = (X : cw_UseDefault; Y : cw_UseDefault);
(* Coordonnées de la fenêtre *)
WindowSize : tPoint = (X : cw_UseDefault; Y : cw_UseDefault);
(* Taille de la fenêtre *)
ScreenSize : tPoint = (X : 80; Y : 25);
(* Taille de l'écran virtuel en caractères *)
Cursor : tPoint = (X : 0; Y : 0);
(* Coordonnées du curseur *)
Origin : tPoint = (X : 0; Y : 0);
(* Coordonnées de la zone client de la fenêtre *)
InactiveTitle : pChar = '(Inactive %s)';
(* Format du titre de la fenêtre inactive *)
AutoTracking : Boolean = True;
(* Défilement automatique ? *)
CheckEOF : Boolean = False;
(* Ctrl-Z marque la fin de fichier ? *)
CheckBreak : Boolean = True;
(* Ctrl-C interrompt l'exécution ? *)

(* ---------- PROCEDURES ET FONCTIONS ---------- *)

Procedure InitWinCrt;
(* Création de la fenêtre *)
Procedure DoneWinCrt;
(* Destruction de la fenêtre *)

Procedure AssignCrt (var F : Text);
(* Assigne un fichier texte à la fenêtre WinCRT (entrées/sorties) *)

Function KeyPressed : Boolean;
(* Indique si une touche a été pressée *)
Function ReadKey : Char;
(* Lecture d'un caractère sans écho *)
Function ReadBuf (Buffer : PChar; Count : Word) : Word;
(* Lecture d'une chaîne - retourne le nombre de caractères *)

Procedure WriteBuf (Buffer : PChar; Count : Word);
(* Ecriture d'une chaîne *)
Procedure WriteChar (Ch : Char);
(* Ecriture d'un caractère - appelle WriteBuf en réalité *)

Procedure GotoXY (X, Y : Integer);
(* Déplacement du curseur *)
Procedure CursorTo (X, Y : Integer);
(* Déplacement du curseur *)
Procedure ScrollTo (X, Y : Integer);
(* Défilement de la fenêtre - X et Y deviennent (0,0) *)
Function WhereX : Integer;
(* Retourne l'abscisse du curseur *)
Function WhereY : Integer;
(* Retourne l'ordonnée du curseur *)
Procedure TrackCursor;
(* Défilement de la fenêtre pour rendre le curseur visible *)

Procedure ClrScr;
(* Efface la fenêtre entière *)
Procedure ClrEol;
(* Efface la ligne courante à partir de la position du curseur *)

Nous n'allons bien sûr pas passer chacune de ces routines en revue. Nous allons prendre un cas concret qui nous permettra d'utiliser les plus courantes d'entre elles. Par la même occasion, nous rencontrerons de nombreuses utilisations des procédures et fonctions de l'unité Strings.

III-B - Programme de démonstration : DIRTOT.PAS

Le programme DIRTOT affiche le nom de tous les fichiers .PAS contenus dans un dossier ainsi que dans ses sous-dossiers.
Une procédure récursive, appelée RECHERCHE_RECURSIVE, affiche le nom de tous les fichiers du dossier passé comme paramètre et s'exécute elle-même pour chacun des sous-dossiers rencontrés. A part l'utilisation de la récursivité, la structure générale du programme ne présente rien de particulier.

Voici le source du programme :

Program DIRTOT;

(* Démonstration des unités Strings et WinCRT.
Recherche récursive et affichage du contenu d'une arborescence de dossiers.

Réalisé par Alcatîz pour Developpez.com - 14-05-2006 *)

Uses WinCRT, Strings, Windows;

Const Masque = '\*.*';
(* Masque de nom de fichier à rechercher *)
Extension = '.PAS';
(* Extension des fichiers à rechercher *)

Var BufferChemin : Array [0..MAX_PATH] of Char;
(* Chaîne de construction d'un chemin *)

Procedure RECHERCHE_RECURSIVE (Dossier : pChar);
(* Procédure récursive de recherche de tous les fichiers dans un dossier *)
Var hRecherche : tHandle;
(* Handle de la session de recherche *)
FindData : tWin32FindData;
(* Structure complétée par FindFirstFile *)
NomDossier : pChar;
(* Nom du dossier actuel sauvegardé sur le tas *)
Chemin : pChar;
(* Chemin complet de recherche sauvegardé sur le tas *)
ContinuerRecherche : LongBool;
(* Indique si la recherche peut être poursuivie *)
Begin
WriteBuf(Dossier,StrLen(Dossier));
WriteBuf(#10#13,2);
// WriteLn(Dossier);
NomDossier := StrNew(Dossier);   (* Sauvegarde du nom de dossier sur le tas *)
StrCopy(BufferChemin,Dossier);
StrCat(BufferChemin,Masque);
Chemin := StrNew(BufferChemin);   (* Sauvegarde du chemin complet sur le tas *)
hRecherche := FindFirstFile(Chemin,FindData);
if hRecherche <> Invalid_Handle_Value
then
begin
ContinuerRecherche := True;
while ContinuerRecherche do
begin
if (FindData.dwFileAttributes and File_Attribute_Directory) <> 0
then   (* Il s'agit d'un nom de dossier : récursion *)
begin
if FindData.cFileName[0] <> '.'
then
begin
StrCopy(BufferChemin,NomDossier);
if BufferChemin[StrLen(BufferChemin) - 1] <> '\'
then
StrCat(BufferChemin,'\');
StrCat(BufferChemin,FindData.cFileName);
(* Traitement du sous-dossier *)
RECHERCHE_RECURSIVE(BufferChemin);
end;
end
else   (* Il s'agit d'un fichier : affichage de son nom *)
if StrPos(StrUpper(FindData.cFileName),Extension) <> Nil
then
begin
CursorTo(3,Cursor.Y);
WriteBuf(FindData.cFileName,StrLen(FindData.cFileName));
WriteBuf(#10#13,2);
// WriteLn('   ',FindData.cFileName);
StrCopy(BufferChemin,NomDossier);
StrCat(BufferChemin,'\');
StrCat(BufferChemin,FindData.cFileName);
end;
ContinuerRecherche := FindNextFile(hRecherche,FindData);
end;
FindClose(hRecherche);
end;
(* Désallocation des chaînes allouées dynamiquement *)
if NomDossier <> Nil
then
StrDispose(NomDossier);
if Chemin <> Nil
then
StrDispose(Chemin);
End;

(* ========== Programme principal ========== *)

BEGIN
CheckBreak := False;
ScreenSize.X := 128;
StrCopy(WindowTitle,'D'#233'monstration de WinCRT et Strings');
InitWinCRT;
StrCopy(BufferChemin,'C:\');
RECHERCHE_RECURSIVE(BufferChemin);
WriteBuf(#10#13,2);
// WriteLn;
WriteBuf('Pressez une touche : ',22);
// Write('Pressez une touche : ');
ReadKey;
DoneWinCRT;
END.

III-B-1 - Variables

Avant tout, il est utile d'expliquer la logique des déclarations de données dans notre programme.
Les variables locales d'une fonction sont réservées dans le segment de pile. Comme nous utilisons la récursivité, nous nous trouvons dans le cas où la fonction RECHERCHE_RECURSIVE va s'appeler elle-même, très probablement sur plusieurs niveaux (correspondant aux niveaux de l'arborescence des dossiers). A chaque appel, outre le dépôt des paramètres sur la pile, l'espace nécessaire aux variables locales est réservé. Par sécurité, pour éviter un éventuel débordement de pile (quoique peu probable), nous réduisons au strict minimum les variables locales et privilégions l'allocation de chaînes dynamiques dans le tas global (routines StrNew et StrDispose, voir ).

La seule variable globale est la chaîne qui va servir à construire les chemins tout au long de l'exécution du programme :

Var BufferChemin : Array [0..MAX_PATH] of Char;

Il s'agit d'une chaîne AZT dont la longueur doit être suffisante pour recevoir n'importe quel nom de chemin. La constante MAX_PATH, déclarée dans l'unité Windows (voir immédiatement après), correspond à cette taille maximale.

III-B-2 - L'unité Windows

Outre les unités Strings et WinCRT, l'unité Windows est utilisée. Cette unité contient la grande majorité des déclarations de types, constantes, procédures et fonctions du SDK de Windows (Software Development Kit, kit de développement d'applications). Si vous avez programmé avec Borland Pascal for Windows, vous vous rappelez certainement que vous déclariez les unités WinTypes et WinProcs; eh bien, l'unité Windows de Virtual Pascal est en quelque sorte la réunion de ces deux unités.

Pourquoi déclarons-nous ici l'unité Windows ? Parce que nous avons besoin de fonctions de l'API :

- FindFirstFile (recherche de la 1ère coccurence d'un fichier)

- FindNextFile (recherche de l'occurence suivante)

- FindClose (fermeture de la session de recherche)

Ces fonctions nécessitent un paramètre de type tWin32FindData, dont vous pouvez trouver tous les détails dans la documentation du SDK (structure WIN32_FIND_DATA).
Le principe est à peu près identique aux fonctions de recherche de fichiers de l'unité DOS de Turbo Pascal qui, elles, nécessitaient un paramètre de type SearchRec.

III-B-3 - Initialisation de la fenêtre WinCRT

Au début du programme principal, nous assignons la variable CheckBreak à False, pour éviter que le programme puisse être interrompu par un Ctrl-Alt-Del ou un Ctrl-C. Ensuite, nous décidons d'élargir la fenêtre : 128 caractères au lieu des 80 par défaut. Pour cela, nous assignons 128 au champ X de la variable ScreenSize. Avant d'afficher la fenêtre, nous lui donnons un titre en assignant une chaîne à la variable WindowTitle à l'aide de la fonction StrCopy (voir ).
Une fois que tout est correctement initialisé, nous pouvons afficher la fenêtre à l'aide de la procédure InitWinCRT.

Immédiatement, la procédure RECHERCHE_RECURSIVE est appelée avec, comme paramètre, le dossier racine C:\.

III-B-4 - La procédure récursive de recherche

III-B-3-a - Affichage du nom de dossier

La première chose que fait la procédure RECHERCHE_RECURSIVE est d'afficher le nom du dossier en cours, dont l'adresse, de type pChar, lui est passée comme paramètre. Pour afficher le nom de dossier à l'écran, nous utilisons la procédure WriteBuf :

WriteBuf(Dossier,StrLen(Dossier));
WriteBuf(#10#13,2);

Les paramètres de WriteBuf sont l'adresse de la chaîne à afficher ainsi que sa longueur, que nous déterminons à l'aide de la fonction StrLen (voir ). Comme vous le voyez également, pour passer à la ligne nous affichons un saut de ligne (#10) et un retour chariot (#13).

WriteBuf est une routine beaucoup moins sophistiquée que WriteLn et ne permet pas de profiter des mêmes possibilités (surtout, le nombre quelconque de paramètres). Mais son grand avantage est qu'elle est beaucoup plus rapide !

III-B-3-b - Allocation dynamique de chaînes

A l'intérieur de la procédure récursive, nous avons besoin de sauvegarder le nom de dossier actuel ainsi que le chemin de recherche complet. Comme nous l'avons dit en introduction, nous préférons l'allocation dynamique sur le tas à l'utilisation de variables locales. Plus exactement, ce ne sont pas les chaînes de caractères qui sont déclarées comme variables locales mais uniquement leur adresse, de type pChar.

NomDossier := StrNew(Dossier);   (* Sauvegarde du nom de dossier sur le tas *)
StrCopy(BufferChemin,Dossier);
StrCat(BufferChemin,Masque);
Chemin := StrNew(BufferChemin);   (* Sauvegarde du chemin complet sur le tas *)

La fonction StrNew alloue exactement l'espace nécessaire sur le tas et y copie la chaîne.

Pour construire le chemin complet de recherche, nous devons concaténer le nom de dossier courant et le masque du nom de fichier (dans ce programme, '*.PAS'). La concaténation de chaînes s'effectue à l'aide de la fonction StrCat (voir ).

III-B-4-c - Changement de position du curseur

Lorsqu'un fichier .PAS est trouvé, il est affiché à l'écran. Afin de produire un décalage, nous décidons de l'afficher à la colonne 3 :

CursorTo(3,Cursor.Y);
WriteBuf(FindData.cFileName,StrLen(FindData.cFileName));
WriteBuf(#10#13,2);

La variable Cursor de l'unité WinCRT contient en permanence les coordonnées du curseur à l'écran. Il nous suffit donc de déplacer le curseur à la colonne 3 de la ligne courante, à l'aide de la procédure CursorTo.

Nous aurions pu ici utiliser le classique GotoXY au lieu de CursorTo. Mais la raison est identique à celle qui nous a fait préférer WriteBuf à WriteLn : CursorTo est plus rapide !

III-B-4-d - Libération des chaînes dynamiques

Très important, bien sûr : avant de se terminer, la procédure RECHERCHE_RECURSIVE doit libérer les chaînes qu'elle a allouées dans le tas :

if NomDossier <> Nil
then
StrDispose(NomDossier);
if Chemin <> Nil
then
StrDispose(Chemin);

Les détails de la procédure StrDispose se trouvent dans .

III-B-5 - Terminaison du programme

A la toute dernière ligne du programme se trouve la procédure DoneWinCRT. Faites l'expérience de passer cette ligne en commentaire : lorsque le programme se termine, la fenêtre WinCRT n'est pas détruite :

Le titre est changé en (Inactive ... ) et il faut fermer la fenêtre manuellement. DoneWinCRT est une manière plus propre de fermer la fenêtre WinCRT.

III-C - Téléchargement des sources des programmes de cette page

- Téléchargez le fichier dirtot.zip

IV - Notre premier programme OWL : MINIMUM.PAS

Le temps est venu de créer notre premier vrai programme Windows basé sur OWL.

Comme déjà dit dans les préliminaires, OWL (Object Windows Library) est une bibliothèque d'objets génériques d'interface avec Windows. Chacun des mots soulignés a son importance et appelle des explications :

- objets : pour travailler avec OWL, nous devons utiliser la POO (Programmation Orientée Objet). Si vous n'êtes pas rompu(e) à la POO, vous risquez de ne pas comprendre les explications données et de ne pas pouvoir progresser par la suite. Je vous conseille de vous plonger dans cet excellent tutoriel : Introduction à la programmation orientée objet par Eric Sigoillot.
A l'exception des index de méthodes virtuelles, il n'y a en fait rien de nouveau par rapport à la programmation objet sous Turbo Pascal.

- génériques : les objets de la bibliothèque OWL ont un fonctionnement très basique et tout le travail de programmation consistera à les adapter à vos besoins et à les enrichir par le biais de l'héritage.

- d'interface avec Windows : les objets OWL contiennent essentiellement des routines qui réagissent aux flux de messages Windows. Tels quels, ils fournissent le travail nécessaire au fonctionnement de base d'un programme; vous pouvez modifier ou enrichir ce fonctionnement en implémentant vos propres routines.

Voici le code du plus petit programme fonctionnel possible, qui servira d'ossature pour tous les programmes que nous réaliserons par la suite :

Program MINIMUM;

(* Le programme Windows minimum !

Réalisé par Alcatîz pour Developpez.com - 18-06-2006 *)

Uses OWindows;

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

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

Var Programme : tProgramme;

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

Compilez et exécutez ce programme : vous obtenez une fenêtre Windows standard, que vous pouvez déplacer, agrandir, réduire, maximiser, iconiser et fermer :

Détaillons à présent chacun des éléments du programme.

IV-A - L'unité OWindows

L'unité OWindows contient la déclaration des objets essentiels de la bibliothèque OWL, dont l'objet tApplication et les objets fenêtres (que nous rencontrerons très bientôt). Sa déclaration est absolument indispensable.

IV-B - L'objet tApplication

L'objet tApplication constitue la colonne vertébrale de tout programme OWL. C'est lui qui assure l'intégration du programme dans l'environnement multitâche Windows.

Tous les programmes OWL doivent créer un objet descendant de tApplication et, au minimum, redéfinir la méthode InitMainWindow pour initialiser la fenêtre principale de l'application

Dans le présent programme, nous avons appelé tProgramme le type d'objet descendant et nous avons déclaré comme variable globale une instance de cet objet appelée Programme (oui, c'est très original). C'est généralement la seule instance d'objet qui soit déclarée comme variable globale, toutes les autres étant allouées dynamiquement dans le tas global de Windows.

Vous verrez tout au long du tutoriel qu'en fait, pour la presque totalité des programmes, le bloc principal fera trois lignes :

Begin
Programme.INIT('Nom du programme');
Programme.RUN;
Programme.DONE;
End.

Le constructeur INIT reçoit le nom du programme et se charge de l'initialisation de l'application.
La méthode RUN met en route le processus de traitement du flux de messages Windows.
Enfin, le destructeur DONE effectue les nettoyages nécessaires pour fermer proprement l'application.

IV-C - La méthode InitMainWindow

Grâce au principe de l'héritage, notre objet tProgramme hérite de tous les champs et de toutes les méthodes de l'objet tApplication.
Une de ces méthodes, InitMainWindow, assigne au champ MainWindow de l'objet tApplication l'adresse de la fenêtre principale de l'application. C'est en redéfinissant cette méthode que, tout d'abord, nous choisissons un type de fenêtre principale (vous verrez qu'il y en a plusieurs) et qu'ensuite nous initialisons cette fenêtre.

Observez bien comment la fenêtre principale est allouée :

MainWindow := New(pWindow,INIT(Nil,'Le programme minimum'));

Cette syntaxe de la fonction New pour allouer dynamiquement un objet permet d'exécuter le constructeur immédiatement après l'allocation de l'objet

On peut de la même façon exécuter le destructeur d'un objet avec Dispose :

Dispose(Instance_d_objet,DONE);

IV-D - L'objet fenêtre tWindow

En allouant un pointeur pWindow, nous choisissons comme type de fenêtre principale une fenêtre toute simple, non spécialisée : tWindow. Ce type d'objet est lui-même, dans la bibliothèque OWL, le parent d'autres types de fenêtres comme les fenêtres MDI (multi-documents) et de toute une série de contrôles commes les boutons, les cases à cocher, les boîtes listes, les champs d'édition, etc.
Vous verrez que, dès le programme suivant, nous utiliserons des objets descendants de tWindow comme fenêtres principales.

Voici la déclaration du constructeur INIT de l'objet tWindow :

Constructor INIT (aParent : pWindowsObject; aTitle : pChar);

Le premier paramètre, aParent, est l'adresse de la fenêtre parent de notre fenêtre et le second paramètre, aTitle, le titre qui sera affiché dans la barre de titre.
Notre fenêtre principale n'ayant pas de parent, nous passons la valeur Nil au constructeur. Si nous n'avions voulu aucun titre, nous aurions également transmis Nil comme second paramètre.

IV-E - En résumé

Pour créer un programme OWL :

- On utilise au minimum l'unité OWindows

- On déclare comme variable globale une instance d'un objet descendant de tApplication

- On redéfinit la méthode InitMainWindow dans laquelle on alloue la fenêtre principale

- Dans le programme principal, on exécute successivement les méthodes Init, Run et Done du descendant de tApplication

IV-F - Téléchargement des sources des programmes de cette page

- Téléchargez le fichier minimum.zip

V - Réponses aux messages Windows

V-A - Evénements et messages

Pour comprendre la suite, nous devons aborder (de manière extrêmement simplifiée) le fonctionnement intime de Windows.

Tous les événements qui se produisent au niveau du système sont stockés dans la file d'attente globale de Windows. Partant de là, Windows se charge d'informer les applications concernées par ces événements au moyen de messages.
Chaque application est dotée de sa propre file d'attente et le gros du travail consiste à traiter tous les messages déposés par Windows dans la file d'attente de l'application. C'est ici que la notion de programmation pilotée par événement prend tout son sens.

Pour illustrer la quantité effarante de messages qui parviennent à une application, vous pouvez faire l'expérience de tracer les messages reçus par le programme MINIMUM.EXE (le tout premier programme OWL que nous avons réalisé) à l'aide d'un utilitaire tel que WinSight (fourni avec Delphi). Le fait de démarrer le programme et de déplacer la souris pour aller le fermer génère des milliers de messages, dont voici un minuscule aperçu :

WM_NCMOUSEMOVE (a004X) Dispatched   wp=0000000E  lp=006F0237  (567,111) in TopRight
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=006E0239  (569,110)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=0200000E  MouseMove in TopRight  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=0000000E  lp=006E0239  (569,110) in TopRight
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00700239  (569,112)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=0200000E  MouseMove in TopRight  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=0000000E  lp=00700239  (569,112) in TopRight
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00720239  (569,114)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00720239  (569,114) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00730239  (569,115)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00730239  (569,115) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00740239  (569,116)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00740239  (569,116) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00750238  (568,117)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00750238  (568,117) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00760237  (567,118)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00760237  (567,118) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00770236  (566,119)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00770236  (566,119) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00770235  (565,119)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00770235  (565,119) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00780235  (565,120)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02000014  MouseMove in HT_0014h  hwnd 00060366
WM_NCMOUSEMOVE (a004X) Dispatched   wp=00000014  lp=00780235  (565,120) in HT_0014h
WM_NCHITTEST (8404X) Sent   wp=00000000  lp=00780235  (565,120)
WM_SETCURSOR (2004X) Sent   wp=00060366  lp=02010014  LButtonDown in HT_0014h  hwnd 00060366
WM_NCLBUTTONDOWN (a104X) Dispatched   wp=00000014  lp=00780235  (565,120) in HT_0014h
WM_LBUTTONUP (20204X) Dispatched   wp=00000000  lp=FFF301C3  (451,65523)
WM_CLOSE (1004X) Sent   wp=00000000  lp=00000000
WM_WINDOWPOSCHANGING (4604X) Sent   wp=00000000  lp=0012FA84  (0,0)-(0,0)    Z-Order Top
WM_WINDOWPOSCHANGED (4704X) Sent wp=00000000 lp=0012FA84 (110,110)-(577,719) NoSize, NoMove, NoZOrder, NoActivate, HideWindow Z-Order Unchanged
WM_NCACTIVATE (8604X) Sent   wp=00000000  lp=00000000  Deactivate to hwnd 00000000
WM_ACTIVATE (604X) Sent   wp=00000000  lp=00000000  Deactivate to hwnd 00000000
WM_ACTIVATEAPP (1c04X) Sent   wp=00000000  lp=000005A0  Deactivate to  task 000005A0
WM_KILLFOCUS (804X) Sent   wp=00000000  lp=00000000  Focus to hwnd 00000000
WM_DESTROY (204X) Sent   wp=00000000  lp=00000000
WM_NCDESTROY (8204X) Sent   wp=00000000  lp=00000000

V-B - Structure d'un message

Un message est un enregistrement de type tMessage, dont voici la déclaration :

Type tMessage = Record
Receiver : HWnd;
Message : Word;
case Integer of
0 : (wParam : Word;
lParam : LongInt;
Result : LongInt);
1 : (wParamLo : SmallWord;
wParamHi : SmallWord;
lParamLo : SmallWord;
lParamHi : SmallWord;
ResultLo : SmallWord;
ResultHi : SmallWord);
2 : (wParamLoLo : Byte;
wParamLoHi : Byte;
wParamHiLo : Byte;
wParamHiHi : Byte;
lParamLoLo : Byte;
lParamLoHi : Byte;
lParamHiLo : Byte;
lParamHiHi : Byte;
ResultLoLo : Byte;
ResultLoHi : Byte;
ResultHiLo : Byte;
ResultHiHi : Byte);
end;

Les champs se définissent ainsi :

- Receiver permet d'identifier le destinataire du message

- Message identifie le message lui-même (wm_Destroy, wm_MouseMove, etc)

- Result renseigne sur l'état de traitement du message

- wParam et lParam sont des informations contextuelles

Le contenu des champs wParam et lParam varie d'un type de message à l'autre. Par exemple, pour le message wm_MouseMove, envoyé à une fenêtre lorsque le curseur de souris bouge, le champ wParam indique l'état des touches de contrôle du clavier, le champ lParamLo donne l'abscisse du curseur de la souris et le champ lParamHi donne son ordonnée.
Vous pouvez toujours consulter l'aide du SDK pour connaître le contenu des différents champs d'un message.

V-C - Identificateurs de messages

Sur l'extrait de fichier log créé par l'utilitaire WinSight ci-dessus, vous constatez que tous les messages répertoriés commencent par wm_. Par convention, tous les messages commencent par un code mnémonique suivi d'un underscore (caractère souligné).
Voici les codes mnémoniques les plus courants :

Code mnémonique

Utilisation

wm_

Messages de Windows (Windows messages)

cm_

Identificateurs de commandes (menus et raccourcis clavier)

id_

Identicateurs de contrôles (éléments des boîtes de dialogue)

lb_

Messages des boîtes listes (list box messages)

cb_

Messages des boîtes combo (combo box messages)

em_

Messages des contrôles d'édition (edit messages)

bm_

Messages des boutons (button messages)

Il y en a bien sûr d'autres, que nous passerons sous silence pour le moment puisque nous ne les rencontrerons que beaucoup plus loin dans le tutoriel.

V-D - Réponse aux clics de souris : CLIC.PAS

Nous allons à présent voir comment un programme OWL répond aux messages Windows en prenant comme exemples les messages générés lorsque l'on clique avec la souris dans une fenêtre : wm_LButtonDown pour le bouton gauche (L pour Left) et wm_RButtonDown pour le bouton droit (R pour Right).
Le programme CLIC.PAS va afficher un message chaque fois que l'on cliquera avec le bouton gauche ou le bouton droit dans la fenêtre. Voici le source :

Program CLIC;

(* Réponse aux clics de la souris.

Réalisé par Alcatîz pour Developpez.com - 25-06-2006 *)

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

Type pFenetrePrincipale = ^tFenetrePrincipale;
tFenetrePrincipale = Object(tWindow)
Constructor INIT (aParent : pWindowsObject; aTitle : pChar);
Procedure WMLBUTTONDOWN (var Msg : tMessage);
virtual wm_First + wm_LButtonDown;
Procedure WMRBUTTONDOWN (var Msg : tMessage);
virtual wm_First + wm_RButtonDown;
end;

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

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

Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
(* Initialisation de la taille de la fenêtre principale *)
Begin
tWindow.INIT(aParent,aTitle);
with Attr do
begin
x := 20; y := 20; w := 300; h := 200;
end;
End;

Procedure tFenetrePrincipale.WMLBUTTONDOWN (var Msg : tMessage);
(* Réponse au clic du bouton gauche *)
Begin
MessageBox(hWindow,'Bouton gauche','Clic souris',mb_IconInformation or mb_OK);
End;

Procedure tFenetrePrincipale.WMRBUTTONDOWN (var Msg : tMessage);
(* Réponse au clic du bouton droit *)
Begin
MessageBox(hWindow,'Bouton droit','Clic souris',mb_IconInformation or mb_OK);
End;

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

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

Var Programme : tProgramme;

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

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

Prenez votre souffle car il y a pas mal de notions à découvrir pour comprendre ce programme.

V-D-1 - La zone client de la fenêtre

Si vous testez le programme, vous pouvez essayer de cliquer sur tous les composants de la fenêtre (la barre de titre, les bordures, les boutons...). Vous constatez qu'il n'y a que lorsque vous cliquez dans la "zone intérieure" de la fenêtre que le programme réagit en affichant un message. Cette zone est appelée la zone client de la fenêtre. Vous retrouverez ce mot de vocabulaire dans la suite du tutoriel.

V-D-2 - Objet descendant de tWindow

Contrairement à notre premier programme MINIMUM, nous ne nous contentons plus d'une fenêtre standard qui ne fait rien. Nous créons une fenêtre un peu plus spécialisée qui va réagir aux clics de souris. Pour ce faire, nous profitons du principe d'héritage de la POO pour créer une fenêtre tFenetreprincipale, descendante de la fenêtre standard tWindow, dont elle hérite bien sûr de tous les champs et méthodes. Nous ajoutons simplement les nouvelles fonctionnalités de la fenêtre sous forme de nouvelles méthodes : WMLBUTTONDOWN et WMRBUTTONDOWN.

L'objet tFenetrePrincipale hérite également du constructeur Init de l'objet ancêtre tWindow. C'est ainsi que la création de notre fenêtre principale est similaire à celle du programme MINIMUM :

MainWindow := New(pFenetrePrincipale,INIT(Nil,'Cliquez dans la fen'#234'tre'));

V-D-3 - Méthodes virtuelles dynamiques indexées

OWL fournit une technique de réponse aux messages Windows très simple : les méthodes virtuelles dynamiques indexées. Lorsque l'application reçoit un message Windows, OWL appelle la méthode qui a spécifiquement été écrite pour répondre à ce message.

Pour déclarer une méthode virtuelle dynamique indexée, on termine sa déclaration par le mot clé virtual suivi d'un index de plage et d'un identificateur de message

Procedure WMLBUTTONDOWN (var Msg : tMessage);
virtual wm_First + wm_LButtonDown;

Tous les messages Windows ont été scindés par convention en différentes plages. Chaque plage est réservée à un type de message particulier et correspond à un index de début de plage.
Le tableau suivant reprend tous les index de début de plage :

Bornes de la plage

Type de messages

Index de plage

0000h .. 7FFFh

Messages Windows

wm_First

8000h .. 8FFFh

Messages enfants

id_First

9000h .. 9FFFh

Messages de notification

nf_First

A000h .. FFFFh

Commandes

cm_First

L'usage veut que le nom d'une méthode virtuelle dynamique indexée reflète le nom du message associé

En effet, par convention, il est fortement conseillé de calquer le nom des méthodes sur celui des messages. Ce n'est pas une obligation absolue si vous savez que personne d'autre que vous ne sera amené à relire vos sources mais bon, sachez que cette norme sera appliquée tout au long du tutoriel.
Ainsi, vous savez déjà que la méthode virtuelle dynamique indexée qui répondra au message wm_Size sera nommée WMSIZE.

V-D-4 - L'unité Windows

Lorsque nous avons réalisé notre tout premier programme, nous n'avions eu besoin de déclarer que l'unité OWindows. Pour ce second programme, nous déclarons également l'unité Windows car nous avons besoin de l'API Windows.
En effet, c'est dans cette unité que sont déclarées toutes les constantes de Windows, dont les identificateurs de messages (wm_LButtonDown et wm_RButtonDown). En outre, nous utilisons une fonction de l'API : MessageBox.

V-D-5 - La fonction MessageBox

Cette fonction de l'API très fréquemment utilisée. Elle permet d'afficher un message et, éventuellement, de poser une question à l'utilisateur. Vous pouvez même agrémenter le tout d'une petite icône et d'un avertissement sonore. Remarquez que, grâce à la syntaxe étendue, nous employons la fonction MessageBox comme une procédure, puisque nous n'avons pas besoin de récupérer une valeur en sortie.
Détaillons la manière dont nous utilisons MessageBox dans notre programme :

MessageBox(hWindow,'Bouton gauche','Clic souris',mb_IconInformation or mb_OK);

Le premier paramètre, hWindow, est le handle de la fenêtre de notre application. Tous les objets Windows possèdent un handle, qui est en quelque sorte un identificateur attribué de manière interne par le système.

Dans la plupart des objets OWL, le handle est stocké dans un champ appelé hWindow

Les 2ème et 3ème paramètres de MessageBox sont le texte et le titre du message (dans cet ordre-là), de type chaîne AZT.

Le dernier paramètre est une combinaison logique de flags (drapeaux) permettant à la fois d'afficher une icône - associée à un avertissement sonore - et de décider quels boutons doivent être affichés. L'aspect de la boîte à messages affichée par notre programme est celui-ci :

Le flag mb_IconInformation affiche l'icône d'information et mb_Ok un simple bouton "OK".
Ces drapeaux sont également des constantes de l'API Windows, déclarées dans l'unité Windows.

MessageBox peut également permettre de poser une question à l'utilisateur. Pour illustrer ceci, nous allons apporter une petite modification au programme pour que, lorsque l'on clique avec le bouton droit de la souris dans la zone client de la fenêtre, une boîte à messages nous demande si nous désirons quitter le programme. Remplaçons notre méthode WMRBUTTONDOWN par celle-ci :

Procedure tFenetrePrincipale.WMRBUTTONDOWN (var Msg : tMessage);
(* Réponse au clic du bouton droit *)
Begin
if MessageBox(hWindow,'D'#233'sirez-vous quitter le programme ?','Question',
mb_IconQuestion or mb_YesNo) = id_Yes
then
CloseWindow;
End;

Cette fois, comme nous avons besoin de connaître le choix de l'utilisateur, nous utilisons réellement MessageBox comme une fonction.
Voici à présent l'aspect de la boîte à messages :

Pour afficher le point d'interrogation, nous utilisons le flag mb_IconQuestion; pour afficher les deux boutons "Oui" et "Non", nous utilisons le flag mb_YesNo.

La valeur de retour de MessageBox contient l'identificateur du bouton pressé par l'utilisateur, en l'occurence soit id_Yes soit id_No (également déclarées dans l'unité Windows). Si cette valeur est id_Yes, nous fermons l'application à l'aide de la méthode CloseWindow héritée de tWindow.

Voici un tableau reprenant les flags principaux utilisés par la fonction MessageBox :

Flag

Résultat

Valeurs possibles renvoyées

mb_Ok

Bouton OK

id_Ok

mb_OkCancel

Boutons OK et Annuler

id_Ok ou id_Cancel

mb_YesNo

Boutons Oui et Non

id_Yes ou id_No

mb_YesNoCancel

Boutons Oui, Non et Annuler

id_Yes, id_No ou id_Cancel

mb_AbortRetryIgnore

Boutons Annuler, Réessayer et Ignorer

id_Abort, id_Retry ou id_Ignore

mb_RetryCancel

Boutons Réessayer et Annuler

id_Retry ou id_Cancel

mb_SystemModal

Empêche de swapper vers une autre application

-

mb_IconHand

Icône Stop

-

mb_IconExclamation

Icône Point d'exclamation

-

mb_IconQuestion

Icône Point d'interrogation

-

mb_IconInformation

Icône Information

-

V-E - En résumé

Pour qu'une fenêtre d'application réponde à un message :

- On utilise l'unité Windows pour accéder aux identificateurs de messages

- On utilise une fenêtre descendante de tWindow

- On crée une méthode virtuelle dynamique indexée qui réagit au message

- Par convention, le nom de cette méthode dérive du nom du message

V-F - Téléchargement des sources des programmes de cette page

- Téléchargez le fichier clic.zip

VI - Création de ressources

Les ressources sont des structures binaires stockées à la fin d'un fichier exécutable. En principe, c'est-à-dire sauf spécification contraire lors de leur création, les ressources ne sont chargées en mémoire que lorsque le programme en a besoin.

Il existe plusieurs types de ressources, dont voici les plus courants :

- Menus

- Tables d'accélérateurs (raccourcis clavier)

- Boîtes de dialogue

- Icônes

- Bitmaps

- Curseurs

- Polices de caractères

- Tables de chaînes

- Informations de version

Pas mal d'outils RAD créent des ressources de manière complètement transparente au programmeur. Nous, nous allons les créer de toutes pièces.
Faites l'expérience d'aller visionner les ressources de programmes existants sur votre machine. Vous pouvez utiliser un utilitaire comme Resource Hacker.

On distingue d'un côté le script de ressources, qui est un fichier source avec une syntaxe relativement simple et dont l'extension est .RC, et, de l'autre côté, le fichier binaire, qui est le résultat de la compilation du script de ressources par un compilateur de ressources et dont l'extension est .RES. C'est ce fichier binaire qui est lié à l'exécutable du programme.

En théorie, n'importe quel éditeur de texte peut permettre d'écrire des scripts de ressources. Mais en pratique, il est bien plus confortable de voir ce que l'on fait. Assez curieusement, les éditeurs visuels permettant de créer des scripts de ressources en partant de rien ne courent pas les rues. Dans le présent tutoriel, l'éditeur de ressources utilisé est Borland Resource Workshop 4.5, qui accompagne plusieurs produits de la gamme Borland dont Borland C++, Turbo C++ et Delphi.

Dans la suite du tutoriel, nous utiliserons l'abréviation BRW.

VI-A - Utilisation de Borland Resource Workshop 4.5

Comme toujours, nous allons prendre appui sur un exemple pratique pour découvrir à la fois la création de différents types de ressources et l'utilisation de ces ressources dans un programme.
Le programme que nous allons réaliser contiendra :

- Un menu principal

- Une table d'accélérateurs

- Une icône

- Un curseur personnalisé

- Des bitmaps

Nous nous attaquerons aux boîtes de dialogue beaucoup plus tard dans le tutoriel.

Tout ce que le programme sera capable de réaliser sera d'appliquer comme fond de fenêtre un motif choisi par l'utilisateur. Nous l'appellerons CHGFOND.PAS.

VI-A-1 - Plateforme de destination et génération des identificateurs

La première chose à régler est la plateforme de destination, via le menu File --> Preferences :

Vous avez deviné, il faut bien sûr sélectionner Win32 dans le groupe Target Windows version.

Ensuite, il faut décocher la case Generate identifiers automatically, ce qui nous permettra d'attribuer nous-même aux identificateurs les valeurs qui nous conviennent.

VI-A-2 - Création du projet

Pour créer un nouveau projet, il faut sélectionner le menu File --> New Project. Un petit dialogue de choix apparaît :

BRW permet de créer et d'éditer des curseurs (.CUR), des images bitmap (.BMP), des icônes (.ICO) et des polices de caractères (.FNT). Ce qui nous intéresse dans le cas présent, c'est de créer un script complet de ressources et donc nous sélectionnons .RC. L'option .RES ne nous intéresse pas car nous demanderons à BRW de compiler notre script de ressources pour créer un fichier .RES.

Immédiatement après avoir coché OK, un dialogue apparaît :

Ignorez-le.

Avant toute autre chose, nous allons enregistrer notre projet... même s'il est vide. Cela permettra de lui donner un nom et ensuite de lui ajouter certaines choses.
Sélectionnez le menu File --> Save project :

Il est assez logique d'enregistrer le script dans le même répertoire que le programme auquel il est destiné. Nous lui donnons le même nom que le programme : chgfond.

A présent, nous allons demander à BRW de créer automatiquement un fichier .RES. Cela se fait dans le même dialogue que lors du choix de la plateforme de destination, c'est-à-dire par le menu File --> Preferences :

Cochez la case .RES dans le groupe Multi-save. Par défaut, le nom du fichier est déjà indiqué. Pour permettre à Virtual Pascal de lier le fichier .RES au programme, précisez le nom du dossier des ressources Win32 spécifié dans les répertoires par défaut de VP. Sur ma configuration, c'est C:\Progra~1\VP21\Res.W32.

BRW étant un programme fort ancien, n'utilisez que des noms de dossiers au format 8.3 (MS-DOS)

VI-A-3 - Ajout d'un fichier include

Par facilité, nous allons demander à BRW de stocker tous les identificateurs qui seront créés dans un fichier include. Pour ce faire, sélectionnez le menu File --> Add to project :

Sélectionnez le type de fichier INC Pascal constant include et donnez également le même nom que le programme : un fichier chgfond.inc sera enregistré dans le dossier du programme. Répondez ensuite par l'affirmative dans le dialogue de confirmation de création du fichier include.

VI-A-4 - Création de bitmaps

Pour personnaliser le fond de notre fenêtre principale, nous allons créer deux images bitmaps (mais rien ne vous empêche d'en faire plus). Pour l'exemple, il s'agira de deux motifs qui seront automatiquement répétés par Windows pour remplir le fond de fenêtre.

Windows 95 ne supporte pas de motifs de taille supérieure à 8 pixels de côté.
Ce genre de détails est important si vous voulez assurer une compatibilité maximale à vos programmes

Utilisez le menu Resource --> New et, dans la liste des types de ressources possibles, sélectionnez BITMAP :

BRW affiche ensuite le dialogue de choix qui se trouve sur la droite du screenshot. En fait, la question posée revient à savoir si l'image que nous allons créer doit être incluse dans le script de ressource (bouton Source) ou bien si elle doit être enregistrée dans un fichier séparé (bouton Binary). Si la taille de l'image n'est pas très élevée, la première des deux possibilités convient très bien.

VI-A-4-a - Création complète d'une image

Nous allons créer nous-même une petite bitmap; sélectionnez le bouton Source :

Dans ce dialogue, nous devons indiquer les attributs de l'image, c'est-à-dire sa taille et le nombre de couleurs. Par exemple, une image de 15 pixels de côté en 16 couleurs.
Lorsque vous avez pressé OK, la fenêtre d'édition apparaît :

Si l'image de départ est aussi petite que celle du screenshot ci-dessus, vous pouvez l'agrandir via le menu View --> Zoom in ou la combinaison de touches Ctrl-Z.

La sélection d'une couleur dans la palette se fait à l'aide de la souris. Le principe suivant est valable dans toutes les fenêtres d'édition de ressources :

- Bouton gauche pour la couleur d'avant plan - notée FG (ForeGround) sur la palette

- Bouton droit pour la couleur de fond - notée BG (BackGround) sur la palette

A l'aide de l'outil cercle et de l'outil crayon, j'ai dessiné un smiley :

Pour refermer la fenêtre d'édition, pressez Ctrl-F4.

VI-A-4-b - Utilisation d'une image existante

La seconde bitmap, nous n'allons pas la créer nous-même mais utiliser une bitmap existante. Car nous sommes là pour apprendre.
Nous allons ajouter un fichier BMP au projet. Utilisez le menu File --> Add to project, choisissez le type BMP, RLE device-independant bitmap et sélectionnez votre fichier BMP :

Immédiatement, une seconde ressource de type bitmap est créée :

L'est mignon, hein ?

VI-A-5 - Création d'un menu

Nous utilisons toujours le même menu Resource --> New pour créer une nouvelle ressource. Cette fois, sélectionnez le type MENU. Voici la fenêtre d'édition du menu :

L'appellation POPUP correspond au titre d'un menu déroulant. Les commandes qui apparaissent dans la partie déroulante du menu sont appelés MENUITEMS.
Généralement, les menus qui apparaissent dans la barre de menu d'une application sont des popups mais il est possible d'y inclure directement des items. Le fait de les sélectionner provoque non pas le déroulement d'un menu mais bien l'exécution d'une commande.

Le premier popup est créé d'office par BRW et porte le libellé Popup, que nous nous empressons de modifier en &Fichier dans le champ Item text en haut à gauche.

Dans les menus, le caractère & provoque le soulignement de la lettre qui le suit immédiatement et permet d'utiliser cette lettre comme raccourci clavier avec la touche Alt

Dans notre programme, la lettre F de Fichier sera donc soulignée et l'utilisateur pourra accéder au menu Fichier en tapant Alt-F.

A présent, nous allons changer le nom du premier item créé d'office par BRW et qui porte le nom Item. Sélectionnez la ligne MENUITEM "Item" dans la colonne de droite de la fenêtre d'édition. Vous constatez que les propriétés de la colonne de gauche sont adaptées en conséquence :

Inscrivez &Quitter dans le champ Item text. Cette fois, le caractère & permettra d'activer l'item Quitter rien qu'en tapant la lettre Q, lorsque le menu Fichier sera déroulé.

Nous allons à présent nous occuper du contenu du champ Item Id.

Contrairement aux popups, les items doivent obligatoirement recevoir un identificateur. Celui-ci peut être soit une valeur entière, soit une chaîne de caractères. Pour des raisons de performances, nous privilégierons toujours l'utilisation d'une valeur entière

Inscrivez cm_Exit dans le champ Item Id. Rappelez-vous, nous avons déjà abordé le sujet des identificateurs de messages dans le chapitre V.C. Les identificateurs de commandes de menu portent le code mnémonique cm_.

Lorsque vous pressez Enter, BRW vous demande si vous souhaitez créer un nouvel identificateur :

Confirmez à l'aide du bouton Yes. A présent, nous pouvons attribuer une valeur à l'identificateur cm_Exit :

Inscrivez 24340. Pourquoi cette valeur ? Parce que certaines commandes de menu sont universelles dans le monde Windows. Nous allons nous simplifier la vie car l'identificateur cm_Exit est déjà déclaré dans l'unité OWindows comme ayant la valeur 24340. Je vous invite à vérifier la chose en regardant le contenu du fichier OWindows.inc, qui se trouve dans les sources de la bibliothèque OWL.

Un petit tableau comprenant les identificateurs standards les plus utilisés se trouve à la fin de ce chapitre.

A présent, nous allons créer un second popup. Dans la partie droite de la fenêtre, placez la surbrillance sur la directive __End Popup__ puis tapez Ctrl-P. Ce second popup, nous l'appelons F&onds. Comme le caractère & a déjà été affecté à la lettre F du premier popup, il n'est plus question de l'affecter de nouveau à cette même lettre pour le second popup sinon, tout simplement, ce popup sera inaccessible par le raccourci Alt-F : à tous les coups, ce sera le premier popup qui sera appelé. Nous affectons donc le & à une autre lettre, par exemple le O.

De la même manière que lorsque nous avons créé la commande Quitter du menu Fichier, renommez l'item créé par BRW en &Défaut et affectez-lui l'identificateur cm_FondDefaut :

Lorsque vous pressez Enter, BRW vous demande s'il faut créer ce nouvel identificateur; répondez par l'affirmative et donnez lui comme valeur 101.

Pour les identificateurs de commandes non standards, il est recommandé de donner une valeur supérieure à 100

Généralement, donc, les identificateurs commencent à 101. Certaines valeurs inférieures sont utilisées par défaut par Windows et, en les utilisant, vous risqueriez de voir votre programme effectuer ou tenter d'effectuer des actions non désirées.

Laissez la surbrillance sur le nouvel item puis tapez Ctrl-P : vous créez un sous-menu, un sous-popup. Appelez-le &Personnalisés. Modifiez l'item créé par BRW pour qu'il s'appelle Fond &1 et donnez-lui cm_Fond1 comme identificateur, avec la valeur 102. La fenêtre d'édition ressemble maintenant à ceci :

A présent, nous allons créer un nouvel item. Laissez la surbrillance sur l'item Fond 1 et pressez Insert : un nouvel item se crée. Appelez-le Fond &2 et donnez-lui cm_Fond2 comme identificateur, avec la valeur 103.
Rien ne vous oblige bien sûr à vous arrêter là, si vous souhaitez proposer plus de motifs de fonds.

Le menu est maintenant créé; vous pouvez le tester directement en haut de la partie de droite de la fenêtre d'édition.

Pour fermer la fenêtre d'édition, pressez Ctrl-F4. Vous constatez que, dans la partie droite de la fenêtre principale, se trouve la ressource sous forme de script :

Vous voyez que la syntaxe est somme toute relativement simple. Mais le fait d'utiliser un éditeur visuel de ressources permet de voir et tester le résultat beaucoup plus facilement et rapidement.

Voici un petit résumé des combinaisons de touches rapides utilisables pour créer un menu :

- Ctrl-P pour créer un popup

- Insert pour créer un item

- Ctrl-S pour créer une ligne séparatrice

VI-A-5-a - Identificateurs standards de commandes de menu

Voici la liste des commandes de menu standards dans l'environnement Windows les plus utilisées :

Identificateur

Valeur

Commande

cm_FileNew

24329

Fichier --> Nouveau

cm_FileOpen

24330

Fichier -- > Ouvrir

cm_FileSave

24333

Fichier -- > Enregistrer

cm_FileSaveAs

24334

Fichier -- > Enregistrer sous

cm_Exit

24340

Fichier -- > Quitter

cm_TileChildren

24336

Fenêtre --> Mosaïque

cm_CascadeChildren

24337

Fenêtre --> Cascade

cm_EditCut

24320

Edition --> Couper

cm_EditCopy

24321

Edition --> Copier

cm_EditPaste

24322

Edition --> Coller

cm_EditDelete

24323

Edition --> Supprimer

cm_EditUndo

24325

Edition --> Annuler la dernière action

cm_EditFind

24326

Edition --> Rechercher

cm_EditReplace

24327

Edition --> Remplacer

cm_EditFindNext

24328

Edition --> Rechercher suivant

VI-A-6 - Création d'une table d'accélérateurs

A présent, nous allons créer quelques raccourcis clavier, appelés accélerateurs, pour commander les changements de motifs de fond de fenêtre.
Nous pouvons affecter ces accélérateurs à des commandes existantes (déjà reprises dans un menu) ou bien leur affecter de nouvelles commandes. Lorsqu'il sont bien choisis, les accélérateurs constituent une bonne plus-value pour une application.

Evitez d'utiliser des combinaisons de touches reconnues par Windows (comme Alt-F4, Ctrl-Insert, Shift-Insert...)

Exécutez le menu Resource --> New et sélectionnez ACCELERATORS comme type de ressource. Voici la fenêtre d'édition :

Nous allons affecter un raccourci aux trois commandes du menu Fonds. Pour la première, inscrivez cm_FondDefaut dans le champ Command puis pressez la touche de tabulation pour déplacer le curseur vers le champ Key :

BRW attend que vous exécutiez directement la combinaison de touches du raccourci. Par exemple, pressez Ctrl-d. Cliquez avec la souris ou pressez Escape pour stopper l'enregistrement. Pour ajouter un second accélérateur, il suffit de presser Insert et de recommencer le processus. Affectons la combinaisons de touches Ctrl-1 à la commande cm_Fond1 et Ctrl-2 à la commande cm_Fond2 :

C'est simple, non ? Pour fermer la fenêtre d'édition, pressez Ctrl-F4. Vous découvrez le script de la table d'accélérateurs créée :

VI-A-6-a - Ajout des raccourcis dans les menus

Il est très courant d'aider les utilisateurs d'un programme en mentionnant les raccourcis clavier dans les items de menu correspondants.
Retournez dans l'éditeur de menu (en double-cliquant sur la ressource MENU_1). Nous allons compléter le libellé des commandes du menu Fonds en ajoutant la directive \t suivie du raccourci :

La directive \t insère une tabulation de manière que, lorsque le menu est déroulé, tous les raccourcis soient correctement alignés à droite.

VI-A-7 - Création d'une icône

Comme pour toutes les ressources créées jusqu'à présent, utilisez le menu Resource --> New et sélectionnez ICON dans la liste des types :

Tout comme lors de la création de bitmap, BRW vous permet soit d'enregistrer l'icône directement dans le script de ressources, soit dans un fichier séparé (dialogue de droite). Sélectionnez Source.
Vous pouvez à présent choisir la résolution et le nombre de couleurs de la première icône :

Pourquoi ai-je dit "la première" ? Parce que :

Une icône n'est pas une image simple mais peut être composée de plusieurs images de résolutions et nombres de couleurs différents

Windows choisira l'image qui donnera le meilleur résultat à l'affichage.

Les possibilités proposées par BRW en matière de résolution et de nombre de couleurs sont devenues un peu obsolètes. Pour le présent programme, je me suis contenté d'une image 32 X 32 en 16 couleurs dans un but didactique mais vous pouvez très bien expérimenter, en utilisant le même procédé que lors de la création de la seconde bitmap, l'ajout d'un fichier .ICO au projet (menu File --> Add to project).

Non seulement une icône peut être composée de plusieurs images de résolutions et nombres de couleurs différents mais, en plus, chacune des images est constituée de deux bitmaps. La première est appelée masque xor et représente l'image réelle; la seconde est appelée masque and et représente un motif monochrome (0 ou 1) qui définit la transparence de l'icône.

Lors de l'affichage d'une icône, Windows effectue l'opération logique suivante :

Ecran := (Ecran and MasqueAND) xor MasqueXOR

Ainsi, pour chaque pixel affiché : si la valeur du masque and est 1 alors le pixel de l'écran reste visible sinon il est effacé. Ensuite, le pixel devient une combinaison xor avec le point correspondant de l'image de l'icône. Typiquement, le masque and correspond au contour de l'image à afficher mais ce n'est pas obligatoire. Cela permet quelques effets, que rien ne vous empêche également d'expérimenter.

Tous ces détails vont vous aider à comprendre pourquoi il y a une couleur transparente et une couleur inversée (inverted) dans l'éditeur d'icônes de BRW : elles vont servir, de manière interne, à définir le masque and.

L'éditeur d'icônes ressemble comme deux gouttes d'eau à l'éditeur de bitmaps. J'ai de nouveau rassemblé tout mon talent pour dessiner un magnifique smiley rose sur un fond entièrement transparent :

Fermez l'éditeur à l'aide des touches Ctrl-F4 pour retourner à la liste des images qui composent l'icône. Nous allons ajouter une seconde image, forcément d'une résolution différente et/ou d'un nombre de couleurs différent, et nous verrons bien, lorsque le programme sera terminé, laquelle des deux Windows choisira pour obtenir le meilleur rendu possible en créant, par exemple, un raccourci sur le bureau.

Utilisez le menu Images --> New image et créez une nouvelle image. J'ai personnellement choisi une résolution de 64 X 64 pixels en 256 couleurs et ai dessiné un smiley (j'adore les smileys) d'un jaune complexe sur un fond non pas transparent mais inversé. Ce sera moche mais cela illustrera bien ce qui précède.

VI-A-8 - Création d'un curseur

Le dernier type de ressource que nous allons inclure dans notre programme sera un curseur.

Tout comme une icône, un curseur peut contenir plusieurs images de tailles différentes composées de masques and et xor

Le curseur possède cependant un élément supplémentaire : un point actif ou hot spot. C'est un unique pixel dont la position sera considérée par Windows comme la position du curseur tout entier. Par exemple, le hot spot d'une flèche sera le pixel qui constitue sa pointe.

Un curseur avec une image bien choisie peut être une aide précieuse à l'utilisateur de votre programme, lui indiquant soit une action en cours (par exemple, un sablier) soit une action possible à un emplacement particulier (par exemple, une quadruple flèche ou une main qui indique qu'un élément peut être déplacé).

Pour accéder à l'éditeur de curseurs de BRW, sélectionnez le menu Resource --> New puis le type CURSOR. La manoeuvre vous est désormais familière. De nouveau, BRW vous demande si le curseur doit être inclus directement dans le script de ressource (Source) ou bien sous forme de fichier séparé (Binary). La faible taille de la ressource nous permet d'opter sans hésiter pour la première solution.
Vu sa relative obsolescence, BRW ne permet de créer que des curseurs inanimés de 32 X 32 pixels. Vous pouvez ajouter des curseurs plus modernes stockés dans des fichiers .CUR via le menu File --> Add to project.

J'ai dessiné... un smiley (c'est le dernier, promis) nanti d'une petite flèche qui pointe au nord-ouest et dont la pointe sera le hot spot. Pour définir ce dernier, il faut passer par le menu Cursor --> Set hot spot :

Les coordonnées de la pointe de la flèche sont bien entendu (0,0).

VI-A-9 - Attribution d'identificateurs aux ressources

Jusqu'à présent, nous nous sommes contentés de laisser BRW attribuer ses propres identificateurs par défaut aux ressources que nous avons créées. L'identificateur du menu est MENU_1, celui des bitmaps BITMAP_1 et BITMAP_2, etc. Pour notre petit programme, nous allons affecter nos propres identificateurs aux ressources.

Pour ce faire, on surbrille une ressource, on active le menu Resource --> Rename et on inscrit le nom de l'identificateur :

Ensuite, BRW demande si l'on veut créer le nouvel identificateur (affirmatif !) et, enfin, attend la valeur de l'identificateur :

N'utilisez pas de valeurs s'approchant de 32767 car pas mal de ressources internes de Windows ont des identificateurs dans la plage immédiatement inférieure à cette valeur

Le fait d'attribuer une constante non typée à une ressource n'est pas obligatoire. Nous pourrions directement attribuer un identificateur numérique - mais c'est beaucoup moins parlant dans le source d'une application - ou même une chaîne littérale. Mais cette dernière possibilité est à proscrire car moins performante qu'une constante ou un identificateur numérique. Vous comprendrez pourquoi lorsque nous détaillerons le processus de chargement d'une ressource.

Voici les valeurs des identificateurs qui ont été choisis pour le présent programme :

Identificateur

Valeur

id_MenuPrincipal

1

id_IconeFenetrePrincipale

11

id_Accel

21

id_Curseur

31

id_Bitmap1

41

id_Bitmap2

42

A tout moment, vous pouvez accéder à la liste des identificateurs via le menu Resource --> Identifiers :

Dans ce dialogue, vous pouvez ajouter, supprimer ou modifier la valeur d'un identificateur.

Voici à présent le contenu du fichier CHGFOND.INC :

(****************************************************************************

chgfond.inc

produced by Borland Resource Workshop

*****************************************************************************)

const
cm_Exit                   = 24340;
cm_FondDefaut             = 101;
cm_Fond1                  = 102;
cm_Fond2                  = 103;
id_MenuPrincipal          = 1;
id_IconeFenetrePrincipale = 11;
id_Accel                  = 21;
id_Curseur                = 31;
id_Bitmap1                = 41;
id_Bitmap2                = 42;

VI-A-10 - Options de gestion des ressources en mémoire

Le menu Resource --> Memory options (après avoir surbrillé une ressource) vous permet de choisir de quelle manière une ressource est chargée et conservée en mémoire par le système :

Voici les options par défaut :

- Load on call : la ressource ne sera chargée en mémoire qu'en cas de besoin et non lors du chargement de l'exécutable

- Moveable : permet à Windows de déplacer la ressource en mémoire dans le cadre de sa gestion interne

- Discardable : permet à Windows d'expurger la ressource de la mémoire quand elle n'est plus utilisée. Au besoin, elle sera rechargée

- Pure : rend la ressource inaccessible en écriture, ce qui empêche de la modifier en mémoire

Ces options par défaut répondent à une saine politique de gestion de la mémoire système et sont rarement modifiées. Un exemple de raison qui pourrait justifier un changement serait la possibilité pour un programme de modifier une ressource chargée en mémoire. Dans ce cas, les options Discardable et Pure seraient désactivées.

VI-A-11 - Enregistrement et compilation du projet

Avec les préférences que nous avons réglées au début du projet, le fait de sauvegarder celui-ci entraîne automatiquement sa compilation en un fichier .RES (binaires) et .INC (identificateurs), qui seront utilisables directement par Virtual Pascal. Une modification n'est donc pas prise en compte par Virtual Pascal tant que le projet n'a pas été recompilé.

Voici le script complet créé par BRW, stocké dans le fichier CHGFOND.RC :

/****************************************************************************

chgfond.rc

produced by Borland Resource Workshop

*****************************************************************************/

#include "chgfond.inc"
id_Bitmap1 BITMAP
{
'42 4D EE 00 00 00 00 00 00 00 76 00 00 00 28 00'
'00 00 0F 00 00 00 0F 00 00 00 01 00 04 00 00 00'
'00 00 78 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80'
'00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80'
'00 00 80 80 80 00 C0 C0 C0 00 00 00 FF 00 00 FF'
'00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF'
'00 00 FF FF FF 00 FF FF F0 00 00 FF FF F0 FF F0'
'0B BB BB 00 FF F0 FF 0B BB BB BB BB 0F F0 F0 BB'
'BB BB BB BB B0 F0 F0 BB B0 00 00 BB B0 F0 0B BB'
'0B BB BB 0B BB 00 0B B0 BB BB BB B0 BB 00 0B BB'
'BB BB BB BB BB 00 0B BB BB BB BB BB BB 00 0B BB'
'BB BB BB BB BB 00 F0 BB 0B BB BB 0B B0 F0 F0 BB'
'BB BB BB BB B0 F0 FF 0B BB BB BB BB 0F F0 FF F0'
'0B BB BB 00 FF F0 FF FF F0 00 00 FF FF F0'
}
id_Bitmap2 BITMAP "chat.bmp"
id_MenuPrincipal MENU
{
POPUP "&Fichier"
{
MENUITEM "&Quitter", cm_Exit
}

POPUP "F&onds"
{
MENUITEM "&Défaut\tCtrl-D", cm_FondDefaut
POPUP "&Personnalisés"
{
MENUITEM "Fond &1\tCtrl-1", cm_Fond1
MENUITEM "Fond &2\tCtrl-2", cm_Fond2
}

}

}
id_Accel ACCELERATORS
{
"D", cm_FondDefaut, VIRTKEY, CONTROL
"1", cm_Fond1, VIRTKEY, CONTROL
"2", cm_Fond2, VIRTKEY, CONTROL
}
id_IconeFenetrePrincipale ICON
{
'00 00 01 00 02 00 20 20 10 00 00 00 00 00 E8 02'
'00 00 26 00 00 00 40 40 00 01 00 00 00 00 28 16'
'00 00 0E 03 00 00 28 00 00 00 20 00 00 00 40 00'
'00 00 01 00 04 00 00 00 00 00 80 02 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 80 00 00 80 00 00 00 80 80 00 80 00'
'00 00 80 00 80 00 80 80 00 00 80 80 80 00 C0 C0'
'C0 00 00 00 FF 00 00 FF 00 00 00 FF FF 00 FF 00'
'00 00 FF 00 FF 00 FF FF 00 00 FF FF FF 00 00 00'
'00 00 00 00 00 DD DD 00 00 00 00 00 00 00 00 00'
'00 00 00 DD DD DD DD DD DD 00 00 00 00 00 00 00'
'00 00 DD DD DD DD DD DD DD DD 00 00 00 00 00 00'
'00 0D DD DD DD DD DD DD DD DD D0 00 00 00 00 00'
'0D DD DD DD DD 00 00 DD DD DD DD D0 00 00 00 00'
'DD DD DD 00 00 00 00 00 00 DD DD DD 00 00 00 00'
'DD DD D0 00 00 00 00 00 00 0D DD DD 00 00 00 0D'
'DD D0 00 00 00 00 00 00 00 00 0D DD D0 00 00 DD'
'DD D0 00 00 00 00 00 00 00 00 0D DD DD 00 00 DD'
'DD 00 00 00 00 DD DD 00 00 00 00 DD DD 00 0D DD'
'D0 00 00 0D DD DD DD DD D0 00 00 0D DD D0 0D DD'
'D0 00 00 DD D0 00 00 0D DD 00 00 0D DD D0 0D DD'
'D0 00 0D DD 00 00 00 00 DD D0 00 0D DD D0 DD DD'
'D0 00 0D D0 00 00 00 00 0D D0 00 0D DD DD DD DD'
'00 00 0D 00 00 00 00 00 00 D0 00 00 DD DD DD DD'
'00 00 00 00 00 00 00 00 00 00 00 00 DD DD DD DD'
'00 00 00 00 00 00 00 00 00 00 00 00 DD DD DD DD'
'00 00 00 00 00 00 00 00 00 00 00 00 DD DD DD DD'
'D0 00 00 00 00 00 00 00 00 00 00 0D DD DD 0D DD'
'D0 00 00 0D D0 00 00 0D D0 00 00 0D DD D0 0D DD'
'D0 00 00 0D D0 00 00 0D D0 00 00 0D DD D0 0D DD'
'D0 00 00 00 00 00 00 00 00 00 00 0D DD D0 00 DD'
'DD 00 00 00 00 00 00 00 00 00 00 DD DD 00 00 DD'
'DD D0 00 00 00 00 00 00 00 00 0D DD DD 00 00 0D'
'DD D0 00 00 00 00 00 00 00 00 0D DD D0 00 00 00'
'DD DD D0 00 00 00 00 00 00 0D DD DD 00 00 00 00'
'DD DD DD 00 00 00 00 00 00 DD DD DD 00 00 00 00'
'0D DD DD DD DD 00 00 DD DD DD DD D0 00 00 00 00'
'00 0D DD DD DD DD DD DD DD DD D0 00 00 00 00 00'
'00 00 DD DD DD DD DD DD DD DD 00 00 00 00 00 00'
'00 00 00 DD DD DD DD DD DD 00 00 00 00 00 00 00'
'00 00 00 00 00 DD DD 00 00 00 00 00 00 00 FF FC'
'3F FF FF C0 03 FF FF 00 00 FF FE 00 00 7F F8 03'
'C0 1F F0 3F FC 0F F0 7F FE 0F E1 FF FF 87 C1 FF'
'FF 83 C3 FC 3F C3 87 E0 07 E1 87 C7 E3 E1 87 8F'
'F1 E1 07 9F F9 E0 0F BF FD F0 0F FF FF F0 0F FF'
'FF F0 0F FF FF F0 07 FF FF E0 87 E7 E7 E1 87 E7'
'E7 E1 87 FF FF E1 C3 FF FF C3 C1 FF FF 83 E1 FF'
'FF 87 F0 7F FE 0F F0 3F FC 0F F8 03 C0 1F FE 00'
'00 7F FF 00 00 FF FF C0 03 FF FF FC 3F FF 28 00'
'00 00 40 00 00 00 80 00 00 00 01 00 08 00 00 00'
'00 00 00 12 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 80 00 00 80'
'00 00 00 80 80 00 80 00 00 00 80 00 80 00 80 80'
'00 00 C0 C0 C0 00 C0 DC C0 00 F0 CA A6 00 00 20'
'40 00 00 20 60 00 00 20 80 00 00 20 A0 00 00 20'
'C0 00 00 20 E0 00 00 40 00 00 00 40 20 00 00 40'
'40 00 00 40 60 00 00 40 80 00 00 40 A0 00 00 40'
'C0 00 00 40 E0 00 00 60 00 00 00 60 20 00 00 60'
'40 00 00 60 60 00 00 60 80 00 00 60 A0 00 00 60'
'C0 00 00 60 E0 00 00 80 00 00 00 80 20 00 00 80'
'40 00 00 80 60 00 00 80 80 00 00 80 A0 00 00 80'
'C0 00 00 80 E0 00 00 A0 00 00 00 A0 20 00 00 A0'
'40 00 00 A0 60 00 00 A0 80 00 00 A0 A0 00 00 A0'
'C0 00 00 A0 E0 00 00 C0 00 00 00 C0 20 00 00 C0'
'40 00 00 C0 60 00 00 C0 80 00 00 C0 A0 00 00 C0'
'C0 00 00 C0 E0 00 00 E0 00 00 00 E0 20 00 00 E0'
'40 00 00 E0 60 00 00 E0 80 00 00 E0 A0 00 00 E0'
'C0 00 00 E0 E0 00 40 00 00 00 40 00 20 00 40 00'
'40 00 40 00 60 00 40 00 80 00 40 00 A0 00 40 00'
'C0 00 40 00 E0 00 40 20 00 00 40 20 20 00 40 20'
'40 00 40 20 60 00 40 20 80 00 40 20 A0 00 40 20'
'C0 00 40 20 E0 00 40 40 00 00 40 40 20 00 40 40'
'40 00 40 40 60 00 40 40 80 00 40 40 A0 00 40 40'
'C0 00 40 40 E0 00 40 60 00 00 40 60 20 00 40 60'
'40 00 40 60 60 00 40 60 80 00 40 60 A0 00 40 60'
'C0 00 40 60 E0 00 40 80 00 00 40 80 20 00 40 80'
'40 00 40 80 60 00 40 80 80 00 40 80 A0 00 40 80'
'C0 00 40 80 E0 00 40 A0 00 00 40 A0 20 00 40 A0'
'40 00 40 A0 60 00 40 A0 80 00 40 A0 A0 00 40 A0'
'C0 00 40 A0 E0 00 40 C0 00 00 40 C0 20 00 40 C0'
'40 00 40 C0 60 00 40 C0 80 00 40 C0 A0 00 40 C0'
'C0 00 40 C0 E0 00 40 E0 00 00 40 E0 20 00 40 E0'
'40 00 40 E0 60 00 40 E0 80 00 40 E0 A0 00 40 E0'
'C0 00 40 E0 E0 00 80 00 00 00 80 00 20 00 80 00'
'40 00 80 00 60 00 80 00 80 00 80 00 A0 00 80 00'
'C0 00 80 00 E0 00 80 20 00 00 80 20 20 00 80 20'
'40 00 80 20 60 00 80 20 80 00 80 20 A0 00 80 20'
'C0 00 80 20 E0 00 80 40 00 00 80 40 20 00 80 40'
'40 00 80 40 60 00 80 40 80 00 80 40 A0 00 80 40'
'C0 00 80 40 E0 00 80 60 00 00 80 60 20 00 80 60'
'40 00 80 60 60 00 80 60 80 00 80 60 A0 00 80 60'
'C0 00 80 60 E0 00 80 80 00 00 80 80 20 00 80 80'
'40 00 80 80 60 00 80 80 80 00 80 80 A0 00 80 80'
'C0 00 80 80 E0 00 80 A0 00 00 80 A0 20 00 80 A0'
'40 00 80 A0 60 00 80 A0 80 00 80 A0 A0 00 80 A0'
'C0 00 80 A0 E0 00 80 C0 00 00 80 C0 20 00 80 C0'
'40 00 80 C0 60 00 80 C0 80 00 80 C0 A0 00 80 C0'
'C0 00 80 C0 E0 00 80 E0 00 00 80 E0 20 00 80 E0'
'40 00 80 E0 60 00 80 E0 80 00 80 E0 A0 00 80 E0'
'C0 00 80 E0 E0 00 C0 00 00 00 C0 00 20 00 C0 00'
'40 00 C0 00 60 00 C0 00 80 00 C0 00 A0 00 C0 00'
'C0 00 C0 00 E0 00 C0 20 00 00 C0 20 20 00 C0 20'
'40 00 C0 20 60 00 C0 20 80 00 C0 20 A0 00 C0 20'
'C0 00 C0 20 E0 00 C0 40 00 00 C0 40 20 00 C0 40'
'40 00 C0 40 60 00 C0 40 80 00 C0 40 A0 00 C0 40'
'C0 00 C0 40 E0 00 C0 60 00 00 C0 60 20 00 C0 60'
'40 00 C0 60 60 00 C0 60 80 00 C0 60 A0 00 C0 60'
'C0 00 C0 60 E0 00 C0 80 00 00 C0 80 20 00 C0 80'
'40 00 C0 80 60 00 C0 80 80 00 C0 80 A0 00 C0 80'
'C0 00 C0 80 E0 00 C0 A0 00 00 C0 A0 20 00 C0 A0'
'40 00 C0 A0 60 00 C0 A0 80 00 C0 A0 A0 00 C0 A0'
'C0 00 C0 A0 E0 00 C0 C0 00 00 C0 C0 20 00 C0 C0'
'40 00 C0 C0 60 00 C0 C0 80 00 C0 C0 A0 00 F0 FB'
'FF 00 A4 A0 A0 00 80 80 80 00 00 00 FF 00 00 FF'
'00 00 00 FF FF 00 FF 00 00 00 FF 00 FF 00 FF FF'
'00 00 FF FF FF 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3E'
'3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3E'
'3E 3E 3E 3E 3E 3E FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E 3E 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 3E 3E'
'3E 3E 3E 3E 3E FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E 3E 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 3E 3E 3E'
'3E 3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E 00'
'00 00 00 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E'
'00 00 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E'
'3E 00 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E'
'3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E'
'3E 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E'
'3E 3E 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E'
'3E 3E 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E'
'3E 3E 3E 00 00 00 00 00 00 3E 3E 3E 3E 3E FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E'
'3E 3E 3E 00 00 00 00 00 3E 3E 3E 3E 3E 3E FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E'
'3E 3E 3E 3E 00 00 00 00 3E 3E 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E'
'3E 3E 3E 3E 3E FF FF FF 3E 3E 3E 3E 3E 3E 3E FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E'
'3E 3E 3E 3E 00 00 00 00 3E 3E 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E'
'FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E'
'3E 3E 3E 3E 00 00 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF FF 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF 3E 3E 3E FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF 3E FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF 3E 3E FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF 3E 3E FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF 3E FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF 3E FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF 3E 3E FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF 3E 3E FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF 3E FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF 3E FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF 3E FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 00 3E 3E 3E 3E 3E FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'3E 3E 3E 3E 3E 00 00 00 3E 3E 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E'
'3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E'
'3E 3E 3E 3E 00 00 00 00 3E 3E 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF 3E'
'3E 3E 3E 3E 00 00 00 00 3E 3E 3E 3E 3E 3E FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E'
'3E 3E 3E 3E 00 00 00 00 00 3E 3E 3E 3E 3E FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E'
'3E 3E 3E 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E FF'
'FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E'
'3E FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E'
'3E 3E 3E 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E'
'3E 3E 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E'
'3E 3E 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E'
'3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E'
'3E 00 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E'
'3E 00 00 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E'
'3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E'
'00 00 00 00 00 00 00 00 00 00 00 00 00 3E 3E 3E'
'3E 3E 3E FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 3E 3E'
'3E 3E 3E 3E 3E FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E 3E 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3E'
'3E 3E 3E 3E 3E 3E FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF 3E 3E 3E 3E 3E 3E 3E 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 3E'
'3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF FF FF 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF'
'FF 3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E FF FF FF'
'FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E FF FF FF FF FF FF FF FF FF FF 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 3E 3E 3E 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E'
'3E 3E 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 3E 3E 3E'
'3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 3E 3E 3E 3E 3E 3E 3E 3E 3E 3E 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 FF FF FF E0 07 FF FF FF FF FF'
'FE 00 00 7F FF FF FF FF F0 00 00 0F FF FF FF FF'
'C0 00 00 03 FF FF FF FF 00 00 00 00 FF FF FF FC'
'00 1F F8 00 3F FF FF F8 01 FF FF 80 1F FF FF F0'
'07 FF FF E0 0F FF FF E0 1F FF FF F8 07 FF FF 80'
'3F FF FF FC 01 FF FF 80 FF FF FF FF 01 FF FF 01'
'FF FF FF FF 80 FF FE 07 FF FF FF FF E0 7F FC 0F'
'FF FF FF FF F0 3F F8 0F FF FF FF FF F0 1F F8 1F'
'FF FF FF FF F8 1F F0 3F FF FF FF FF FC 0F F0 3F'
'FF FF FF FF FC 0F E0 7F FF FF FF FF FE 07 E0 FF'
'FF F8 0F FF FF 07 C0 FF FF C0 01 FF FF 03 C1 FF'
'FE 01 C0 7F FF 83 C1 FF F8 3F FE 3F FF 83 83 FF'
'F1 FF FF 8F FF C1 83 FF E3 FF FF C7 FF C1 83 FF'
'C7 FF FF E3 FF C1 83 FF 8F FF FF FB FF C1 07 FF'
'3F FF FF F9 FF E0 07 FF 7F FF FF FD FF E0 07 FE'
'7F FF FF FC FF E0 07 FE FF FF FF FE FF E0 07 FF'
'FF FF FF FE FF E0 07 FF FF FF FF FF FF E0 07 FF'
'FF FF FF FF FF E0 07 FF FF FF FF FF FF E0 07 FF'
'FF FF FF FF FF E0 07 FF FF FF FF FF FF E0 83 FF'
'FF FF FF FF FF C1 83 FF FF FF FF FF FF C1 83 FF'
'FF FF FF FF FF C1 83 FF FF FF FF FF FF C1 C1 FF'
'F8 FF FF 1F FF 83 C1 FF F0 7F FE 0F FF 83 C0 FF'
'F0 7F FE 0F FF 03 E0 FF F0 7F FE 0F FF 07 E0 7F'
'F8 FF FF 1F FE 07 F0 3F FF FF FF FF FC 0F F0 3F'
'FF FF FF FF FC 0F F8 1F FF FF FF FF F8 1F F8 0F'
'FF FF FF FF F0 1F FC 0F FF FF FF FF F0 3F FE 07'
'FF FF FF FF E0 7F FF 01 FF FF FF FF 80 FF FF 80'
'FF FF FF FF 01 FF FF 80 3F FF FF FC 01 FF FF E0'
'1F FF FF F8 07 FF FF F0 07 FF FF E0 0F FF FF F8'
'01 FF FF 80 1F FF FF FC 00 1F F8 00 3F FF FF FF'
'00 00 00 00 FF FF FF FF C0 00 00 03 FF FF FF FF'
'F0 00 00 0F FF FF FF FF FE 00 00 7F FF FF FF FF'
'FF E0 07 FF FF FF'
}
id_Curseur CURSOR
{
'00 00 02 00 01 00 20 20 00 00 00 00 00 00 30 01'
'00 00 16 00 00 00 28 00 00 00 20 00 00 00 40 00'
'00 00 01 00 01 00 00 00 00 00 00 02 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 FF FF FF 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00'
'00 00 00 00 00 00 FF FF 87 FF FF F8 78 7F FF F7'
'FF BF FF CF FF CF FF BF FF F7 FF BF FF F7 FF 7F'
'FF FB FE FF 83 FD FE FE 00 FD FE FC 7C 7D FE F9'
'FF 3D FD F3 FF 9E FD F7 FF DE FD FF FF FE FD FF'
'FF FE FE FF FF FD FE FF FF FD FE FE FD FD FE FC'
'78 FD FF 7E FD FB FF BF FF F7 FF BF FF F7 FF 0F'
'FF CF FE 37 FF BF 7C 78 78 7F 78 FF 87 FF 71 FF'
'FF FF 23 FF FF FF 07 FF FF FF 0F FF FF FF 07 FF'
'FF FF 00 FF FF FF'
}

Vous voyez que l'icône (composée de plusieurs images, rappelons-le), le curseur et la première bitmap sont codés en hexadécimal. Pour la seconde bitmap, rappelez-vous que nous avons choisi de la stocker dans un fichier séparé; BRW inclut l'image dans le fichier .RES au moment de la compilation.

VI-A-11-a - Fichier de configuration

Vous avez peut-être remarqué qu'en plus des fichiers .RC, .RES et .INC, BRW enregistre également un fichier CHGFOND.RWS. Ce fichier contient la configuration de BRW liée au projet : les options d'affichage et, surtout, les préférences que nous avons définies. Il n'est donc pas superflu de le conserver avec les sources du projet.

VI-B - Utilisation des ressources dans un programme

Nous allons à présent voir de quelle façon utiliser les différentes ressources que nous avons créées.

VI-B-1 - Menu

Le menu principal est affecté à la fenêtre principale de l'application dès la création de celle-ci, c'est-à-dire dans son constructeur.
Une fonction de l'API permet de charger une ressource de type menu : LoadMenu. Voici sa déclaration :

Function LoadMenu (Instance : hInst; MenuName : pChar) : hMenu;

- Instance est le handle de l'instance du programme. On passe généralement la constante typée hInstance (voir ci-dessous)

- MenuName est l'identificateur de la ressource

- La valeur retournée par la fonction est le handle du menu chargé

Tout d'abord, hInstance est une constante typée (de type LongInt) définie dans l'unité System. Elle représente le handle de l'application. Lors de l'initialisation de l'exécutable, la valeur de hInstance est automatiquement demandée au système.

Autres chose : vous voyez que l'identificateur de la ressource est passé comme un paramètre de type pChar. Pour un identificateur défini sous forme de chaîne de caractères, c'est évident mais... quid pour un identificateur numérique comme ceux que nous avons attribués à toutes nos ressources ? Eh bien, nous devons recourir à un transtypage :

Attr.Menu := LoadMenu(hInstance,pChar(id_MenuPrincipal));

Que se passe-t-il concrètement ? Grâce au transtypage, le mot de poids fort de l'adresse vaut 0000h et le mot de poids faible a la valeur de id_MenuPrincipal. Lorsqu'elle détecte un mot de poids fort égal à 0000h, la fonction LoadMenu sait qu'elle a affaire non pas à l'adresse d'une chaîne de caractères mais bien à un identificateur stocké dans le mot de poids faible.

La technique du transtypage sera utilisée systématiquement pour passer un identificateur numérique à un paramètre pChar

Une dernière chose : le handle du menu retourné par la fonction LoadMenu est affecté au champ Attr.Menu de l'objet tWindow.
Voici la déclaration du champ Attr :

Type tWindowAttr = Record
Title : pChar;
Style : LongInt;
ExStyle : LongInt;
X, Y, W, H : Integer;
Param : Pointer;
case Integer of
0 : (Menu : hMenu);
1 : (Id : Integer);
end;

tWindow = Object(tWindowsObject)
Attr : tWindowAttr;
... etc

Attr contient les attributs qui définissent l'aspect de la fenêtre au moment de sa création. Nous aurons l'occasion de détailler tous les champs du type tWindowAttr au fur et à mesure que nous les rencontrerons.
En fait, nous avons déjà croisé, sans en parler, les champs X, Y, W et H dans le constructeur INIT de la fenêtre principale du programme CLIC.PAS. Ces quatre champs représentent respectivement l'abscisse (X), l'ordonnée (Y), la largeur (W comme width) et la hauteur (H comme height) de la fenêtre à l'écran.
Le champ Menu, lui, contient le handle du menu affecté à la fenêtre.

Dans le constructeur Init de l'objet descendant de tWindow, il ne faut pas oublier d'appeler le constructeur de l'objet ancêtre :

Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
(* Chargement du menu *)
Begin
tWindow.INIT(aParent,aTitle);
Attr.Menu := LoadMenu(hInstance,pChar(id_MenuPrincipal))
End;

VI-B-2 - Table d'accélérateurs

Le principe du chargement d'une table d'accélérateurs est identique à celui d'un menu, mis à part que la fonction de l'API utilisée est LoadAccelerators :

Function LoadAccelerators (Instance : hInst; TableName : pChar) : hAccel;

De la même façon, on passera hInstance comme handle d'instance et, à l'aide du transtypage, l'identificateur de la ressource comme nom de ressource.
Par contre, à la différence du menu, qui est propre à une fenêtre, la table d'accélérateurs est unique pour toute l'application et doit être chargée dans une méthode InitInstance de l'objet descendant de tApplication. Dès que le handle de la table d'accélérateurs retourné par LoadAccelerators est affecté au champ hAccTable de l'objet descendant de tApplication, ce dernier commence à convertir les combinaisons de touches en messages de commandes similaires à ceux générés par un menu.

Voici le code de chargement de la table d'accélérateurs :

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

VI-B-3 - Icône

Toute fenêtre Windows est susceptible de posséder son icône. Donc, pas seulement la fenêtre principale d'une application.
Une icône est liée à une classe de fenêtre; on parle d'ailleurs d'icône de classe.

La fonction de l'API dédiée au chargement d'une icône est LoadIcon :

Function LoadIcon (Instance : hInst; IconName : pChar) : hIcon;

Le fonctionnement de cette fonction est identique à celui de LoadMenu et LoadAccelerators, que nous venons de voir.

Pour affecter une icône à une fenêtre, il faut modifier les paramètres de classe de celle-ci. Typiquement, cela se fait dans la méthode GetWindowClass de l'objet descendant de tWindow :

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Chargement de l'icône de classe de la fenêtre *)
Begin
tWindow.GETWINDOWCLASS(aWndClass);
aWndClass.hIcon := LoadIcon(hInstance,pChar(id_IconeFenetrePrincipale));
End;

Le fait d'appeler la méthode GetWindowClass de l'objet ancêtre initialise la classe de fenêtre avec ses valeurs par défaut. Dans un second temps, l'icône de classe est affectée au champ hIcon de la structure aWndClass. Cette structure, de type tWndClass, contient les paramètres de la classe de fenêtre. Cette dernière est alors prête à être enregistrée par le système.

Si l'application ne comporte qu'une seule fenêtre (je parle bien de fenêtre, pas de dialogue - que nous aborderons bien plus tard), la méthode qui vient d'être exposée est suffisante. Par contre, si elle comporte plusieurs fenêtres qui doivent avoir des icônes différentes, il faut les lier à des classes différentes. On ne peut plus se contenter du nom de classe par défaut attribué par OWL, TurboWindow, mais il faudra définir un nom de classe unique pour chaque fenêtre.
La méthode GetClassName de l'objet descendant de tWindow est faite pour cela. Par exemple :

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

VI-B-4 - Curseur

Tout comme nous avons pu définir une icône de classe, nous pouvons définir un curseur de classe pour une fenêtre. Ce curseur sera alors utilisé par Windows à l'intérieur de la zone client de la fenêtre.

Il n'est pas question, avec la technique présentée ici, de redéfinir les curseurs que Windows affiche par défaut lorsque la souris survole une barre de menu, une bordure ou tout autre élément

La fonction de l'API utilisée pour charger un curseur est LoadCursor :

Function LoadCursor (Instance : hInst; CursorName : pChar) : hCursor;

Tout comme pour l'icône de classe, le curseur est chargé dans la méthode GetWindowClass de l'objet descendant de tWindow :

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Chargement du curseur de classe de la fenêtre *)
Begin
tWindow.GETWINDOWCLASS(aWndClass);
aWndClass.hCursor := LoadCursor(hInstance,pChar(id_Curseur));
End;

C'est au champ hCursor de la structure aWndClass qu'il est affecté.

VI-B-5 - Bitmap

Vous connaissez la musique, à présent, puisque les fonctions de chargement de ressources se ressemblent toutes. Voici LoadBitmap :

Function LoadBitmap (Instance : hInst; BitmapName : pChar) : hBitmap;

Il n'y a pas de méthode d'objet privilégiée dans laquelle charger une bitmap : c'est selon les besoins.

Il faut détruire la bitmap chargée par LoadBitmap avant la fermeture du programme. C'est important, sinon l'espace mémoire alloué pour la ressource ne sera pas libéré par Windows

La fonction de l'API qui permet de détruire la bitmap est DeleteObject :

Function DeleteObject (GDIObject : tHandle) : Bool;

Nous utiliserons très souvent DeleteObject dans les programmes de ce tutoriel, pour détruire tout un tas d'objets de l'interface graphique (pinceaux, crayons, bitmaps, polices de caractères, etc). Elle attend comme paramètre le handle de l'objet à détruire.

VI-B-6 - En résumé

Pour charger une ressource :

- On utilise un transtypage en pChar pour passer un identificateur numérique comme paramètre

- On charge un menu à l'aide de LoadMenu dans le constructeur Init de la fenêtre

- On charge une table d'accélérateurs à l'aide de LoadAccelerators dans la méthode InitInstance de l'application

- On charge une icône de classe à l'aide de LoadIcon dans la méthode GetWindowClass de la fenêtre

- On charge un curseur de classe à l'aide de LoadCursor dans la méthode GetWindowClass de la fenêtre

- On charge une bitmap à l'aide de LoadBitmap et on la détruit à l'aide de DeleteObject

VI-C - Le programme CHGFOND.PAS

Nous allons à présent rassembler toutes ces notions dans notre programme. Son but est particulièrement transcendant : en utilisant les commandes du menu ou les accélérateurs, nous pouvons changer le motif de fond de la fenêtre.

Voici le source complet :

Program CHGFOND;

(* Illustration création et utilisation de ressources.

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

{$R CHGFOND.RES}

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

{$I CHGFOND.INC}

Type pFenetrePrincipale = ^tFenetrePrincipale;
tFenetrePrincipale = Object(tWindow)
AncienPinceauFond : hBrush;
PinceauFond1, PinceauFond2 : hBrush;
Constructor INIT (aParent : pWindowsObject;  aTitle : pChar);
(* Chargement du menu et des bitmaps.
Création des pinceaux pour le fond *)
Function GETCLASSNAME : pChar;
virtual;
(* Retourne le nom de classe de la fenêtre *)
Procedure GETWINDOWCLASS (var aWndClass : tWndClass);
virtual;
(* Chargement de l'icône de classe de la fenêtre *)
Procedure CMFONDDEFAUT (var Msg : tMessage);
virtual cm_First + cm_FondDefaut;
(* Restauration du fond par défaut *)
Procedure CMFOND1 (var Msg : tMessage);
virtual cm_First + cm_Fond1;
(* Utilisation de la bitmap id_Bitmap1 comme fond *)
Procedure CMFOND2 (var Msg : tMessage);
virtual cm_First + cm_Fond2;
(* Utilisation de la bitmap id_Bitmap2 comme fond *)
Destructor DONE; virtual;
(* Restaure le fond original.
Détruit les pinceaux créés *)
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 et des bitmaps - Création des pinceaux pour le fond *)
Var h : hBitmap;
Begin
tWindow.INIT(aParent,aTitle);
Attr.Menu := LoadMenu(hInstance,pChar(id_MenuPrincipal));
h := LoadBitmap(hInstance,pChar(id_Bitmap1));
if h <> 0
then
begin
PinceauFond1 := CreatePatternBrush(h);
DeleteObject(h);
end;
h := LoadBitmap(hInstance,pChar(id_Bitmap2));
if h <> 0
then
begin
PinceauFond2 := CreatePatternBrush(h);
DeleteObject(h);
end;
End;

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

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Chargement de l'icône et du curseur de classe de la fenêtre.
Détermination du fond par défaut *)
Begin
tWindow.GETWINDOWCLASS(aWndClass);
aWndClass.hIcon := LoadIcon(hInstance,pChar(id_IconeFenetrePrincipale));
aWndClass.hCursor := LoadCursor(hInstance,pChar(id_Curseur));
AncienPinceauFond := aWndClass.hbrBackground;
End;

Procedure tFenetrePrincipale.CMFONDDEFAUT (var Msg : tMessage);
(* Restauration du fond par défaut *)
Begin
SetClassLong(hWindow,gcl_hbrBackground,AncienPinceauFond);
InvalidateRect(hWindow,Nil,True);
End;

Procedure tFenetrePrincipale.CMFOND1 (var Msg : tMessage);
(* Utilisation de la bitmap id_Bitmap1 comme fond *)
Begin
SetClassLong(hWindow,gcl_hbrBackground,PinceauFond1);
InvalidateRect(hWindow,Nil,True);
End;

Procedure tFenetrePrincipale.CMFOND2 (var Msg : tMessage);
(* Utilisation de la bitmap id_Bitmap2 comme fond *)
Begin
SetClassLong(hWindow,gcl_hbrBackground,PinceauFond2);
InvalidateRect(hWindow,Nil,True);
End;

Destructor tFenetrePrincipale.DONE;
(* Restaure le fond original - Détruit les pinceaux créés *)
Begin
SetClassLong(hWindow,gcw_HbrBackground,AncienPinceauFond);
DeleteObject(PinceauFond1);
DeleteObject(PinceauFond2);
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,'Travail avec les ressources'));
End;

Var Programme : tProgramme;

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

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

VI-C-1 - Directives de compilation

Pour demander à Virtual Pascal de lier le fichier binaire de ressources, il faut inclure en tout début de programme la directive $R suivie du nom de fichier :

{$R CHGFOND.RES}

Astuce : si le nom du fichier .RES est le même que celui du source compilé, vous pouvez utiliser un joker :

{$R *.RES}

Virtual Pascal attend le fichier .RES dans le dossier défini dans les options (menu Options --> Directories --> Resource directories).

Parallèlement, il faut indiquer au compilateur qu'il doit inclure les constantes qui se trouvent dans le fichier .INC. Cela se fait idéalement dans la section de déclaration des constantes non typées, au moyen de la directive $I :

{$I CHGFOND.INC}

Virtual Pascal attend le fichier .INC dans le même dossier que le source à compiler, à moins qu'un chemin soit spécifié dans la directive.

VI-C-2 - Les bitmaps et les fonds de fenêtre

Le fond d'une fenêtre est dessiné par Windows en répétant un motif. Généralement, il s'agit d'un motif plein dont la répétition donne une surface unie. Comme tout bon écolier, pour dessiner, Windows utilise des crayons et des pinceaux; les premiers servent à dessiner des lignes et des bordures de figures géométriques, les seconds servent à peindre des surfaces.
Nous aurons l'occasion de détailler tout cela dans le chapitre consacré à l'interface graphique (GDI).

Notre programme permet de choisir entre trois motifs différents : le fond par défaut et les deux ressources de type bitmap que nous avons créées. Dans le constructeur de la fenêtre principale du programme, nous chargeons les deux bitmaps et nous en faisons des motifs utilisables par Windows pour peindre le fond de la fenêtre :

h := LoadBitmap(hInstance,pChar(id_Bitmap1));
if h <> 0
then
begin
PinceauFond1 := CreatePatternBrush(h);
DeleteObject(h);
end;

La fonction LoadBitmap charge la ressource de type bitmap en mémoire et retourne son handle. Si celui-ci est non nul, c'est que le chargement a réussi. Nous créons ensuite le motif de fond à l'aide de CreatePatternBrush :

Function CreatePatternBrush (Bitmap : hBitmap) : hBrush;

Cette fonction reçoit comme paramètre un handle de bitmap (hBitmap) et retourne un handle de pinceau (hBrush).
A partir du moment où le motif de fond est créé, nous n'avons plus besoin de la bitmap et nous pouvons immédiatement la détruire avec DeleteObject. Mais attention : avant la fermeture du programme, nous devrons également détruire les pinceaux qui viennent d'être créés.

Vous devez absolument garder ce principe à l'esprit : tout objet créé doit être détruit.
L'endroit idéal pour détruire des objets créés par le programme et utilisés pendant toute la durée de son exécution est soit le destructeur de la fenêtre principale, soit le destructeur de l'objet descendant de tApplication

Comme les handles des deux motifs créés à partir de nos ressources sont des champs déclarés dans l'objet tFenetrePrincipale, nous détruisons ces motifs dans le destructeur de cet objet :

Destructor tFenetrePrincipale.DONE;
Begin
...
DeleteObject(PinceauFond1);
DeleteObject(PinceauFond2);
tWindow.DONE;
End;

N'oubliez pas de détruire les motifs avant d'appeler le destructeur Done de l'objet ancêtre tWindow.

Maintenant que nous savons créer des motifs utilisables par Windows pour peindre le fond de la fenêtre, nous allons voir comment demander à Windows de les utiliser. Selon que l'on veut redéfinir le fond de la fenêtre au démarrage du programme ou en cours d'exécution, la technique est différente. Nous sommes ici dans le second cas, puisque nous laissons le programme démarrer avec le fond de fenêtre par défaut et que ce n'est qu'en réponse aux commandes de l'utilisateur que le fond changera. Malgré tout, nous allons détailler les deux techniques.

VI-C-2-a - Définir un fond de fenêtre au démarrage du programme

Rappelons donc que ce paragraphe ne s'applique pas au programme CHGFOND.PAS.
La technique consiste à inclure le nouveau motif de fond dans les informations de classe de la fenêtre principale avant que celle-ci devienne visible.

Voici un petit programme d'exemple :

Program FONDEMAR;

(* Définition d'un fond de fenêtre au démarrage du programme.

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

{$R FONDEMAR.RES}

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

{$I FONDEMAR.INC}

Type pFenetrePrincipale = ^tFenetrePrincipale;
tFenetrePrincipale = Object(tWindow)
PinceauFond : hBrush;
Constructor INIT (aParent : pWindowsObject; aTitle : pChar);
(* Chargement de la bitmap - Création du pinceau pour le fond *)
Function GETCLASSNAME : pChar;
virtual;
(* Retourne le nom de classe de la fenêtre *)
Procedure GETWINDOWCLASS (var aWndClass : tWndClass);
virtual;
(* Définition du nouveau fond *)
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);
(* Chargement de la bitmap - Création du pinceau pour le fond *)
Var h : hBitmap;
Begin
tWindow.INIT(aParent,aTitle);
h := LoadBitmap(hInstance,pChar(id_Bitmap1));
if h <> 0
then
begin
PinceauFond := CreatePatternBrush(h);
DeleteObject(h);
end;
End;

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

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Définition du nouveau fond *)
Begin
tWindow.GETWINDOWCLASS(aWndClass);
aWndClass.hbrBackground := PinceauFond;
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,'Fond de fen'#234'tre au d'#233'marrage'));
End;

Var Programme : tProgramme;

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

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

Le chargement de la ressource de type bitmap et la création du pinceau devant servir de motif peuvent se dérouler dans le constructeur de la fenêtre principale. Ensuite, il faut redéfinir deux méthodes de tWindow : GetClassName et GetWindowClass. Dans cette dernière méthode,nous affectons le handle du pinceau créé au champ hbrBackground de la structure aWndClass, que nous avons déjà rencontrée lorsque nous avons parlé des icônes de classe et des curseurs.
Enfin, vous voyez que nous n'avons pas oublié de détruire le pinceau dans le destructeur de la fenêtre principale.

VI-C-2-b - Définir un fond de fenêtre en cours de programme

Pour changer le fond de la fenêtre une fois qu'elle est visible, comme dans notre programme CHGFOND.PAS, il faut utiliser une tout autre technique. En effet, les méthodes GetClassName et GetWindowClass ne sont appelées qu'au démarrage du programme, pour enregistrer la classe de fenêtre auprès du système. Par la suite, il faut aller directement modifier les paramètres de la classe de fenêtre.

Voici donc ce qui se passe dans chaque méthode virtuelle indexée chargée de répondre aux commandes de changement de fond :

Procedure tFenetrePrincipale.CMFOND1 (var Msg : tMessage);
(* Utilisation de la bitmap id_Bitmap1 comme fond *)
Begin
SetClassLong(hWindow,gcl_hbrBackground,PinceauFond1);
InvalidateRect(hWindow,Nil,True);
End;

La fonction SetClasslong permet d'aller changer une valeur dans les paramètres de la classe de fenêtre à laquelle appartient la fenêtre dont le handle est passé comme premier paramètre :

Function SetClassLong (Wnd : hWnd; Index : Integer; NewLong : LongInt) : DWord;

Je vous laisse voir la documentation du SDK pour les détails sur cette structure de type tWndClass (WNDCLASS dans le SDK); ce qui nous intéresse ici est le champ hbrBackground, que l'on peut modifier à l'aide de SetClasslong en spécifiant gcl_hbrBackground comme index.

Une fois la modification faite dans les paramètres de la classe de fenêtre, il faut forcer la fenêtre à se réafficher. C'est le but de la fonction InvalidateRect :

Function InvalidateRect (Wnd : hWnd; Rect : pRect; Erase : Bool) : Bool;

Nous rencontrerons cette fonction à de nombreuses reprises. Elle rend "invalide" un rectangle dans la zone client de la fenêtre, de sorte que Windows est forcé de le redessiner.
Le paramètre Rect doit recevoir l'adresse d'une structure de type tRect, qui contient les coordonnées d'un rectangle. Le fait de passer Nil comme adresse rend invalide toute la zone client de la fenêtre.
Le dernier paramètre, Erase, indique si le fond de la fenêtre doit être redessiné par Windows.

VI-C-2-c - Déterminer le fond par défaut

Parmi les commandes qui permettent de changer le fond de la fenêtre de notre programme, il y en a une qui restaure le fond par défaut. Donc, au démarrage du programme, nous devons déterminer quel pinceau est utilisé par Windows.

Même si elle est inutilisable pour notre programme, nous avons vu au paragraphe VI-C-2-a la technique qui permet de définir un fond de fenêtre au démarrage. Eh bien, nous allons nous en inspirer : dans la méthode GetWindowClass, au lieu de modifier le champ hbrBackground de la structure aWndClass, nous allons le lire et sauvegarder sa valeur !

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
Begin
tWindow.GETWINDOWCLASS(aWndClass);
...
AncienPinceauFond := aWndClass.hbrBackground;
End;

Tout simplement, en réponse à la commande cm_FondDefaut, nous utilisons ce handle de pinceau sauvegardé :

Procedure tFenetrePrincipale.CMFONDDEFAUT (var Msg : tMessage);
(* Restauration du fond par défaut *)
Begin
SetClassLong(hWindow,gcl_hbrBackground,AncienPinceauFond);
InvalidateRect(hWindow,Nil,True);
End;

Nous allons clôturer ici tous ces aspects théoriques sur les fonds de fenêtre pour nous pencher sur l'utilisation des autres ressources que nous avons créées.

VI-C-3 - Les autres ressources

Les autres ressources que les bitmaps sont chargées de la manière expliquée précédemment dans ce chapitre (voyez le résumé).

Le menu :

Constructor tFenetrePrincipale.INIT (aParent : pWindowsObject; aTitle : pChar);
Begin
tWindow.INIT(aParent,aTitle);
Attr.Menu := LoadMenu(hInstance,pChar(id_MenuPrincipal));
...
End;

La table d'accélérateurs :

Procedure tProgramme.INITINSTANCE;
Begin
tApplication.INITINSTANCE;
hAccTable := LoadAccelerators(hInstance,pChar(id_Accel));
End;

L'icône et le curseur de classe :

Function tFenetrePrincipale.GETCLASSNAME : pChar;
Begin
GETCLASSNAME := 'ChgFond';
End;

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
Begin
tWindow.GETWINDOWCLASS(aWndClass);
aWndClass.hIcon := LoadIcon(hInstance,pChar(id_IconeFenetrePrincipale));
aWndClass.hCursor := LoadCursor(hInstance,pChar(id_Curseur));
...
End;

VI-C-4 - Version avec une méthode WMCOMMAND

Dans notre programme, nous avons géré les commandes du menu et des accélérateurs à l'aide de méthodes virtuelles dynamiques indexées. Pour chaque message cm_XXX, nous avons créé une méthode tFenetreprincipale.CMXXX.

Imaginons à présent qu'au lieu de trois commandes, il faille répondre à plusieurs dizaines de commandes. Créer une méthode virtuelle dynamique indexée pour chacune d'entre elles alourdirait le programme et, de plus, serait particulièrement fastidieux.
Voici donc une variante du programme qui, au lieu de répondre individuellement aux messages de commandes, ne répond qu'à l'unique message wm_Command :

Program CHGFOND2;

(* Illustration création et utilisation de ressources.
Version utilisant une méthode WMCOMMAND pour traiter les messages de commandes.

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

{$R CHGFOND.RES}

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

{$I CHGFOND.INC}

Type pFenetrePrincipale = ^tFenetrePrincipale;
tFenetrePrincipale = Object(tWindow)
AncienPinceauFond : hBrush;
PinceauFond1, PinceauFond2 : hBrush;
Constructor INIT (aParent : pWindowsObject;  aTitle : pChar);
(* Chargement du menu et des bitmaps.
Création des pinceaux pour le fond *)
Function GETCLASSNAME : pChar;
virtual;
(* Retourne le nom de classe de la fenêtre *)
Procedure GETWINDOWCLASS (var aWndClass : tWndClass);
virtual;
(* Chargement de l'icône de classe de la fenêtre *)
Procedure WMCOMMAND (var Msg : tMessage);
virtual wm_First + wm_Command;
(* Réponse aux messages de commandes *)
Destructor DONE; virtual;
(* Restaure le fond original.
Détruit les pinceaux créés *)
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 et des bitmaps - Création des pinceaux pour le fond *)
Var h : hBitmap;
Begin
tWindow.INIT(aParent,aTitle);
Attr.Menu := LoadMenu(hInstance,pChar(id_MenuPrincipal));
h := LoadBitmap(hInstance,pChar(id_Bitmap1));
if h <> 0
then
begin
PinceauFond1 := CreatePatternBrush(h);
DeleteObject(h);
end;
h := LoadBitmap(hInstance,pChar(id_Bitmap2));
if h <> 0
then
begin
PinceauFond2 := CreatePatternBrush(h);
DeleteObject(h);
end;
End;

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

Procedure tFenetrePrincipale.GETWINDOWCLASS (var aWndClass : tWndClass);
(* Chargement de l'icône et du curseur de classe de la fenêtre.
Détermination du fond par défaut *)
Begin
tWindow.GETWINDOWCLASS(aWndClass);
aWndClass.hIcon := LoadIcon(hInstance,pChar(id_IconeFenetrePrincipale));
aWndClass.hCursor := LoadCursor(hInstance,pChar(id_Curseur));
AncienPinceauFond := aWndClass.hbrBackground;
End;

Procedure tFenetrePrincipale.WMCOMMAND (var Msg : tMessage);
(* Réponse aux messages de commandes *)
Begin
case LoWord(Msg.wParam) of
cm_FondDefaut : begin   (* Restauration du fond par défaut *)
SetClassLong(hWindow,gcl_hbrBackground,AncienPinceauFond);
InvalidateRect(hWindow,Nil,True);
end;
cm_Fond1 : begin   (* Utilisation de la bitmap id_Bitmap1 comme fond *)
SetClassLong(hWindow,gcl_hbrBackground,PinceauFond1);
InvalidateRect(hWindow,Nil,True);
end;
cm_Fond2 : begin   (* Utilisation de la bitmap id_Bitmap2 comme fond *)
SetClassLong(hWindow,gcl_hbrBackground,PinceauFond2);
InvalidateRect(hWindow,Nil,True);
end;
else
tWindow.WMCOMMAND(Msg);
end;
End;

Destructor tFenetrePrincipale.DONE;
(* Restaure le fond original - Détruit les pinceaux créés *)
Begin
SetClassLong(hWindow,gcw_HbrBackground,AncienPinceauFond);
DeleteObject(PinceauFond1);
DeleteObject(PinceauFond2);
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,'Travail avec les ressources'));
End;

Var Programme : tProgramme;

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

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

Le message wm_Command est généré, notamment, chaque fois qu'un utilisateur sélectionne une commande de menu ou presse un raccourci clavier. Nous verrons beaucoup plus tard d'autres cas où ce message est généré.

Voici la signification des champs, que vous pouvez trouver dans le SDK :

- wNotifyCode = HiWord(wParam) : code de notification (0 si menu, 1 si accélérateur)

- wID = LoWord(wParam) : identificateur du menu ou de l'accélérateur

- hwndCtl = lParam : handle de contrôle (0 si menu ou accélérateur)

Dans le cas qui nous occupe, c'est-à-dire dans le cas de messages de commandes de menu ou d'accélérateurs, seul le mot de poids faible du champ wParam nous intéresse. A la place des trois méthodes virtuelles indexées, nous avons donc créé une unique méthode WMCOMMAND dans laquelle la valeur de ce champ est testée :

Procedure tFenetrePrincipale.WMCOMMAND (var Msg : tMessage);
(* Réponse aux messages de commandes *)
Begin
case LoWord(Msg.wParam) of
cm_FondDefaut : begin   (* Restauration du fond par défaut *)
SetClassLong(hWindow,gcl_hbrBackground,AncienPinceauFond);
InvalidateRect(hWindow,Nil,True);
end;
cm_Fond1 : begin   (* Utilisation de la bitmap id_Bitmap1 comme fond *)
SetClassLong(hWindow,gcl_hbrBackground,PinceauFond1);
InvalidateRect(hWindow,Nil,True);
end;
cm_Fond2 : begin   (* Utilisation de la bitmap id_Bitmap2 comme fond *)
SetClassLong(hWindow,gcl_hbrBackground,PinceauFond2);
InvalidateRect(hWindow,Nil,True);
end;
else
tWindow.WMCOMMAND(Msg);
end;
End;

Pour toutes les commandes non traitées, surtout n'oubliez pas d'appeler la méthode WMCOMMAND de l'objet ancêtre tWindow !

VI-C-5 - Téléchargement des sources des programmes de cette page

- Téléchargez le fichier chgfond.zip

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 :

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.

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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

{$I ONDOYANT.INC}

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

{$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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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) :

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 :

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 :

#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 :

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 !

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

idc_Wait

idc_AppStarting

idc_Cross

idc_IBeam

idc_No

idc_UpArrow

idc_SizeWE

idc_SizeNS

idc_SizeNWSE

idc_SizeNESW

idc_Size / idc_SizeAll

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

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 :

Voici le source du programme :

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 :

#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 :

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 :

Function GetOpenFileName (var OpenFile : tOpenFilename) : Bool;

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

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 :

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 :

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) :

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 :

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 :

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 :

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 :

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 :

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;

VIII - Index

Paragraphe

Accélérateurs

VI-A-6 / VI-B-2

ANSI

I-E

ASCII

I-E

Attr (tWindow)

VI-B-1

Bitmap

Ressources de type bitmap

VI-A-4 / VI-B-5

Borland Pascal for Windows 7.0

I-C-1

Licence

I-D-1

Run-Time Library (RTL)

I-D-1

Borland Resource Workshop 4.5 (BRW)

I-C-2 / VI-A

Caractères accentués

I-E

Chaînes à zéro terminal (AZT)

II

Opérations sur les chaînes AZT

II-B

Allocation dynamique

II-B-10

Assignation

II-B-1

Comparaison

II-B-6

Concaténation

II-B-8

Conversion vers et depuis le type String

II-B-9

Copie

II-B-5

Détermination de la longueur

II-B-4

Détermination de la position d'une chaîne dans une autre

II-B-7

Initialisation d'une chaîne vide

II-B-2

Troncature

II-B-3

Type PChar

II-A

CheckBreak (unité WinCRT)

III-B-3

Classe de fenêtre

VI-B-3

CloseWindow (tWindow)

V-D-5

cm_CascadeChildren (identificateur standard Windows)

VI-A-5-a

cm_EditCopy (identificateur standard Windows)

VI-A-5-a

cm_EditCut (identificateur standard Windows)

VI-A-5-a

cm_EditDelete (identificateur standard Windows)

VI-A-5-a

cm_EditFind (identificateur standard Windows)

VI-A-5-a

cm_EditFindNext (identificateur standard Windows)

VI-A-5-a

cm_EditPaste (identificateur standard Windows)

VI-A-5-a

cm_EditReplace (identificateur standard Windows)

VI-A-5-a

cm_EditUndo (identificateur standard Windows)

VI-A-5-a

cm_Exit (identificateur standard Windows)

VI-A-5-a

cm_FileNew (identificateur standard Windows)

VI-A-5-a

cm_FileOpen (identificateur standard Windows)

VI-A-5-a

cm_FileSave (identificateur standard Windows)

VI-A-5-a

cm_FileSaveAs (identificateur standard Windows)

VI-A-5-a

cm_TileChildren (identificateur standard Windows)

VI-A-5-a

cm_First

V-D-3

Codes mnémoniques des identificateurs de messages

V-C

Coloration syntaxique

I-D-3-f

Compilateurs compatibles

I-C-1

CreatePatternBrush

VI-C-2

Curseur

Ressources de type curseur

VI-A-8 / VI-B-4

Cursor (unité WinCRT)

III-B-4-c

CursorTo (unité WinCRT)

III-B-4-c

DeleteObject

VI-B-5

Discardable (option de mémoire d'une ressource)

VI-A-10

Done (tApplication)

IV-B

DoneWinCRT (unité WinCRT)

III-B-5

Editeur de ressources

I-C-2 / VI-A

FindClose

III-B-2

FindFirstFile

III-B-2

FindNextFile

III-B-2

Fond de fenêtre

VI-C-2

gcl_hbrBackground

VI-C-2-b

GetClassName (tWindow)

VI-B-3

GetWindowClass (tWindow)

VI-B-3

GotoXY (unité WinCRT)

III-B-4-c

hAccTable (tApplication)

VI-B-2

Handle

V-D-5

hBitmap

VI-C-2

hbrBackground (tWndClass)

VI-C-2-a

hBrush

VI-C-2

hCursor (tWndClass)

VI-B-4

hIcon (tWndClass)

VI-B-3

hInstance

VI-B-1

Hot spot

VI-A-8

hWindow

V-D-5

id_First

V-D-3

Identificateurs de messages

V-C

Index de début de plage (messages)

V-D-3

Init (tApplication)

IV-B

Init (tWindow)

IV-D

InitInstance (tApplication)

VI-B-2

InitMainWindow (tApplication)

IV-C

InitWinCRT (unité WinCRT)

III-B-3

Integrated Development Environment (IDE)

I-D-3

Configuration

I-D-3

Curseur de texte

I-D-3

Raccourci

I-D-3

Taille de l'éditeur

I-D-3

InvalidateRect

VI-C-2-b

Load on call (option de mémoire d'une ressource)

VI-A-10

LoadAccelerators

VI-B-2

LoadBitmap

VI-B-5

LoadCursor

VI-B-4

LoadIcon

VI-B-3

LoadMenu

VI-B-1

MainWindow (tApplication)

IV-C

Masque AND

VI-A-7 / VI-A-8

Masque XOR

VI-A-7 / VI-A-8

Menu

Identificateurs standards de commandes

VI-A-5-a

Raccourcis dans les menus

VI-A-6-a

Ressources de type menu

VI-A-5 / VI-B-1

MessageBox

V-D-5

Messages Windows

V

Identificateurs

V-C

wm_Command

VI-C-4

wm_LButtonDown

V-D

wm_RButtonDown

V-D

Méthodes dynamiques virtuelles indexées

V-D-3

Moveable (option de mémoire d'une ressource)

VI-A-10

MSDN

I-C-3

nf_First

V-D-3

Object Windows library (OWL) - installation

I

Compilation

I-D-2

Sources

I-D-2

Téléchargement

I-D-2

Options de l'IDE de Virtual Pascal

I-D-3

Coloration syntaxique

I-D-3-f

Compilateur

I-D-3-b

Curseur

I-D-3-e

Editeur de liens

I-D-3-c

Plateforme

I-D-3-a

Syntaxe étendue

I-D-3-b

Taille de l'éditeur

I-D-3-d

PChar

II-A

Prérequis

I-A

Programmation pilotée par événements

V

Pure (option de mémoire d'une ressource)

VI-A-10

pWindow

IV-D

Raccourci vers l'IDE de Virtual Pascal

I-D-3-d

Raccourcis dans les menus

VI-A-6-a

Ressources

VI

Accélérateurs

VI-A-6 / VI-B-2

Bitmap

VI-A-4 / VI-B-5

Curseur

VI-A-8 / VI-B-4

Fichier include

VI-A-3

Icône

VI-A-7 / VI-B-3

Menu

VI-A-5 / VI-B-1

Options de mémoire

VI-A-10

Utilisation de ressources dans un programme

VI-B

Run (tApplication)

IV-B

Run-Time Library (RTL)

I-D-2

Compilation

I-D-2

ScreenSize (unité WinCRT)

III-B-3

SetClassLong

VI-C-2-b

StrCat (unité Strings)

II-B-8

StrComp (unité Strings)

II-B-6

StrCopy (unité Strings)

II-B-1

StrDispose (unité Strings)

II-B-10

StrIComp (unité Strings)

II-B-6

StrLCat (unité Strings)

II-B-8

StrLen (unité Strings)

II-B-4

StrNew (unité Strings)

II-B-10

StrPas (unité Strings)

II-B-9

StrPCopy (unité Strings)

II-B-9

StrPos (unité Strings)

II-B-7

Syntaxe étendue

I-D-3-b

Table d'accélérateurs

VI-A-6 / VI-B-2

Tableaux de caractères

II-A

tApplication

IV-B

Done

IV-B

Init

IV-B

InitInstance

VI-B-2

InitMainWindow

IV-C

MainWindow

IV-C

Run

IV-B

tMessage

V-B

Transtypage (pour un identificateur numérique de ressource)

VI-B-1

Tutoriels disponibles sur Developpez.com

I-B

tWindow

IV-D / V-D-2

Attr

VI-B-1

CloseWindow

V-D-5

Init

IV-D

tWindowAttr

VI-B-1

tWndClass

VI-B-3

Unité OWindows

IV-A

Unité Strings

II-B

StrCat

II-B-8

StrComp

II-B-6

StrCopy

II-B-1

StrDispose

II-B-10

StrIComp

II-B-6

StrLCat

II-B-8

StrLen

II-B-4

StrNew

II-B-10

StrPas

II-B-9

StrPCopy

II-B-9

StrPos

II-B-7

Unité WinCRT

III

CheckBreak

III-B-3

Cursor

III-B-4-c

CursorTo

III-B-4-c

DoneWinCRT

III-B-5

GotoXY

III-B-4-c

InitWinCRT

III-B-3

ScreenSize

III-B-3

WindowTitle

III-B-3

WriteBuf

III-B-3-a

WriteLn

III-B-3-a

Unité Windows

III-B-2

Unité WinProcs

III-B-2

Unité WinTypes

III-B-2

Virtual Pascal 2.1

I-C-1

Configuration

I-D-3

Installation

I-D-1

Téléchargement

I-C-1

WinSight

V

WinSpector

V

WindowTitle (unité WinCRT)

III-B-3

WMCOMMAND (tWindow)

VI-C-4

wm_Command

VI-C-4

wm_First

V-D-3

wm_LButtonDown

V-D

wm_RButtonDown

V-D

WriteBuf (unité WinCRT)

III-B-3-a

WriteLn (unité WinCRT)

III-B-3-a

Zone client de fenêtre

V-D-1

IX - Abréviations

Vous trouverez ci-dessous la signification des abréviations rencontrées tout au long du tutoriel :

Abréviation

Signification

API

Application Programming Interface

AZT

A Zéro Terminal (type de chaîne de caractères)

BRW

Borland Resource Workshop 4.5

GDI

Graphics Device Interface

GUI

Graphics User Interface

IDE

Integrated Development Environment

OWL

Object Windows Library

RTL

Run-Time Library

SDK

Software Development Kit

VP

Virtual Pascal 2.1

X - Téléchargement des sources des programmes du tutoriel

Tous les programmes de ce tutoriel (du moins, ceux qui font plus de 10 lignes ;-) sont disponibles au téléchargement, y compris les sources, scripts et binaires de ressources et exécutables.

Liens de téléchargement

CHGFOND (création et utilisation des ressources)

CHGFOND2 (version de CHGFOND avec méthode WMCOMMAND)

CLIC (réponse aux clics de souris)

CLIC2 (CLIC avec démonstration de MessageBox)

DIRTOT (démonstration des unités Strings et WinCRT)

FONDEMAR (changement de fond de fenêtre au démarrage)

MINIMUM (le programme OWL minimum)