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

Partie III : Créer un composant graphique

Partie III : Créer un composant graphique


précédentsommairesuivant

VII. Réagir aux clics sur les quartiers

Puisque nous avons décidé d'ajouter un événement OnClick aux TChartQuarter, il faut bien pouvoir intercepter les clics de souris, et détecter sur quel quartier on a cliqué.

Pour rendre la chose compatible à la norme Windows, nous ne déclencherons l'événement que si on a relâché la souris au-dessus du même quartier que celui sur lequel on l'a enfoncée. C'est le comportement du bouton par exemple.

VII-A. Un événement supplémentaire au niveau de TCircleChart

Puisqu'il est fort possible que l'on veuille intercepter les clics venant de tous les quartiers de la même façon, il serait utile d'avoir un événement générique au niveau de TCircleChart, qui indiquerait en paramètre le quartier sur lequel on a cliqué.

En fait, en plus du traditionnel paramètre Sender, nous utiliserons deux paramètres : un de type integer indiquant l'index du quartier et un de type TChartQuarter indiquant le quartier lui-même. Nous nommerons ce type d'événement TQuarterClickEvent.

 
Sélectionnez
type
  TQuarterClickEvent = procedure(Sender : TObject; Index : integer; Quarter : TChartQuarter) of object;

Nous ajouterons donc un événement OnClickQuarter dans TCircleChart :

 
Sélectionnez
private
  FOnClickQuarter : TQuarterClickEvent;
published
  property OnClickQuarter : TQuarterClickEvent read FOnClickQuarter write FOnClickQuarter;

VII-B. Méthodes ClickQuarter et DoClickQuarter

Si vous vous souvenez bien, nous avions, dans la méthode Click de TChartQuarter, appelé une méthode ClickQuarter de TCircleChart. Nous ne l'avons pas encore définie.

Cette méthode sera protégée et acceptera en paramètre l'index du quartier correspondant.

En outre, nous déclarerons une méthode DoClickQuarter, elle aussi protégée, mais virtuelle, qui acceptera en paramètre l'index du quartier et le quartier lui-même. C'est cette méthode qui se chargera de déclencher l'événement, la méthode ClickQuarter ne faisant que l'appeler elle.

Ce système est une façon de bien séparer les appels lors du déclenchement d'événements. Vous retrouverez souvent ce type d'arrangement dans les sources de la VCL, ou même de la JVCL par exemple.

 
Sélectionnez
protected
  procedure DoClickQuarter(Index : integer; Quarter : TChartQuarter); virtual;

  procedure ClickQuarter(Index : integer);

Comme dit plus haut, la méthode DoClickQuarter doit se charger de déclencher l'événement :

 
Sélectionnez
procedure TCircleChart.DoClickQuarter(Index : integer; Quarter : TChartQuarter);
begin
  if Assigned(FOnClickQuarter) then
    FOnClickQuarter(Self, Index, Quarter);
end;

La méthode ClickQuarter, elle, se charge simplement d'appeler DoClickQuarter, avec un paramètre en plus :

 
Sélectionnez
procedure TCircleChart.ClickQuarter(Index : integer);
begin
  if Index <> -1 then
    DoClickQuarter(Index, Quarters[Index]);
end;

VII-C. Interception des événements souris

Pour intercepter les événements souris, nous surchargerons les méthodes MouseDown et MouseUp que nous avons déjà rencontrée dans la construction du TDropImage.

 
Sélectionnez
protected
  procedure MouseDown(Button : TMouseButton; Shift : TShiftState;
    X, Y : integer); override;
  procedure MouseUp(Button : TMouseButton; Shift : TShiftState;
    X, Y : integer); override;

Pour retenir sur quel quartier on a enfoncé la souris, nous aurons besoin d'une variable privée FClickedQuarter de type integer. Nous l'initialiserons à -1 dans le constructeur.

 
Sélectionnez
private
  FClickedQuarter : integer;

L'implémentation de ces deux méthodes utilise encore une autre méthode PointToQuarterIndex. Cette méthode accepte un paramètre de type TPoint indiquant les coordonnées relatives dans le TCircleChart ; et elle renvoie l'index du quartier qui se trouve à cette position. Nous implémenterons cette méthode un peu plus tard.

Avec cela, l'implémentation de MouseDown et de MouseUp devient triviale :

 
Sélectionnez
procedure TCircleChart.MouseDown(Button : TMouseButton; Shift : TShiftState;
  X, Y : integer);
begin
  if Button = mbLeft then
  begin
    FClickedQuarter := PointToQuarterIndex(Point(X, Y));
    if (FClickedQuarter <> -1) and (not Quarters[FClickedQuarter].Enabled) then
      FClickedQuarter := -1;
  end;
end;

procedure TCircleChart.MouseUp(Button : TMouseButton; Shift : TShiftState;
  X, Y : integer);
begin
  if (Button = mbLeft) and (FClickedQuarter <> -1) then
  begin
    if PointToQuarterIndex(Point(X, Y)) = FClickedQuarter then
      Quarters[FClickedQuarter].Click;
    FClickedQuarter := -1;
  end;
end;

Remarquez que l'on invoque la méthode Click de TChartQuarter qui déclenchera son événement OnClick avant d'appeler la méthode ClickQuarter de TCircleChart, qui, à travers la méthode DoClickQuarter, déclenchera l'événement OnClickQuarter.

VII-D. Les méthodes PointToQuarterIndex et PointToQuarter

Les méthodes PointToQuarterIndex et PointToQuarter permettront de trouver un quartier avec ses coordonnées.

Nous déclarerons ces deux méthodes publiques, puisqu'elles peuvent avoir un intérêt depuis l'application.

 
Sélectionnez
public
  function PointToQuarterIndex(Point : TPoint) : integer;
  function PointToQuarter(Point : TPoint) : TChartQuarter;

VII-D-1. Méthode PointToQuarter

L'implémentation de PointToQuarter est triviale : elle appelle juste PointToQuarterIndex et renvoie le quartier correspondant. Comme PointToQuarterIndex peut renvoyer -1 (si les coordonnées ne se trouvent sur aucun quartier, en dehors du cercle par exemple), il faut tester la valeur de retour de PointToQuarterIndex et renvoyer nil le cas échéant.

 
Sélectionnez
function TCircleChart.PointToQuarter(Point : TPoint) : TChartQuarter;
var Index : integer;
begin
  Index := PointToQuarterIndex(Point);
  if Index = -1 then Result := nil else
    Result := FQuarters[Index];
end;

VII-D-2. Algorithme de la méthode PointToQuarterIndex

À nouveau, nous tombons sur une méthode dont l'implémentation n'est pas simple du tout. L'algorithme lui-même n'est pas trivial.

Nous allons donc, tout comme pour Paint, décrire l'algorithme avant de l'implémenter.

Contrairement à Paint, PointToQuarterIndex accepte un paramètre en entrée et renvoie une valeur en sortie. Signalons-le dans l'algorithme :

 
Sélectionnez
entree :
  Point de type TPoint; // coordonnées informatiquesfinentree;
sortie :
  Result de type integer; // index du quartierfinsortie;

Comme nous utiliserons souvent la valeur 2*Pi dans cet algo, nous déclarerons une constante TwoPi pour cette valeur.

 
Sélectionnez
soit TwoPi une constante <- 2*Pi;

Outre cela, nous aurons besoin de quelques variables :

 
Sélectionnez
soit CenterToPoint de type Single; // disance du centre au pointsoit PtCos de type Single; // cosinus de l'angle au pointsoit PtAngle de type Single; // valeur de l'angle au pointsoient MinAngle et MaxAngle de type Single;
  // valeurs des angles de départ et d'arrivée de chaque quartiersoit Quarter de type TChartQuarter; // quartier courant

Nous pouvons maintenant réellement commencer l'algorithme.

Tout d'abord, il faut transformer les coordonnées informatiques du point en coordonnées mathématiques, afin de pouvoir utiliser les fonctions trigonométriques. Pour des raisons de commodités, nous utiliserons comme origine de repère le centre du contrôle. Il faut donc soustraire la moitié des largeur et hauteur de ces coordonnées. Ensuite, il faut prendre l'opposé de l'ordonnée.

 
Sélectionnez
Point.X <- Point.X - (largeur du contrôle / 2);
Point.Y <- Point.Y - (hauteur du contrôle / 2);
Point.Y <- -Point.Y;

Ensuite, nous calculons la distance du centre au point, donc la distance de l'origine au point, soit la racine carrée de la somme des carrés de ses coordonnées.

 
Sélectionnez
PointToCenter <- racine carrée(Point.X² + Point.Y²);

Si cette valeur est plus grande que Spoke, alors le point est situé en dehors du disque, et il n'est donc forcément sur aucun quartier.

 
Sélectionnez
si PointToCenter est strictement plus grand que Spoke :
  Result <- -1;
  fin du sous-programme;
finsi;

Nous savons maintenant que le point est dans le disque.

Nous allons à présent calculer l'angle au point. Pour cela, commençons par déterminer son cosinus. Celui-ci peut être trouvé en divisant l'abscisse du point par sa distance au centre. En effet, considérant le cercle dont le centre est l'origine et qui contient ce point, son rayon est CenterToPoint et c'est donc un agrandissement du cercle trigonométrique avec un facteur CenterToPoint.

 
Sélectionnez
PtCos <- Point.X / CenterToPoint;

Nous pouvons maintenant déterminer la valeur de l'angle grâce à un arc cosinus. Cependant, si l'ordonnée est négative, alors on n'obtient pas le bon angle, mais son opposé. On règle donc ce petit défaut.

 
Sélectionnez
PtAngle <- ArcCos(PtCos);
si Point.Y est négatif :
  PtAngle <- -PtAngle;
finsi;

Ici, nous allons soustraire la valeur de FBaseAngle (convertie en radian) à la valeur d'angle ainsi obtenue. Il est en effet plus facile de la soustraire ici plutôt que de l'ajouter au premier MaxAngle, contrairement à la méthode Paint, et contrairement à la logique.

 
Sélectionnez
PtAngle <- PtAngle - DegreeToRadian(FBaseAngle);

Il est cependant plus facile de traiter un angle dont on est certain qu'il est compris entre 0 radian (inclus) et TwoPi (exclus).

Pour s'assurer que PtAngle est compris dans cet intervalle, nous allons effectuer une sorte de modulo pour nombres décimaux. Le principe est de diviser PtAngle par TwoPi, d'en prendre l'arrondi vers le bas (pour les puristes, vers l'infini négatif), de multiplier à nouveau cette valeur par TwoPi, et de soustraire le résultat à PtAngle.

 
Sélectionnez
PtAngle <- PtAngle - TwoPi * Floor(PtAngle/TwoPi);

Il ne reste plus qu'à itérer sur les quartiers jusqu'à en trouver un dont PtAngle soit compris entre MinAngle et MaxAngle. Dès qu'on l'a trouvé, on peut arrêter la méthode. Si au bout de l'itération, on n'a trouvé aucune correspondance (cela pourrait arriver si la somme des valeurs des quartiers était inférieure à 100%), on renvoie -1.

 
Sélectionnez
MaxAngle <- 0;
pour chaque Quarter dans Quarters :
  MinAngle <- MaxAngle;
  MaxAngle <- MinAngle + PercentToRadian(Quarter.Percent);
  
  si PtAngle est compris entre MinAngle et MaxAngle :
    Result <- Quarter.Index;
    fin du sous-programme;
  finsi;
finpour;
Result <- -1;

L'algorithme est terminé. Le voici en entier :

 
Sélectionnez
entree :
  Point de type TPoint; // coordonnées informatiquesfinentree;
sortie :
  Result de type integer; // index du quartierfinsortie;

soit TwoPi une constante <- 2*Pi;

soit CenterToPoint de type Single; // disance du centre au pointsoit PtCos de type Single; // cosinus de l'angle au pointsoit PtAngle de type Single; // valeur de l'angle au pointsoient MinAngle et MaxAngle de type Single;
  // valeurs des angles de départ et d'arrivée de chaque quartiersoit Quarter de type TChartQuarter; // quartier courant

Point.X <- Point.X - (largeur du contrôle / 2);
Point.Y <- Point.Y - (hauteur du contrôle / 2);
Point.Y <- -Point.Y;

PointToCenter <- racine carrée(Point.X² + Point.Y²);
si PointToCenter est strictement plus grand que Spoke :
  Result <- -1;
  fin du sous-programme;
finsi;

PtCos <- Point.X / CenterToPoint;
PtAngle <- ArcCos(PtCos);
si Point.Y est négatif :
  PtAngle <- -PtAngle;
finsi;
PtAngle <- PtAngle - DegreeToRadian(FBaseAngle);
PtAngle <- PtAngle - TwoPi * Floor(PtAngle/TwoPi);

MaxAngle <- 0;
pour chaque Quarter dans Quarters :
  MinAngle <- MaxAngle;
  MaxAngle <- MinAngle + PercentToRadian(Quarter.Percent);
  
  si PtAngle est compris entre MinAngle et MaxAngle :
    Result <- Quarter.Index;
    fin du sous-programme;
  finsi;
finpour;
Result <- -1;

VII-D-3. Implémentation de la méthode PointToQuarterIndex

Nous allons maintenant implémenter cet algorithme en Delphi. Cela ne sera pas difficile du tout, la traduction est ici presque littérale.

Commençons les constante et variables.

 
Sélectionnez
soit TwoPi une constante <- 2*Pi;

soit CenterToPoint de type Single; // disance du centre au pointsoit PtCos de type Single; // cosinus de l'angle au pointsoit PtAngle de type Single; // valeur de l'angle au pointsoient MinAngle et MaxAngle de type Single;
  // valeurs des angles de départ et d'arrivée de chaque quartiersoit Quarter de type TChartQuarter; // quartier courant

Voilà ce que ça donne :

 
Sélectionnez
function TCircleChart.PointToQuarterIndex(Point : TPoint) : integer;
const
  TwoPi = 2*Pi;
var CenterToPoint, PtCos : Single;
    PtAngle, MinAngle, MaxAngle : Single;
    Quarter : TChartQuarter;
begin
end;

Le début est on ne peut plus mathématique, et est donc très simple à traduire :

 
Sélectionnez
Point.X <- Point.X - (largeur du contrôle / 2);
Point.Y <- Point.Y - (hauteur du contrôle / 2);
Point.Y <- -Point.Y;

PointToCenter <- racine carrée(Point.X² + Point.Y²);
si PointToCenter est strictement plus grand que Spoke :
  Result <- -1;
  fin du sous-programme;
finsi;

PtCos <- Point.X / CenterToPoint;
PtAngle <- ArcCos(PtCos);
si Point.Y est négatif :
  PtAngle <- -PtAngle;
finsi;
PtAngle <- PtAngle - DegreeToRadian(FBaseAngle);
PtAngle <- PtAngle - TwoPi * Floor(PtAngle/TwoPi);

En voici la traduction :

 
Sélectionnez
dec(Point.X, Width  div 2);
dec(Point.Y, Height div 2);
Point.Y := -Point.Y;

CenterToPoint := Sqrt(Point.X*Point.X + Point.Y*Point.Y);
if CenterToPoint > Spoke then
begin
  Result := -1;
  exit;
end;

PtCos := Point.X / CenterToPoint;
PtAngle := ArcCos(PtCos);
if Point.Y < 0 then
  PtAngle := -PtAngle;
PtAngle := PtAngle - FBaseAngle * Pi / 180;
PtAngle := PtAngle - TwoPi * Floor(PtAngle/TwoPi);

Pour l'implémentation de la boucle, on utilisera Result comme variable de contrôle de boucle. Cependant, cela nous empêche d'utiliser une boucle for, puisque la variable de contrôle de ce type de boucle est souvent remplacée par le registre ax, la valeur de Result peut être indéfinie. Nous utiliserons donc une boucle while.

 
Sélectionnez
MaxAngle <- 0;
pour chaque Quarter dans Quarters :
  MinAngle <- MaxAngle;
  MaxAngle <- MinAngle + PercentToRadian(Quarter.Percent);
  
  si PtAngle est compris entre MinAngle et MaxAngle :
    Result <- Quarter.Index;
    fin du sous-programme;
  finsi;
finpour;
Result <- -1;

L'intérieur de la boucle étant très simple, je ne m'étendrai pas dessus.

 
Sélectionnez
MaxAngle := 0;
Result := 0;
while Result < FQuarters.Count do
begin
  Quarter := Quarters[Result];

  MinAngle := MaxAngle;
  MaxAngle := MinAngle + (Quarter.Percent * TwoPi / 100);

  if (MinAngle < PtAngle) and (PtAngle < MaxAngle) then exit;

  inc(Result);
end;
Result := -1;

Cette traduction n'était pas aussi difficile que celle de Paint. Voici le code complet de la méthode PointToQuarterIndex :

 
Sélectionnez
function TCircleChart.PointToQuarterIndex(Point : TPoint) : integer;
const
  TwoPi = 2*Pi;
var CenterToPoint, PtCos : Single;
    PtAngle, MinAngle, MaxAngle : Single;
    Quarter : TChartQuarter;
begin
  dec(Point.X, Width  div 2);
  dec(Point.Y, Height div 2);
  Point.Y := -Point.Y;

  CenterToPoint := Sqrt(Point.X*Point.X + Point.Y*Point.Y);
  if CenterToPoint > Spoke then
  begin
    Result := -1;
    exit;
  end;

  PtCos := Point.X / CenterToPoint;
  PtAngle := ArcCos(PtCos);
  if Point.Y < 0 then
    PtAngle := -PtAngle;
  PtAngle := PtAngle - FBaseAngle * Pi / 180;
  PtAngle := PtAngle - TwoPi * Floor(PtAngle/TwoPi);

  MaxAngle := 0;
  Result := 0;
  while Result < FQuarters.Count do
  begin
    Quarter := Quarters[Result];

    MinAngle := MaxAngle;
    MaxAngle := MinAngle + (Quarter.Percent * TwoPi / 100);

    if (MinAngle < PtAngle) and (PtAngle < MaxAngle) then exit;

    inc(Result);
  end;
  Result := -1;
end;

précédentsommairesuivant

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