Pourquoi Canvas est-il «caché» dans tous les contrôles VCL?

Nov 28 2020

Je veux faire une procédure de base qui dessine quelque chose (disons un triangle, pour plus de simplicité) sur le canevas de n'importe quel contrôle (bouton, panneau, etc.):

procedure DrawTriangle(Control: TCustomControl);

Dans cette fonction, j'ai besoin d'utiliser Control.Width & Control.Height pour savoir quelle est la taille du contrôle. Cela s'avère plus difficile qu'on ne l'imaginait car Canvas est protégé.

Une solution serait d'obtenir le canevas du champ à l'intérieur de la procédure:

VAR
   ParentControl: TWinControl;
   canvas: TCanvas;
begin
 ParentControl:= Control.Parent;
 Canvas:= TCanvas.Create;
 TRY
  Canvas.Handle:= GetWindowDC(ParentControl.Handle);
  WITH Canvas DO
    xyz
 FINALLY
   FreeAndNil(canvas);
 END;
end;

Mais cela me semble un tel gaspillage de CPU pour créer et détruire une toile à chaque fois que je veux peindre quelque chose ...

Donc, mes questions sont:

  1. Pourquoi la toile était-elle cachée (protégée) par la conception?
  2. Comment résoudre cela avec élégance (un seul paramètre) et sans gaspiller de CPU?

Maintenant, je remplace la méthode Paint, mais cela signifie dupliquer le code de peinture à plusieurs endroits. Bien sûr, le DrawTriangle pourrait recevoir plus de paramètres (Canvas, Control Width / Height etc), .... mais bon ... avec une méthode Paint exposée, tout aurait été tellement plus élégant.

Réponses

2 AndreasRejbrand Nov 28 2020 at 16:49

Dans un commentaire à la question, il s'avère que

  1. il suffit que cette solution soit réservée aux TCustomControldescendants, et
  2. c'est assez "élégant" si la procédure de dessin peut obtenir le canevas du contrôle d'argument avec un simple appel de fonction.

Si tel est le cas, la solution suivante est possible:

//
// Infrastructure needed
//

type
  TCustomControlCracker = class(TCustomControl)
  end;

function CustomControlCanvas(AControl: TCustomControl): TCanvas;
begin
  Result := TCustomControlCracker(AControl).Canvas;
end;

//
// My reusable drawing functions
// (Can only be used in TCustomControl descendants)
//

procedure DrawFrog(AControl: TCustomControl);
var
  Canvas: TCanvas;
begin
  Canvas := CustomControlCanvas(AControl);
  Canvas.TextOut(10, 10, 'Frog');
end;

Notez que DrawFrogne prend qu'un seul paramètre, le contrôle lui-même. Et il peut ensuite obtenir le canevas du contrôle à l'aide d'un simple appel de fonction avec une surcharge du processeur extrêmement faible.

Exemple complet:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.ExtCtrls, Vcl.StdCtrls;

type
  TForm1 = class(TForm)
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

type
  TTestControl = class(TCustomControl)
  protected
    procedure Paint; override;
  end;

type
  TCustomControlCracker = class(TCustomControl)
  end;

function CustomControlCanvas(AControl: TCustomControl): TCanvas;
begin
  Result := TCustomControlCracker(AControl).Canvas;
end;

procedure DrawFrog(AControl: TCustomControl);
var
  Canvas: TCanvas;
begin
  Canvas := CustomControlCanvas(AControl);
  Canvas.TextOut(10, 10, 'Frog');
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  with TTestControl.Create(Self) do
  begin
    Parent := Self;
    Top := 100;
    Left := 100;
    Width := 400;
    Height := 200;
  end;
end;

{ TTestControl }

procedure TTestControl.Paint;
begin
  inherited;
  Canvas.Brush.Color := clSkyBlue;
  Canvas.FillRect(ClientRect);
  DrawFrog(Self); // use my reusable frog-drawing function
end;

end.

Tout cela étant dit, cependant, j'utiliserais personnellement toujours l'approche standard consistant à passer un TCanvas(ou même un HDC) au lieu d'un contrôle, ainsi que certaines dimensions:

procedure DrawFrog(ACanvas: TCanvas; const ARect: TRect);

Cela me permettra de l'utiliser également pour d'autres contrôles (pas seulement pour les TCustomControldescendants), ainsi que pour les toiles d'imprimantes, etc.

fpiette Nov 28 2020 at 14:27

Pourquoi la toile était-elle cachée par le design?

Pas vraiment caché, mais en section protégée. Pour y accéder, vous devez dériver une nouvelle classe de celle qui vous intéresse et déclarer Canvas comme public.

Il est privé car vous n'êtes pas censé y accéder au niveau de l'application.

Vous n'avez pas besoin d'installer votre composant si vous utilisez une interposerclasse dans la source dont vous en avez besoin.

Vous pouvez également envisager de remplacer la Paintméthode et d'y placer votre code de dessin.