¿Por qué Canvas está "oculto" en todos los controles VCL?

Nov 28 2020

Quiero hacer un procedimiento básico que dibuje algo (digamos un triángulo, por simplicidad) en el lienzo de cualquier control (botón, panel, etc.):

procedure DrawTriangle(Control: TCustomControl);

En esta función necesito usar Control.Width & Control.Height para saber qué tan grande es el control. Resulta ser más difícil de lo imaginado porque Canvas está protegido.

Una solución sería obtener el lienzo del control dentro del procedimiento:

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;

Pero parece un desperdicio de CPU crear y destruir un lienzo cada vez que quiero pintar algo ...

Entonces, mis preguntas son:

  1. ¿Por qué el lienzo estaba oculto (protegido) por diseño?
  2. ¿Cómo resolver esto elegantemente (un solo parámetro) y sin desperdiciar CPU?

Ahora estoy anulando el método Paint, pero esto significa duplicar el código de pintura en varios lugares. Por supuesto, DrawTriangle podría recibir más parámetros (Canvas, Control Width / Height, etc.), ... pero bueno ... con un método Paint expuesto, todo habría sido mucho más elegante.

Respuestas

2 AndreasRejbrand Nov 28 2020 at 16:49

En un comentario a la pregunta resulta que

  1. basta con que esta solución se restrinja a los TCustomControldescendientes, y
  2. es lo suficientemente "elegante" si el procedimiento de dibujo puede obtener el lienzo del control de argumento con una simple llamada de función.

Si es así, la siguiente solución es posible:

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

Tenga en cuenta que DrawFrogsolo toma un único parámetro, el control en sí. Y luego puede obtener el lienzo del control mediante una simple llamada a función con muy poca sobrecarga de CPU.

Ejemplo 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.

Dicho todo esto, sin embargo, personalmente seguiría usando el enfoque estándar de pasar un TCanvas(o incluso a HDC) en lugar de un control, junto con algunas dimensiones:

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

Esto me permitirá usarlo también para otros controles (no solo TCustomControldescendientes), así como para lienzos de impresora, etc.

fpiette Nov 28 2020 at 14:27

¿Por qué el lienzo estaba oculto por el diseño?

No realmente escondido, pero en la sección protegida. Para acceder a él, debe derivar una nueva clase de la que le interesa y declarar Canvas como público.

Es privado porque se supone que no debe acceder a él en el nivel de la aplicación.

No necesita instalar su componente si usa una interposerclase en la fuente que la necesita.

También puede considerar anular el Paintmétodo y poner allí su código de dibujo.