XII. Ajout d'actions personnalisées▲
Nous allons de nouveau aborder un sujet relativement complexe de la création de composants : la création d'actions personnalisées et leur utilisation dans vos composants.
Pour illustrer cela, nous créerons un type d'actions associé à chaque quartier du TCircleChart. Vous aurez d'ailleurs peut-être remarqué que nombre des propriétés de la classe TChartQuarter ressemblaient aux propriétés standards de TAction : c'était bien entendu dans ce but.
XII-A. Système des actions▲
Nous allons tout d'abord voir comment fonctionne le système des actions, comment les composants savent qu'ils doivent actualiser leurs propriétés par rapport à l'action qui leur est associée.
XII-A-1. Les classes en présence▲
Le système des actions est basé sur trois types de classes : les classes d'actions, les classes des composants les utilisant, et les classes de liens d'actions.
Vous connaissez certainement les deux premiers types. C'est le troisième qui est intéressant.
Les classes de liens d'actions (devant hériter de TBasicActionLink, mais en pratique, héritant souvent de TActionLink) se trouvent, si l'on peut dire, entre les deux premiers types : elles effectuent la liaison des propriétés du composant et de son action. C'est une sorte d'interface entre les deux.
En général, chaque composant désirant mettre en œuvre les actions possède sa classe propre de lien d'actions.
Le but des liens d'actions est que les composants n'aient pas besoin de savoir quel type d'action leur est associé, et que les actions n'aient pas besoin de savoir le type des différents composants auxquels elles sont associées.
C'est la classe de lien d'actions, connaissant le type de composant (fixe pour chaque classe de lien en général) et le type d'action, qui effectue tout le travail.
Il est également important de savoir que les types d'actions doivent hériter de TBasicAction, et qu'en pratique ils héritent de TCustomAction.
Pour éclairer les lanternes, rien ne vaut un petit diagramme UML :
XII-A-2. La classe d'action▲
Premièrement, nous allons étudier la classe d'action. Comme dit plus haut, elle doit hériter de TBasicAction. Cette classe, qui hérite elle-même de TComponent, déclare des méthodes et propriétés indispensables à la gestion des actions.
Une propriété intéressante est la propriété ActionComponent de type TComponent qui indique le dernier composant qui a déclenché l'événement OnExecute de l'action.
Une méthode utile est la méthode Execute, qui déclenche l'événement OnExecute.
Cependant, la classe TBasicAction n'est guère utilisable. On hérite pour ainsi dire toujours de la classe TContainedAction, qui est la classe de base pour les actions devant apparaître dans une liste d'action ou un gestionnaire d'actions.
Cette classe apporte notamment une propriété Category qui permet de classer l'action dans la liste ou le gestionnaire.
En outre, on utilise la plupart du temps la classe TCustomAction qui ajoute des propriétés classiques (toutes celles se trouvent dans TAction en fait).
Ainsi, nous hériterons de TCustomAction une classe TCustomChartQuarterAction ; nous spécialiserons encore celle-ci avec la classe TChartQuarterAction, qui en publiera les propriétés.
La classe TCustomChartQuarterAction ajoute deux propriétés à TCustomAction : Percent et ShowText. On y redéfinit aussi la méthode AssignTo de TPersistent pour pouvoir l'assigner à une autre instance de TCustomChartQuarterAction.
La propriété Percent spécifie le pourcentage associé au quartier concerné. La propriété ShowText, quant à elle, indique s'il faut dessiner le texte (comprenant la propriété Caption et une indication de pourcentage) sur le quartier.
type
TCustomChartQuarterAction = class
(TCustomAction)
private
FPercent : Single
;
FShowText : boolean
;
procedure
SetPercent(New : Single
);
procedure
SetShowText(New : boolean
);
protected
procedure
AssignTo(Dest : TPersistent); override
;
public
constructor
Create(AOwner : TComponent); override
;
property
Percent : Single
read
FPercent write
SetPercent;
property
ShowText : boolean
read
FShowText write
SetShowText default
True
;
end
;
TChartQuarterAction = class
(TCustomChartQuarterAction)
published
property
AutoCheck;
property
Caption;
property
Percent;
property
Checked;
property
Enabled;
property
GroupIndex;
property
HelpContext;
property
HelpKeyword;
property
HelpType;
property
Hint;
property
ImageIndex;
property
ShortCut;
property
SecondaryShortCuts;
property
Visible;
property
ShowText;
property
OnExecute;
property
OnHint;
property
OnUpdate;
end
;
L'implémentation du constructeur et de la méthode AssignTo est assez simple. Aussi je me contenterai de vous en donner le code :
constructor
TCustomChartQuarterAction.Create(AOwner : TComponent);
begin
inherited
;
FPercent := 0
.0
;
FShowText := True
;
end
;
procedure
TCustomChartQuarterAction.AssignTo(Dest : TPersistent);
begin
if
Dest is
TCustomChartQuarterAction then
with
TCustomChartQuarterAction(Dest) do
begin
Percent := Self
.Percent;
ShowText := Self
.ShowText;
end
;
inherited
;
end
;
En revanche, les méthodes d'accès aux deux propriétés méritent leur mot d'explication. En effet, ces deux méthodes doivent avertir tous les liens d'actions (héritant de TBasicActionLink) qui référencent cette action. Il faut leur signaler que des changements sont survenus, et qu'il faut actualiser le composant correspondant.
Pour cela, nous bouclons sur la propriété FClients. Nous vérifions d'abord que la valeur retenue est bien un descendant de TActionLink. Ensuite, au cas où c'est aussi un descendant de TChartQuarterActionLink (que nous définirons plus tard), nous appelons respectivement sa méthode SetPercent ou SetShowText.
Finalement, nous appelons la méthode Change pour déclencher l'événement OnChange de l'action au cas où celui-ci serait renseigné.
procedure
TCustomChartQuarterAction.SetPercent(New : Single
);
var
I : integer
;
Link : TActionLink;
begin
if
(New <> FPercent) and
(New >= 0
.0
) then
begin
for
I := 0
to
FClients.Count-1
do
begin
Link := TObject(FClients.List[I]) as
TActionLink;
if
Assigned(Link) and
(Link is
TChartQuarterActionLink) then
TChartQuarterActionLink(Link).SetPercent(New);
end
;
FPercent := New;
Change;
end
;
end
;
procedure
TCustomChartQuarterAction.SetShowText(New : boolean
);
var
I : integer
;
Link : TActionLink;
begin
if
New <> FShowText then
begin
for
I := 0
to
FClients.Count-1
do
begin
Link := TObject(FClients.List[I]) as
TActionLink;
if
Assigned(Link) and
(Link is
TChartQuarterActionLink) then
TChartQuarterActionLink(Link).SetShowText(New);
end
;
FShowText := New;
Change;
end
;
end
;
XII-A-3. La classe de lien d'action▲
La clef du succès des actions, c'est la façon dont est lié un composant à son action.
Comment, en effet, un composant peut-il savoir qu'une propriété de l'action qui lui est associée a changé ? Il doit en effet en être informé afin de mettre à jour sa propriété correspondante ? Autrement dit, par exemple, comment un TMenuItem peut-il savoir que la propriété Checked de son action a changé, et qu'il doit donc modifier la sienne pour refléter les changements ?
Cette réponse est apportée par les classes de liens d'actions, qui viennent s'intercaler si l'on peut dire entre la classe du composant et celle de l'action.
Comme nous l'avons dit plus haut, les classes de liens d'actions sont des classes descendantes de TBasicActionLink, et en pratique de TActionLink. Notre classe de lien d'action TChartQuarterActionLink sera donc déclarée comme suit :
TChartQuarterActionLink = class
(TActionLink)
Ces classes doivent satisfaire trois capacités :
- Référencer le composant contrôlé
- Déterminer si une propriété donnée est concernée par le lien
- Mettre à jour une propriété du composant contrôlé
XII-A-3-a. Posséder une référence vers le composant contrôlé▲
Cette première capacité est implémentée au simple moyen d'une variable protégée :
protected
FClient : TChartQuarter;
Pour permettre aux objets extérieurs de modifier cette propriété, on surcharge la méthode AssignClient, déclarée initialement dans TBasicActionLink.
protected
procedure
AssignClient(AClient : TObject); override
;
Son implémentation est triviale :
procedure
TChartQuarterActionLink.AssignClient(AClient : TObject);
begin
FClient := AClient as
TChartQuarter;
end
;
XII-A-3-b. Déterminer si une propriété donnée est concernée par le lien▲
Afin de savoir si une propriété modifiée dans l'action doit être également modifiée dans le composant, et afin de savoir si la propriété doit être stockée en flux côté composant, il faut, pour chaque propriété de l'action, savoir si elle est liée.
Cela se fait au moyen de méthodes IsXXXLinked, de ce gabarit :
function
IsAutoCheckLinked : boolean
; virtual
;
Pour les méthodes existant déjà dans TActionLink, on les surchargera bien entendu.
Ainsi, TChartQuarterActionLink possède les fonctions suivantes :
function
IsAutoCheckLinked : boolean
; virtual
;
function
IsCaptionLinked : boolean
; override
;
function
IsPercentLinked : boolean
; virtual
;
function
IsCheckedLinked : boolean
; override
;
function
IsEnabledLinked : boolean
; override
;
function
IsHelpContextLinked : boolean
; override
;
function
IsHintLinked: boolean
; override
;
function
IsGroupIndexLinked : boolean
; override
;
function
IsImageIndexLinked : boolean
; override
;
function
IsShortCutLinked : boolean
; override
;
function
IsVisibleLinked : boolean
; override
;
function
IsShowTextLinked : boolean
; virtual
;
function
IsOnExecuteLinked : boolean
; override
;
Leurs implémentations varient selon que la propriété existe ou pas côté composant et côté action, et que leurs valeurs sont identiques de ces deux côtés.
Voici les implémentations de ces fonctions pour TChartQuarterActionLink :
function
TChartQuarterActionLink.IsAutoCheckLinked : boolean
;
begin
Result := (Action is
TCustomAction) and
(FClient.AutoCheck = (Action as
TCustomAction).AutoCheck);
end
;
function
TChartQuarterActionLink.IsCaptionLinked : boolean
;
begin
Result := inherited
IsCaptionLinked and
(FClient.Text = (Action as
TCustomAction).Caption);
end
;
function
TChartQuarterActionLink.IsPercentLinked : boolean
;
begin
Result := (Action is
TCustomChartQuarterAction) and
(FClient.Percent = (Action as
TCustomChartQuarterAction).Percent);
end
;
function
TChartQuarterActionLink.IsCheckedLinked : boolean
;
begin
Result := inherited
IsCheckedLinked and
(FClient.Down = (Action as
TCustomAction).Checked);
end
;
function
TChartQuarterActionLink.IsEnabledLinked : boolean
;
begin
Result := inherited
IsEnabledLinked and
(FClient.Enabled = (Action as
TCustomAction).Enabled);
end
;
function
TChartQuarterActionLink.IsHelpContextLinked : boolean
;
begin
Result := False
;
end
;
function
TChartQuarterActionLink.IsHintLinked : boolean
;
begin
Result := inherited
IsHintLinked and
(FClient.Hint = (Action as
TCustomAction).Hint);
end
;
function
TChartQuarterActionLink.IsGroupIndexLinked : boolean
;
begin
Result := inherited
IsGroupIndexLinked and
(FClient.GroupIndex = (Action as
TCustomAction).GroupIndex);
end
;
function
TChartQuarterActionLink.IsImageIndexLinked : boolean
;
begin
Result := False
;
end
;
function
TChartQuarterActionLink.IsShortCutLinked : boolean
;
begin
Result := False
;
end
;
function
TChartQuarterActionLink.IsVisibleLinked : boolean
;
begin
Result := False
;
end
;
function
TChartQuarterActionLink.IsShowTextLinked : boolean
;
begin
Result := (Action is
TCustomChartQuarterAction) and
(FClient.ShowText = (Action as
TCustomChartQuarterAction).ShowText);
end
;
function
TChartQuarterActionLink.IsOnExecuteLinked : boolean
;
begin
Result := inherited
IsOnExecuteLinked and
(@FClient.OnClick = @Action.OnExecute);
end
;
Remarquez l'utilisation de l'opérateur @ parfois méconnu des programmeurs Delphi. Celui-ci permet de récupérer l'adresse d'une variable. Son utilisation est rarement nécessaire. On l'utilise ici pour pouvoir comparer deux variables de type procédural.
XII-A-3-c. Mettre à jour une propriété du composant contrôlé▲
Il ne reste plus qu'à implémenter les méthodes qui mettent à jour les propriétés côté composant en fonction des modifications effectuées côté action.
Ces fonctions doivent bien entendu d'abord vérifier que la propriété en question est liée, au moyen des méthodes IsXXXLinked vues précédemment.
Certaines de ces fonctions existent déjà au niveau de TActionLink, il faut le cas échéant les surcharger, sinon les déclarer virtual.
procedure
SetAutoCheck(Value : boolean
); override
;
procedure
SetCaption(const
value : string
); override
;
procedure
SetPercent(Value : Single
); virtual
;
procedure
SetChecked(Value : boolean
); override
;
procedure
SetEnabled(Value : boolean
); override
;
procedure
SetHint(const
Value : string
); override
;
procedure
SetShowText(Value : boolean
); virtual
;
procedure
SetOnExecute(Value : TNotifyEvent); override
;
Leur implémentation se résume à la modification d'une propriété de FClient si l'appel correspondant à IsXXXLinked a renvoyé True :
procedure
TChartQuarterActionLink.SetAutoCheck(Value : boolean
);
begin
if
IsAutoCheckLinked then
FClient.AutoCheck := Value;
end
;
procedure
TChartQuarterActionLink.SetCaption(const
Value : string
);
begin
if
IsCaptionLinked then
FClient.Text := Value;
end
;
procedure
TChartQuarterActionLink.SetPercent(Value : Single
);
begin
if
IsPercentLinked then
FClient.Percent := Value;
end
;
procedure
TChartQuarterActionLink.SetChecked(Value : boolean
);
begin
if
IsCheckedLinked then
FClient.Down := Value;
end
;
procedure
TChartQuarterActionLink.SetEnabled(Value : boolean
);
begin
if
IsEnabledLinked then
FClient.Enabled := Value;
end
;
procedure
TChartQuarterActionLink.SetHint(const
Value : string
);
begin
if
IsHintLinked then
FClient.Hint := Value;
end
;
procedure
TChartQuarterActionLink.SetShowText(Value : boolean
);
begin
if
IsShowTextLinked then
FClient.ShowText := Value;
end
;
procedure
TChartQuarterActionLink.SetOnExecute(Value : TNotifyEvent);
begin
if
IsOnExecuteLinked then
FClient.OnClick := Value;
end
;
XII-B. Support des actions par le composant▲
À présent que les classes du système d'action sont implémentées, il ne nous reste plus qu'à faire en sorte que notre composant TChartQuarter supporte les actions.
Contrairement à ce que l'on pourrait penser, la propriété Action que l'on voit en surface dans la VCL ne correspond pas à une variable privée de type TBasicAction. Sa valeur passe par un lien d'action.
XII-B-1. Propriétés ActionLink et Action▲
Nous allons donc déclarer une variable privée de type TChartQuarterActionLink, avec une propriété pour y accéder de manière protégée. Nous déclarerons également une propriété publiée de type TBasicAction, avec un getter et un setter.
TChartQuarter = class
(TCollectionItem)
private
{...}
FActionLink : TChartQuarterActionLink;
{...}
function
GetAction : TBasicAction;
procedure
SetAction(New : TBasicAction);
{...}
protected
{...}
property
ActionLink : TChartQuarterActionLink read
FActionLink write
FActionLink;
public
{...}
published
property
Action : TBasicAction read
GetAction write
SetAction;
{...}
end
;
Le constructeur de TChartQuarter initialisera FActionLink à nil, et le destructeur détruira l'instance si elle est assignée.
constructor
TChartQuarter.Create(Collection : TCollection);
begin
inherited
;
{...}
FActionLink := nil
;
{...}
end
;
destructor
TChartQuarter.Destroy;
begin
{...}
if
Assigned(FActionLink) then
FActionLink.Free;
inherited
;
end
;
La propriété Action de notre TChartQuarter se fera le reflet de la propriété Action de l'instance du lien d'action FActionLink. Le getter en est donc trivial :
function
TChartQuarter.GetAction : TBasicAction;
begin
if
Assigned(FActionLink) then
Result := FActionLink.Action
else
Result := nil
;
end
;
Le setter est légèrement plus compliqué : il doit allouer et libérer l'objet FActionLink en fonction de la nullité de la nouvelle valeur.
procedure
TChartQuarter.SetAction(New : TBasicAction);
begin
if
New = nil
then
FreeAndNil(FActionLink) else
begin
if
not
Assigned(FActionLink) then
FActionLink := GetActionLinkClass.Create(Self
);
FActionLink.Action := New;
end
;
end
;
Le code de SetAction, tel que figuré dans la partie de code ci-dessus, n'est pas définitif. Il sera modifié plusieurs fois d'ici la fin de ce tutoriel, afin de vous présenter pas à pas le pourquoi du comment de ce que nous y plaçons.
Le lecteur attentif aura remarqué que nous créons une instance de GetActionLinkClass. Qu'est-ce que c'est que ça pour un type classe ? Rassurez-vous, ce n'en est pas un. Il s'agit d'une fonction qui renvoie la classe de lien d'action.
Cette fonction renvoie une valeur de type TChartQuarterActionLinkClass, déclarée comme suit :
type
TChartQuarterActionLinkClass = class
of
TChartQuarterActionLink;
L'appel à Create se résoud donc – grâce à la magie des constructeurs virtuels – en un appel au constructeur de la classe renvoyée par GetActionLinkClass. Cela permet aux éventuels descendants de TChartQuarter d'utiliser une classe de lien d'action plus personnalisée.
La méthode GetActionLinkClass est déclarée protégée dans TChartQuarter :
protected
function
GetActionLinkClass : TChartQuarterActionLinkClass; dynamic
;
La directive dynamic n'est pas toujours bien connue. Elle est globalement semblable à virtual, mais privilégie l'espace mémoire utilisé plutôt que la rapidité d'appel. Cette directive doit en principe être choisie en lieu et place de virtual lorsque la méthode concernée a peu de chance d'être surchargée dans les classes enfants. Si vous ne la comprenez pas bien - et c'est le cas de beaucoup -, utilisez toujours virtual, qui est préférable dans 95% des cas.
Vous trouverez des explications supplémentaires dans la FAQ : Qu'est-ce que la DMT ?
Son implémentation est particulièrement simple :
function
TChartQuarter.GetActionLinkClass : TChartQuarterActionLinkClass;
begin
Result := TChartQuarterActionLink;
end
;
XII-B-2. Mettre à jour les propriétés lors du changement d'action▲
Nous allons maintenant définir les méthodes qui permettront de mettre à jour toutes les propriétés d'un coup lorsque l'action elle-même sera modifiée. Rappelons que si c'est une propriété de l'action qui est modifiée, celle-ci le fait savoir à tous ses liens d'action clients, qui se chargent de modifier leurs composants clients.
Pour ce faire, déclarons une méthode ActionChange. Celle-ci accepte en paramètre la nouvelle action sous forme de Sender, ainsi qu'un paramètre booléen CheckDefaults, qui indique si l'on doit vérifier que les valeurs actuelles sont celles par défaut avant de les mettre à jour.
Le seul cas où une telle vérification sera nécessaire est lors du chargement des composants depuis un flux. En effet, il faut veiller à ne pas faire interférer les changements dus à l'action d'avec ceux dus à des propriétés non liées.
Cette méthode est ainsi déclarée :
protected
procedure
ActionChange(Sender : TObject; CheckDefaults : boolean
); dynamic
;
Et voici son implémentation :
procedure
TChartQuarter.ActionChange(Sender : TObject; CheckDefaults : boolean
);
begin
if
Sender is
TCustomAction then
with
TCustomAction(Sender) do
begin
if
not
CheckDefaults or
(Self
.AutoCheck = False
) then
Self
.AutoCheck := AutoCheck;
if
not
CheckDefaults or
(Self
.Text = ''
) then
Self
.Text := Caption;
if
not
CheckDefaults or
(Self
.Down = False
) then
Self
.Down := Checked;
if
not
CheckDefaults or
(Self
.Enabled = True
) then
Self
.Enabled := Enabled;
if
not
CheckDefaults or
(Self
.Hint = ''
) then
Self
.Hint := Hint;
if
not
CheckDefaults or
(Self
.GroupIndex = 0
) then
Self
.GroupIndex := GroupIndex;
if
not
CheckDefaults or
not
Assigned(Self
.OnClick) then
Self
.OnClick := OnExecute;
end
;
if
Sender is
TCustomChartQuarterAction then
with
TCustomChartQuarterAction(Sender) do
begin
if
not
CheckDefaults or
(Self
.Percent = 0
.0
) then
Self
.Percent := Percent;
if
not
CheckDefaults or
(Self
.ShowText = True
) then
Self
.ShowText := ShowText;
end
;
end
;
À nouveau, nous avons eu recours à une directive dynamic. En effet, il est peu probable que cette méthode soit effectivement surchargée dans une classe enfant de TChartQuarter.
Nous allons donc modifier le setter de la propriété Action pour appeler ActionChange lors de l'affectation d'une nouvelle action.
procedure
TChartQuarter.SetAction(New : TBasicAction);
begin
if
New = nil
then
FreeAndNil(FActionLink) else
begin
if
not
Assigned(FActionLink) then
FActionLink := GetActionLinkClass.Create(Self
);
FActionLink.Action := New;
ActionChange(New, csLoading in
New.ComponentState);
end
;
end
;
Notez que nous avons dû utiliser la propriété ComponentState de la nouvelle action. Les éléments de collection tels que TChartQuarter ne dérivant pas de TComponent, ils ne possèdent pas cette source d'informations cruciale.
Nous ne sommes pas encore tirés d'affaire. Rappelez-vous que nous avions déclaré une propriété protégée ActionLink, ce qui permet aux classes enfants de modifier directement sa propriété Action, sans passer par notre setter. Nous devons donc intercepter l'événement OnChange de notre FActionLink pour appeler également ActionChange.
Définissons donc une méthode privée dont la signature est celle d'un TNotifyEvent, qui est le type de l'événement OnChange.
private
procedure
DoActionChange(Sender : TObject);
Son implémentation est un simple appel à ActionChange, avec False pour valeur de CheckDefaults.
procedure
TChartQuarter.DoActionChange(Sender : TObject);
begin
if
Sender = FActionLink then
ActionChange(FActionLink.Action, False
);
end
;
Nous n'avons plus qu'à assigner ce gestionnaire à l'événement OnChange de tout nouveau lien d'action que nous créons :
procedure
TChartQuarter.SetAction(New : TBasicAction);
begin
if
New = nil
then
FreeAndNil(FActionLink) else
begin
if
not
Assigned(FActionLink) then
FActionLink := GetActionLinkClass.Create(Self
);
FActionLink.Action := New;
FActionLink.OnChange := DoActionChange;
ActionChange(New, csLoading in
New.ComponentState);
end
;
end
;
XII-B-3. Down et OnClick : des propriétés à forte interaction▲
Si vous arrivez encore à suivre, vous vous rappellerez peut-être des méthodes SetDown#setdown et Click de notre TChartQuarter. Celles-ci vont devoir être réétudiées ici, car elles opposent une forte interaction avec les autres éléments de la collection.
En effet, la première devait positionner à False la propriété Down de tous les autres quartiers ayant la même valeur de GroupIndex lorsqu'elle était positionnée à True.
La seconde modifiant la propriété Down, ce qui entraîne un appel à SetDown, se voit répercuter l'interaction de cette méthode.
Afin de nous rafraîchir la mémoire, voici l'implémentation actuelle des deux méthodes :
procedure
TChartQuarter.SetDown(New : boolean
);
var
I : integer
;
Quarter : TChartQuarter;
begin
if
New = FDown then
exit;
FDown := New;
if
(FGroupIndex > 0
) and
FDown then
for
I := 0
to
Collection.Count-1
do
begin
Quarter := TChartQuarters(Collection)[I];
if
(Quarter <> Self
) and
(Quarter.FGroupIndex = FGroupIndex) then
Quarter.Down := False
;
end
;
Changed(False
);
end
;
procedure
TChartQuarter.Clic;
begin
if
Enabled then
begin
if
AutoCheck and
((not
Down) or
(GroupIndex = 0
)) then
Down := not
Down;
if
Assigned(FOnClick) then
FOnClick(Self
);
FCircleChart.ClickQuarter(Index
);
end
;
end
;
En quoi les actions viennent-elles poser problème ?
Elles posent problème, car les actions, de leur côté, effectuent également cette interaction, au niveau des actions. Il faut empêcher que cette interaction soit faite des deux côtés, sous peine de risquer des interférences difficiles à gérer.
XII-B-3-a. SetDown▲
Commençons par la méthode SetDown. Son cas est simple, il s'agit juste d'éviter de s'occuper des autres quartiers si la propriété Checked est régie par le mécanisme des actions. Seul le test d'entrée dans la boucle est donc modifié :
if
((FActionLink = nil
) or
(not
FActionLink.IsCheckedLinked)) and
(FGroupIndex > 0
) and
FDown then
Le code complet et définitif est donc celui-ci :
procedure
TChartQuarter.SetDown(New : boolean
);
var
I : integer
;
Quarter : TChartQuarter;
begin
if
New = FDown then
exit;
FDown := New;
if
((FActionLink = nil
) or
(not
FActionLink.IsCheckedLinked)) and
(FGroupIndex > 0
) and
FDown then
for
I := 0
to
Collection.Count-1
do
begin
Quarter := TChartQuarters(Collection)[I];
if
(Quarter <> Self
) and
(Quarter.FGroupIndex = FGroupIndex) then
Quarter.Down := False
;
end
;
Changed(False
);
end
;
XII-B-3-b. Click▲
Nous allons décomposer cette seconde méthode en ces trois instructions, et les faire évoluer une par une.
La première instruction a pour responsabilité d'inverser la propriété Down si la propriété AutoCheck est positionnée à True :
if
AutoCheck and
((not
Down) or
(GroupIndex = 0
)) then
Down := not
Down;
Ici, tout comme pour SetDown, il s'agit juste de vérifier que la propriété AutoCheck n'est pas liée au système des actions. Il faut donc juste ajouter une condition au test :
if
AutoCheck and
(not
Down) or
(GroupIndex = 0
) and
(not
Assigned(ActionLink) or
not
ActionLink.IsAutoCheckLinked) then
Down := not
Down;
La seconde instruction invoque le gestionnaire affecté à l'événement OnClick.
if
Assigned(FOnClick) then
FOnClick(Self
);
Si la propriété OnClick est liée (à OnExecute), il faut appeler la méthode Execute de l'action associée. Celle-ci se chargera d'effectuer les traitements côté action dus à la propriété AutoCheck.
À force de fouilles dans les sources de la VCL, j'ai fini par trouver les bons tests à effectuer. Cependant, leur logique ne m'est pas parvenue, et je ne suis donc pas en mesure de vous expliquer le pourquoi du comment de chacun des tests.
Tout ce que je peux vous donner, c'est le bon code à utiliser. À vous de l'adapter, plus tard, si vous n'avez pas exactement les mêmes besoins :
{ Appeler OnClick s'il est assigné et différent du OnExecute de l'action
associée. Si une action est associée, alors invoquer sa méthode Execute,
sinon, appeler OnClick }
if
Assigned(FOnClick) and
(Action <> nil
) and
(@FOnClick <> @Action.OnExecute) then
FOnClick(Self
)
else
if
not
(csDesigning in
FCircleChart.ComponentState) and
(FActionLink <> nil
) then
FActionLink.Action.Execute
else
if
Assigned(FOnClick) then
FOnClick(Self
);
La troisième instruction se chargeait d'appeler le gestionnaire de clic au niveau de TCircleChart :
FCircleChart.ClickQuarter(Index
);
Au sujet de cette instruction, nous allons devoir – horresco referens – ne rien faire.
Le code complet et définitif de la méthode Click est donc le suivant :
procedure
TChartQuarter.Clic;
begin
if
Enabled then
begin
if
AutoCheck and
((not
Down) or
(GroupIndex = 0
)) and
(not
Assigned(ActionLink) or
not
ActionLink.IsAutoCheckLinked) then
Down := not
Down;
{ Appeler OnClick s'il est assigné et différent du OnExecute de l'action
associée. Si une action est associée, alors invoquer sa méthode Execute,
sinon, appeler OnClick }
if
Assigned(FOnClick) and
(Action <> nil
) and
(@FOnClick <> @Action.OnExecute) then
FOnClick(Self
)
else
if
not
(csDesigning in
FCircleChart.ComponentState) and
(FActionLink <> nil
) then
FActionLink.Action.Execute
else
if
Assigned(FOnClick) then
FOnClick(Self
);
FCircleChart.ClickQuarter(Index
);
end
;
end
;
XII-B-4. Ne pas enregistrer les propriétés liées dans le flux▲
Nous devons maintenant, non seulement pour ne pas stocker des informations inutiles dans le flux, mais aussi, dans ce cas, pour éviter des interférences gênantes, ajouter des clauses stored à nos propriétés.
Celles-ci devront, pour chacune des propriétés que peut contrôler une action, spécifier de stocker la propriété si et seulement si cette propriété n'est pas liée par le système d'action et si sa valeur n'est pas celle par défaut.
Nous n'apprenons ici rien de nouveau, puisque nous avons déjà étudié les spécificateurs de stockage dans la partie I de ce tutoriel.
Ce n'est guère compliqué. Voici les déclarations des méthodes ainsi que les déclarations modifiées des propriétés le nécessitant :
private
function
IsAutoCheckStored : boolean
;
function
IsTextStored : boolean
;
function
IsPercentStored : boolean
;
function
IsDownStored : boolean
;
function
IsEnabledStored : boolean
;
function
IsGroupIndexStored : boolean
;
function
IsHintStored : boolean
;
function
IsShowTextStored : boolean
;
published
property
AutoCheck : boolean
read
FAutoCheck write
FAutoCheck stored
IsAutoCheckStored;
property
Text : string
read
FText write
SetText stored
IsTextStored;
property
Percent : Single
read
FPercent write
SetPercent stored
IsPercentStored;
property
Down : boolean
read
FDown write
SetDown stored
IsDownStored;
property
Enabled : boolean
read
FEnabled write
SetEnabled stored
IsEnabledStored;
property
GroupIndex : integer
read
FGroupIndex write
SetGroupIndex stored
IsGroupIndexStored;
property
Hint : string
read
FHint write
FHint stored
IsHintStored;
property
ShowText : boolean
read
FShowText write
SetShowText stored
IsShowTextStored;
Voici maintenant l'implémentation de ces méthodes. Elles n'ont rien d'extraordinaire, comparé à ce que nous avons déjà vu :
function
TChartQuarter.IsAutoCheckStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsAutoCheckLinked) and
FAutoCheck <> False
;
end
;
function
TChartQuarter.IsTextStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsCaptionLinked) and
(FText <> ''
);
end
;
function
TChartQuarter.IsPercentStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsPercentLinked) and
(FPercent <> 0
.0
);
end
;
function
TChartQuarter.IsDownStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsCheckedLinked) and
(FDown <> False
);
end
;
function
TChartQuarter.IsEnabledStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsEnabledLinked) and
(FEnabled <> True
);
end
;
function
TChartQuarter.IsGroupIndexStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsGroupIndexLinked) and
(FGroupIndex <> 0
);
end
;
function
TChartQuarter.IsHintStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsHintLinked) and
(FHint <> ''
);
end
;
function
TChartQuarter.IsShowTextStored : boolean
;
begin
Result := (not
Assigned(FActionLink) or
not
FActionLink.IsShowTextLinked) and
(FShowText <> True
);
end
;
XII-B-5. Sécuriser la destruction des actions associées▲
Dans la deuxième partie de ce tutoriel, nous avions vu que lorsqu'une propriété d'un composant indiquait un autre composant, il était nécessaire de se prémunir contre la destruction de ce second composant. La section qui en parlait s'intitulait Sécuriser la destruction du contrôle DropControl.
Or, nous nous trouvons ici dans la même situation : un composant (TChartQuarter) en référence un autre (TBasicAction) par l'intermédiaire d'une propriété (Action).
Il est donc nécessaire de sécuriser la destruction de toute action reliée à un TChartQuarter.
Le hic, c'est que TChartQuarter, comme tout autre élément de collection, n'est pas un descendant de TComponent, et ne possède donc pas de méthode Notification. On ne peut d'ailleurs pas non plus passer une instance de cette classe à la méthode FreeNotification de l'action concernée.
Pour remédier à ce problème, nous allons squatter la méthode Notification du TCircleChart associé à notre TChartQuarter. C'est celle-ci qui se chargera de boucler sur ses quartiers et de nullifier les propriétés Action qui référence l'action en cours de destruction :
TCircleChart = class
(TGraphicControl)
protected
procedure
Notification(AComponent : TComponent; Operation : TOperation); override
;
end
;
Son implémentation est on ne peut plus simple :
procedure
TCircleChart.Notification(AComponent : TComponent; Operation : TOperation);
var
I : integer
;
begin
inherited
;
if
Operation = opRemove then
for
I := 0
to
Quarters.Count-1
do
if
AComponent = Quarters[I].Action then
Quarters[I].Action := nil
;
end
;
Il ne reste plus qu'à ajouter l'objet graphique à la liste de notification de toute nouvelle action référencée. Nous modifions donc pour la troisième et dernière fois la méthode SetAction de TChartQuarter.
procedure
TChartQuarter.SetAction(New : TBasicAction);
begin
if
New = nil
then
FreeAndNil(FActionLink) else
begin
if
not
Assigned(FActionLink) then
FActionLink := GetActionLinkClass.Create(Self
);
FActionLink.Action := New;
FActionLink.OnChange := DoActionChange;
ActionChange(New, csLoading in
New.ComponentState);
New.FreeNotification(FCircleChart);
end
;
end
;
Nous n'utilisons pas ici la méthode RemoveFreeNotification en cas de déréférencement d'une action. En effet, il se pourrait (même si c'est illogique) que plusieurs quartiers du même graphique soient attachés à la même action.
Nous avons donc, enfin, terminé notre composant graphique.