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

Partie III : Créer un composant graphique


précédentsommairesuivant

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 :

Image non disponible

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.

 
Sélectionnez
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 :

 
Sélectionnez
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é.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
protected
  procedure AssignClient(AClient : TObject); override;

Son implémentation est triviale :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
protected
  procedure ActionChange(Sender : TObject; CheckDefaults : boolean); dynamic;

Et voici son implémentation :

 
Sélectionnez
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.

 
Sélectionnez
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.

 
Sélectionnez
private
  procedure DoActionChange(Sender : TObject);

Son implémentation est un simple appel à ActionChange, avec False pour valeur de CheckDefaults.

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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é :

 
Sélectionnez
if ((FActionLink = nil) or (not FActionLink.IsCheckedLinked)) and
   (FGroupIndex > 0) and FDown then

Le code complet et définitif est donc celui-ci :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
{ 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
TCircleChart = class(TGraphicControl)
protected
  procedure Notification(AComponent : TComponent; Operation : TOperation); override;
end;

Son implémentation est on ne peut plus simple :

 
Sélectionnez
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.

 
Sélectionnez
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.


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.