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.
type
TQuarterClickEvent = procedure
(Sender : TObject; Index
: integer
; Quarter : TChartQuarter) of
object
;
Nous ajouterons donc un événement OnClickQuarter dans TCircleChart :
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.
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 :
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 :
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.
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.
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 :
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.
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.
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 :
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.
soit TwoPi une constante <- 2*Pi;
Outre cela, nous aurons besoin de quelques variables :
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.
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.
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.
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.
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.
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.
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.
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.
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 :
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.
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 :
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 :
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 :
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.
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.
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 :
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
;