Why Canvas is "hidden" in all VCL controls?
Asked Answered
A

2

1

I want to do a basic procedure that draws something (let's say a triangle, for simplicity) on any control's (button, panel, etc) canvas:

procedure DrawTriangle(Control: TCustomControl);

In this function I need to use Control.Width & Control.Height to know how big is the control. Turns out to be more difficult than imagined because Canvas is protected.

A solution would be to obtain the canvas of the control inside the procedure:

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;

But seems such a waste of CPU to create and destroy a canvas each time I want to paint something...

So, my questions are:

  1. Why was canvas hidden (protected) by design?
  2. How to solve this elegantly (one single parameter) and without wasting CPU?

Now I am overriding the Paint method, but this means duplication the painting code in several places. Of course, the DrawTriangle could receive more parameters (Canvas, Control Width/Height etc), .... but well... with an exposed Paint method, everything would have been so much more elegant.

Ario answered 28/11, 2020 at 14:7 Comment(9)
You shouldn't be doing that. A control should paint itself.Blender
I am afraid Olivier is right. When are you drawing on the controls? Typically, a window can only be drawn on when you receive its WM_PAINT message. It is not doable (without a lot of problems) to draw on a canvas at some other time (e.g. in an OnClick event handler). For all we know, the control might chose to redraw itself the millisecond after you added your triangle. In addition, many Win32 controls are very tricky to custom draw in the first place.Enginery
The idea here is that if I have a painting procedure with A LOT of code in it, I need to duplicate all that code in several custom controls.... This does not match well with the "re-usability of the code" concept.Ario
@InTheNameOfScience: Now I see what you mean. Then the solution is to have a procedure DrawTriangle(ACanvas: TCanvas; const ARect: TRect) which draws the triangle in the ACanvas canvas at ARect. In fact, this is how all of the GDI works behind the scenes in Win32. See, for instance, the Rectangle function: it takes a HDC, which is the functional equivalent of a TCanvas. (In essence: you don't draw on controls, you draw on "canvases".) I often have s fcns to be able to draw both on scn&to a printer's cvs.Enginery
@AndreasRejbrand - I know. That's what I do (actually I only pass width and height).... But when we are so close to be there... to just pass any damn TCustomControl as parameter, and no extra params :) ... Feels like this "hidden canvas" decision was a bit overzealous.Ario
@InTheNameOfScience: I understand. I can think of a few "solutions", but they all require more work than simply passing two additional parameters! :)Enginery
Well, is not about (hard) work. Passing few extra parameters is not a big deal. It is about elegance... :)Ario
@InTheNameOfScience: (1) Would it be enough if this is restricted to drawing in TCustomControl descendants (custom windowed controls)? (2) Would it be enough if your procedure DrawTriangle(Control: TCustomControl); has a local variable Canvas: TCanvas and Canvas := GetCanvas(Control) as its first line?Enginery
@AndreasRejbrand - I think yes.Ario
E
2

In a comment to the question it turns out that

  1. it is enough for this solution to be restricted to TCustomControl descendants, and
  2. it is "elegant" enough if the drawing procedure can obtain the canvas from the argument control with a simple function call.

If so, the following solution is 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;

Notice that DrawFrog only takes a single parameter, the control itself. And it can then obtain the control's canvas using a simple function call with extremely little CPU overhead.

Full example:

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.

All this being said, however, I would personally still use the standard approach of passing a TCanvas (or even a HDC) instead of a control, together with some dimensions:

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

This will allow me to use it for other controls as well (not only TCustomControl descendants), as well as printer canvases etc.

Enginery answered 28/11, 2020 at 16:49 Comment(1)
FYI, the VCL has a TControlCanvas class for drawing on any TControl. So you shouldn’t need to crack open the TCustomControl.Canvas manually.Onomastics
B
0

Why was canvas hidden by design?

No really hidden, but in protected section. To access it, you have to derive a new class from the one you are interested in and declare Canvas as public.

It is private because you are not supposed to access it at the application level.

You don't need to install your component if you use an interposer class in the source you need it.

You may also consider overriding the Paint method and put your drawing code there.

Benzyl answered 28/11, 2020 at 14:27 Comment(2)
What if I need the Canvas for other reasons? Not for painting on it? For example, I want to truncate a text with ellipsis. For this I need access to canvas to measure the text size.Ario
The reason why you want to access the canvas is independent on how to get access to the canvas! The solution remains what I said: either derive your own class, or use an interposer class. If you have only a couple of sources, interposer is the easiest. If you have a lot of sources using the same component, deriving your own is worth.Benzyl

© 2022 - 2024 — McMap. All rights reserved.