Por que o Canvas está “escondido” em todos os controles VCL?

Nov 28 2020

Quero fazer um procedimento básico que desenhe algo (digamos um triângulo, para simplificar) em qualquer tela de controle (botão, painel, etc):

procedure DrawTriangle(Control: TCustomControl);

Nesta função, preciso usar Control.Width & Control.Height para saber o tamanho do controle. É mais difícil do que se imaginava porque o Canvas está protegido.

Uma solução seria obter a tela do controle dentro do procedimento:

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;

Mas parece um desperdício de CPU criar e destruir uma tela cada vez que quero pintar algo ...

Então, minhas perguntas são:

  1. Por que a tela foi escondida (protegida) pelo design?
  2. Como resolver isso de forma elegante (um único parâmetro) e sem desperdiçar CPU?

Agora estou substituindo o método Paint, mas isso significa a duplicação do código de pintura em vários lugares. Claro, o DrawTriangle poderia receber mais parâmetros (Canvas, Control Width / Height etc), .... mas bem ... com um método Paint exposto, tudo teria sido muito mais elegante.

Respostas

2 AndreasRejbrand Nov 28 2020 at 16:49

Em um comentário sobre a pergunta, descobrimos que

  1. basta que essa solução seja restrita aos TCustomControldescendentes, e
  2. é "elegante" o suficiente se o procedimento de desenho puder obter a tela do controle de argumento com uma chamada de função simples.

Nesse caso, a seguinte solução é possível:

//
// 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;

Observe que DrawFrogleva apenas um único parâmetro, o próprio controle. E ele pode obter a tela do controle usando uma chamada de função simples com uma sobrecarga de CPU extremamente pequena.

Exemplo completo:

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.

Com tudo isso dito, no entanto, eu pessoalmente ainda usaria a abordagem padrão de passar um TCanvas(ou mesmo um HDC) em vez de um controle, juntamente com algumas dimensões:

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

Isso me permitirá usá-lo para outros controles também (não apenas TCustomControldescendentes), bem como telas de impressão etc.

fpiette Nov 28 2020 at 14:27

Por que a tela foi ocultada por design?

Não realmente escondido, mas na seção protegida. Para acessá-lo, você deve derivar uma nova classe daquela em que está interessado e declarar Canvas como pública.

É privado porque você não deve acessá-lo no nível do aplicativo.

Você não precisa instalar seu componente se usar uma interposerclasse na fonte que você precisa.

Você também pode considerar substituir o Paintmétodo e colocar seu código de desenho lá.