I am investigating replacing GDI with Direct2D in some parts of my applications.
To this end, I read the official Embarcadero documentation and created this minimal Direct2D application:
unit Unit1;
interface
uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.Direct2D, D2D1;
type
TForm1 = class(TForm)
procedure FormCreate(Sender: TObject);
procedure FormPaint(Sender: TObject);
private
FCanvas: TDirect2DCanvas;
protected
procedure CreateWnd; override;
procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
procedure WMSize(var Message: TWMSize); message WM_SIZE;
procedure WMEraseBkgnd(var Message: TWMEraseBkgnd); message WM_ERASEBKGND;
public
destructor Destroy; override;
property Canvas: TDirect2DCanvas read FCanvas;
end;
var
Form1: TForm1;
implementation
{$R *.dfm}
procedure TForm1.CreateWnd;
begin
inherited;
FreeAndNil(FCanvas);
FCanvas := TDirect2DCanvas.Create(Handle);
end;
destructor TForm1.Destroy;
begin
FreeAndNil(FCanvas);
inherited;
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
ReportMemoryLeaksOnShutdown := True;
end;
procedure TForm1.FormPaint(Sender: TObject);
var
R: TRect;
S: string;
begin
Canvas.RenderTarget.Clear(D2D1ColorF(clWhite));
R := ClientRect;
S := 'Hello, Direct2D!';
Canvas.TextRect(R, S, [tfSingleLine, tfVerticalCenter, tfCenter]);
Canvas.MoveTo(0, 0);
Canvas.LineTo(ClientWidth, ClientHeight);
Canvas.MoveTo(0, ClientHeight);
Canvas.LineTo(ClientWidth, 0);
end;
procedure TForm1.WMEraseBkgnd(var Message: TWMEraseBkgnd);
begin
Message.Result := 1;
end;
procedure TForm1.WMPaint(var Message: TWMPaint);
var
PaintStruct: TPaintStruct;
begin
BeginPaint(Handle, PaintStruct);
try
if Assigned(FCanvas) then
begin
FCanvas.BeginDraw;
try
Paint;
finally
FCanvas.EndDraw;
end;
end;
finally
EndPaint(Handle, PaintStruct);
end;
end;
procedure TForm1.WMSize(var Message: TWMSize);
var
S: TD2DSizeU;
begin
if Assigned(FCanvas) then
begin
S := D2D1SizeU(ClientWidth, ClientHeight);
ID2D1HwndRenderTarget(FCanvas.RenderTarget).Resize(S);
end;
Invalidate;
inherited;
end;
end.
This is taken directly from the documentation, except for a few improvements:
- I prefer to
FreeAndNil
the canvas before I (re)create it inCreateWnd
. - I prefer to make sure that the canvas is assigned in
WMPaint
. - Since the
ID2D1HwndRenderTarget.Resize
method uses avar
parameter, the version in the documentation doesn't even compile and needs this adjustment. - I want to invalidate the form on resize.
- I respond to
WM_ERASEBKGND
to avoid flickering. - I prefer to free the canvas when the form is destroyed.
- I turn on memory leak reporting.
- I draw some visually impressive graphics.
Interestingly, if I do not free the canvas in the form's destructor, I would expect a memory leak report, but instead I get an AV. This worries me a bit, but since I usually don't leak things, I'll just ignore that part for the moment.
When I compile this using Delphi 10.3.2 and run it on a Microsoft Windows 7 (64-bit, Aero enabled) system with 125% DPI, I get this result:
Although I am mesmerised by the stunning antialiasing of the lines, clearly, this was not the image I had in mind.
It seems like the problem is related to DPI scaling, and it seems like the following simple adjustment resolves the issue:
procedure TForm1.WMPaint(var Message: TWMPaint);
var
PaintStruct: TPaintStruct;
begin
BeginPaint(Handle, PaintStruct);
try
if Assigned(FCanvas) then
begin
FCanvas.BeginDraw;
try
// BEGIN ADDITION
var f := 96 / Screen.PixelsPerInch;
Canvas.RenderTarget.SetTransform(TD2DMatrix3x2F.Scale(f, f, D2D1PointF(0, 0)));
// END ADDITION
Paint;
finally
FCanvas.EndDraw;
end;
end;
finally
EndPaint(Handle, PaintStruct);
end;
end;
But will this work in all circumstances? And this makes it impossible to use the transform facility the normal way in one's OnPaint
, doesn't it? Is there a better solution? What's the right (best-practice) solution?
Update
A different solution that "works on my system" is
procedure TForm1.CreateWnd;
begin
inherited;
FreeAndNil(FCanvas);
FCanvas := TDirect2DCanvas.Create(Handle);
FCanvas.RenderTarget.SetDpi(96, 96); // <-- Add this!
end;
But again, I am not sure if this is the "right" approach.