Une ligne élastique en couleur

Maxence Delannoy

Lorsqu'on trace une ligne dans un programme graphique, une ligne dite élastique (rubber-band line en anglais) est tracée du premier point choisi à la position courante du curseur, ce qui vous permet de prévisualiser le résultat de l'opération. Voici la procédure à suivre pour doter vos programmes Delphi d'une telle fonctionnalité.

- Titre : Une ligne élastique en couleur

- Auteur : Maxence Delannoy

- Parution : 2 novembre 2000

- Mise à jour : 3 janvier 2020

- 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 Maxence Delannoy. 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. Un programme de tracé de ligne

- II. Implémenter la ligne élastique

- III. Un petit plus

I - Un programme de tracé de ligne

Tout d'abord, il faut permettre à l'utilisateur d'indiquer les deux extrémités de la ligne à tracer. Nous avons deux possibilités :

1. Quand l'utilisateur appuie sur le bouton gauche de la souris, il indique le point de départ de la ligne. Quand il relâche ce bouton, il indique le point final ;

2. L'utilisateur clique une première fois pour indiquer le point de départ, puis une seconde fois pour le point final. Cette deuxième solution présente l'avantage de permettre une plus grande précision, car l'utilisateur n'a pas à garder son doigt appuyé lorsqu'il déplace la souris. C'est cette approche que nous allons détailler.

Tout le code qui gère ce comportement est placé dans l'événement OnMouseDown de la fiche. Nous avons besoin de deux variables globales :

1. WaitForSecondPoint: Boolean; : cette variable booléenne nous permet de savoir si le clic correspond au premier point (la variable est alors égale à False) ou au second point (la variable est égale à True). À chaque clic, on inverse sa valeur.

2. FirstPoint: TPoint; : cette variable nous permet de mémoriser la position du premier point.

[...]

var
FirstPoint: TPoint;
Form1: TForm1;
WaitForSecondPoint: Boolean;

implementation

{$R *.DFM}
procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if WaitForSecondPoint then
begin
// C'est le deuxième point, on dessine réellement la ligne.
Canvas.MoveTo(FirstPoint.X, FirstPoint.Y);
Canvas.LineTo(X, Y);
end
else FirstPoint := Point(X, Y); // On mémorise la position du premier point.

WaitForSecondPoint := not WaitForSecondPoint;
end;

end.

II - Implémenter la ligne élastique

Voilà, nous avons un programme qui nous permet de tracer des lignes. Il ne nous reste plus qu'à implémenter cette fameuse ligne élastique. Pour ce faire, nous devons ajouter un gestionnaire à l'événement OnMouseMove de notre fiche.

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
begin
if WaitForSecondPoint then
begin
// On passe le crayon en mode inverse.
Canvas.Pen.Mode := pmNot;

// Si on a tracé une ligne la fois précédente, alors on l'efface.
if ErasePrevious then
begin
Canvas.MoveTo(FirstPoint.X, FirstPoint.Y);
Canvas.LineTo(PreviousPoint.X, PreviousPoint.Y);
end;

// On trace la ligne du premier point au point en cours.
Canvas.MoveTo(FirstPoint.X, FirstPoint.Y);
Canvas.LineTo(X, Y);

// On mémorise le point courant pour l'éventuel effacement.
PreviousPoint := Point(X, Y);

// On a tracé une ligne, il faut l'effacer la prochaine fois.
ErasePrevious := True;

// On restore le mode initial.
Canvas.Pen.Mode := pmCopy;
end;
end;

À chaque fois que le message WM_MOUSEMOVE est envoyé à notre fiche par Windows, nous traçons une ligne du premier point sélectionné à la position courante du curseur et nous effaçons la ligne qui a été éventuellement tracée à l'occurrence précédente de cet événement. Pour ce faire, nous mémorisons la position précédente du curseur dans la variable globale PreviousPoint de type TPoint et nous passons le crayon en mode inverse. Tous les points qui composent la ligne ont alors leur couleur inversée. Les pixels noirs deviennent blancs, et vice et versa. L'intérêt de la méthode, c'est que si nous traçons deux fois la ligne, nous inversons l'effet. Les pixels noirs passés en blanc reprennent leur couleur noire, et ainsi de suite, ce qui nous permet de tracer, puis d'effacer une ligne sans affecter le fond de l'écran.

Pour savoir si nous avons une ligne à effacer, nous basculons la variable globale ErasePrevious (Effacer précédent) à False dans le gestionnaire de l'événement OnMouseDown, puis à True après que l'on a tracé une ligne dans le gestionnaire de l'événement OnMouseMove. Le code de la méthode FormMouseDown devient donc :

procedure TForm1.FormMouseDown(Sender: TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if WaitForSecondPoint then
begin
// C'est le deuxième point, on dessine réellement la ligne.
Canvas.MoveTo(FirstPoint.X, FirstPoint.Y);
Canvas.LineTo(X, Y);
end
else
begin
FirstPoint := Point(X, Y); // On mémorise la position du premier point.
ErasePrevious := False;
end;

WaitForSecondPoint := not WaitForSecondPoint;
end;

III - Un petit plus

Notre ligne élastique fonctionnant, nous pouvons l'améliorer un peu. En effet, notre programme souffre d'un léger défaut. La couleur de la ligne élastique est différente de celle de la ligne finale. En fait, elle correspond à l'inverse de la couleur de la fiche (la fiche est par défaut de couleur gris clair, ce qui nous donne une ligne élastique de couleur gris foncé).
Nous allons utiliser un autre mode pour notre crayon : le mode pmXor.

Alors que le mode pmNot inversait la couleur du fond, celui-ci combine la couleur du crayon et celle du fond à l'aide d'une opération binaire Xor. Pour mémoire, voici le tableau des différentes combinaisons possibles :

A

B

A xor B = C

0

0

0

0

1

1

1

0

1

1

1

0

Cette opération présente la même particularité que l'opérateur unaire not : elle est réversible. Si nous calculons C xor B, nous obtenons à nouveau A.

C

B

C xor B = A

0

0

0

1

1

0

1

0

1

0

1

1

Nous pouvons donc utiliser le mode pmXor comme le mode pmNot. Si nous traçons deux fois une ligne dans ce mode, nous l'effaçons sans altérer le fond de l'écran. L'intérêt du mode pmXor, c'est que nous pouvons contrôler la couleur qui sera affichée à l'écran en modifiant la couleur du crayon.

Windows détermine la couleur à afficher de la façon suivante.

Soit Ca la couleur à afficher, Cc la couleur du crayon et Cf la couleur du fond. On a Ca = Cc xor Cf. On connait Ca : c'est la couleur qu'on veut donner à la ligne. Cf correspond à la valeur de la propriété Color de la fiche. Pour déterminer la couleur à donner au crayon, on fait le tableau des différentes combinaisons :

Cc

Cf

Ca

0

0

0

0

1

1

1

0

1

1

1

0

Après examen, on en déduit que Cc = Ca xor Cf.

Nous pouvons donc modifier notre méthode FormMouseMove de la façon suivante :

procedure TForm1.FormMouseMove(Sender: TObject; Shift: TShiftState; X,
Y: Integer);
var
OldColor: TColor;
begin
if WaitForSecondPoint then
begin
// On passe le crayon en mode pmXor, et on modifie sa couleur.
Canvas.Pen.Mode := pmXor;
OldColor := Canvas.Pen.Color;
Canvas.Pen.Color := ColorToRgb(Canvas.Pen.Color) xor ColorToRgb(Color);

// Si on a tracé une ligne la fois précédente, alors on l'efface.
if ErasePrevious then
begin
Canvas.MoveTo(FirstPoint.X, FirstPoint.Y);
Canvas.LineTo(PreviousPoint.X, PreviousPoint.Y);
end;

// On trace la ligne du premier point au point en cours.
Canvas.MoveTo(FirstPoint.X, FirstPoint.Y);
Canvas.LineTo(X, Y);

// On mémorise le point courant pour l'éventuel effacement.
PreviousPoint := Point(X, Y);

// On a tracé une ligne, il faut l'effacer la prochaine fois.
ErasePrevious := True;

// On restore le mode et la couleur initiale.
Canvas.Pen.Mode := pmCopy;
Canvas.Pen.Color := OldColor;
end;
end;

Le code modifié est en rouge. Vous noterez qu'on utilise la fonction ColorToRgb pour convertir les couleurs au format Delphi en couleur au format Window, ceci pour éviter les problèmes avec les couleurs déterminées à l'exécution (clBtnFace, clWindowText,etc).

Ce code fonctionne parfaitement… sauf avec un affichage qui utilise une palette. En effet dans ce cas, ce ne sont pas des couleurs qui sont manipulées, mais des index dans la palette système. C'est complètement illogique et on peut dire que sur ce coup-là, les programmeurs de chez Microsoft ne se sont pas foulés. Comme il semble impossible de savoir à quelles couleurs correspondent ces index, il nous est impossible de déterminer la couleur à donner au crayon. Cela peut être gênant dans certains cas, car on obtient parfois une couleur pour la ligne élastique qui est similaire à la couleur du fond. La ligne élastique n'est alors plus visible. Pour éviter cela, il faut tester la profondeur de couleur. Vous pouvez ainsi déterminer si le système utilise une palette ou non. Dans le premier cas, utilisez le mode pmNot pour éviter les problèmes.

if GetDeviceCaps(Canvas.Handle, BITSPIXEL) >= 24
then PenMode := pmXor  // Le système travaille en vraies couleurs, on peut utiliser le mode pmXor.
else PenMode := pmNot; // Le système utilise une palette, on ne peut pas utiliser le mode pmXor.

Pour télécharger le code source de l'exemple suivant (Delphi 2.0), cliquez ici.
Si ce lien ne fonctionne pas chez vous, essayez celui-ci.

Fig. 1 - Le programme exemple.

Vous pouvez modifier la couleur du crayon ou de la fiche dans la méthode FormCreate pour observer le comportement du programme avec différentes couleurs et des profondeurs de couleurs différentes.