August 29, 2020

Direct2D canvas for Delphi forms

In this blog post, I will show you how easy it is the have a Direct2D canvas for your Delphi form.

What is a canvas?
 

In Delphi VCL technology, a canvas is an abstraction encapsulating Windows API to render content on screen. In VCL, the standard canvas is implemented using GDI. It allows the developer to draw anything on screen. The class that encapsulate a canvas is names TCanvas. Every form has a TCanvas instance. You use it from the form’s OnPaint event handler. Similarly, all VCL component which are able to render something on screen has a TCanvas instance and a Paint method you can override to draw your own content.

TCanvas has method and properties to render many graphic primitives such as lines, rectangles, ellipses, polygons, bitmaps and text. There are properties such as Brush and Pen to select how you want something to be rendered. For example, a Pen is used to select the color, width and style of the rectangle outline while a Brush is use to select the color used to paint the rectangle’s interior.



Why would you like a Direct2D canvas?

Direct2D is a Microsoft DirectX technology especially designed for high performance 2D drawing. This is an API that provides Win32 / Win64 developers with the ability to perform 2D graphics rendering tasks with superior performance and visual quality.

Delphi is exactly a development platform for Win32 / Win64 application and has everything required to use almost any Windows API.

Delphi VCL has a TCanvas based on Direct2D API. It is not created by default but you can easily create it if you need it. But why would you need it?

I see two reasons:
    1. Speed
    2. More rendering features

The aim of this blog post is not to explain all the details of Direct2D. No, it is to show you how easy it is to start using it. Actually, my previous blog post already made use od it to display images. Now, I will show you how to benefit from powerful new Direct2D API: apply transformations to your drawings.


Direct2D transformation are geometric computation inserted between you call of a drawing primitive (For example a rectangle) and the actual rendering (The rectangle appears on screen).

 

The demo

The demo will show a simple transformation: a rotation. The demo has a for-loop which draw the same rectangle 24 times after applying a rotation transformation with an incrementing angle. The result visible on screen is 24 rectangles rotated 15°, drawing a nice picture.





How does it works?

Instead of showing the full code as is, I will explain the steps required to build the application from scratch. You’ll then be able to apply the steps to your own application.

1. Create a VCL form application

2. Add the units Vcl.Direct2D and Winapi.D2D1 to the uses clause.

3. Add the following code to the form’s declaration:

    private
       
FD2DCanvas : TDirect2DCanvas;
        function CreateD2DCanvas: Boolean;
    protected
        procedure CreateWnd; override;

 

 4. Implement CreateD2DCanvas method:

    function TMainForm.CreateD2DCanvas: Boolean;
    begin
        try
           
FD2DCanvas.Free;
           
FD2DCanvas    := TDirect2DCanvas.Create(Handle);
           
Result        := TRUE;
        except
           
Result        := FALSE;
        end;
    end;

5. Implement CreateWnd method:

    procedure TMainForm.CreateWnd;
    begin
        inherited;
        CreateD2DCanvas;
    end;

6. Add an OnResize event handler to your form:

    procedure TMainForm.FormResize(Sender: TObject);
    var
        Size: D2D1_SIZE_U;
    begin
        // When the windows is resized, we needs to resize RenderTarget as well
        Size := D2D1SizeU(ClientWidth, ClientHeight);
        ID2D1HwndRenderTarget(FD2DCanvas.RenderTarget).Resize(Size);
        Invalidate;
    end;

7. Add a OnPaint event handler to your form:

    procedure TMainForm.FormPaint(Sender: TObject);
    var
        Rect1 : D2D1_RECT_F;
       Angle : Single;
       I     : Integer;
    const
        RECT_SIZE  = 50;
        ANGLE_STEP = 15.0;
    begin
        FD2DCanvas.BeginDraw;
        try
            // Erase background
            FD2DCanvas.RenderTarget.Clear(D2D1ColorF(clDkGray));

            // Set pen color to draw rectangle outline
            FD2DCanvas.Pen.Color   := clYellow;

            // Clear all transformations

            FD2DCanvas.RenderTarget.SetTransform(TD2DMatrix3x2F.Identity);

            // Define rectangle to be drawn. Top left corner in center of window
            Rect1                  := Rect((ClientWidth  div 2),
                                           (ClientHeight div 2),
                                           (ClientWidth  div 2) + RECT_SIZE,
                                           (ClientHeight div 2) + RECT_SIZE);
            // Loop drawing the same rectangle but rotated step by step
            for I := 0 to Round(360.0 / ANGLE_STEP) do begin
                Angle := ANGLE_STEP * I;
                FD2DCanvas.RenderTarget.SetTransform(
                             TD2DMatrix3x2F.Rotation(Angle,
                                                     Rect1.Left,
                                                     Rect1.Top));
                FD2DCanvas.DrawRectangle(Rect1);
            end;
        finally
            FD2DCanvas.EndDraw;
        end;
    end;

8. Compile and run your application.

TDirect2DCanvas has almost the same methods and properties as the standard TCanvas. Porting code from standard TCanvas  to TDirect2DCanvas is very easy.

But TDirect2DCanvas is far from implementing all the features of Direct2D API. Fortunately, all the new features – such as transformations – are accessible very easily thru the property RenderTarget.
RenderTarget is an interface implemented in Direct2D DLL. When you call a method of RenderTarget, your are actually calling a Microsoft DLL!

In the demo code you see above, there are two calls to SetTransform. This is how we specify Direct2D to apply one or more transformation. Here we apply a simple rotation.

Transformations are described mathematically by a matrix of 3x2 floating point number. I will not enter the math details here. To help us, Microsoft has prebuilt several matrices for common transformations. Rotation is among them. In the call

     TD2DMatrix3x2F.Rotation(Angle, Rect1.Left, Rect1.Top);

We simply invoke a prebuilt matrix to rotate all subsequent drawings by the specified angle. The rotation take place around the point specified by the second and third arguments.

To cancel any transformation, just set a new transformation. If you want to transformation at all, you can use the matrix names “Identity” which is a kind of do-nothing. The code is:

    FD2DCanvas.RenderTarget.SetTransform(TD2DMatrix3x2F.Identity);

I invite your to see the online help for TDirect2DCanvas at http://docwiki.embarcadero.com/Libraries/Sydney/en/Vcl.Direct2D.TDirect2DCanvas

--
François Piette




August 6, 2020

Using Direct2D and GDI+

Direct2D is an API that provides Win32 developers with the ability to perform 2-D graphics rendering tasks with superior performance and visual quality.

Direct2D is a hardware-accelerated. That is it make use of the GPU whenever possible. This is what gives high performance and high-quality rendering for 2-D geometry, bitmaps, and text. But when a GPU is not available, Direct2D transparently fallback to software to replace the missing GPU functions.

The Direct2D API is designed to interoperate with existing code that uses GDI, GDI+, or Direct3D.

Applications that use Direct2D for graphics can deliver higher visual quality than what can be achieved using GDI. Direct2D uses per-primitive antialiasing to deliver smoother looking curves and lines in rendered content. There is also full support for transparency and alpha blending when rendering 2D primitives.

Windows GDI+ is the portion of the Windows operating system that provides two-dimensional vector graphics, imaging, and typography. GDI+ improves on Windows Graphics Device Interface (GDI) (the graphics device interface included with earlier versions of Windows) by adding new features and by optimizing existing features.

In this demo, we will use GDI+ for file I/O of image files and Direct2D for drawing the images on screen.

The demo is – as any good demo – a minimal application aimed at demonstrating how to read an image file (JPG, PNG, …) and show it on screen with zoom/pan/rotate and flip operation.


Demo code organization

For the purpose of this demo, the code is divided in two parts: a VCL component to expose Direct2D screen area and a form showing a user interface where an image file is shown and a few buttons to execute simple operations such as zoom, pan, rotate and flip.


TAcceleratedPaintPanel component

I created this component to have something similar to the standard TPaintBox but using a Direct2D canvas. I named this component TAcceleratedPaintPanel to avoid conflict with any existing component.

The component code is very simple since Delphi VCL already has a Direct2D canvas which hides most of the complexity. So, we simply start from a TPanel, activate his Direct2D canvas and expose an OnPaint method which can then be used on any form making use of TAcceleratedPaintPanel.


unit AcceleratedPaintPanel;


interface


uses

    Winapi.Messages, Winapi.D2D1,

    System.Classes,

    Vcl.Graphics, Vcl.ExtCtrls, Vcl.Direct2D;


type

    TCustomAcceleratedPaintPanel = class(TPanel)

    private

        FD2DCanvas             : TDirect2DCanvas;

        FPrevRenderTarget      : IntPtr;

        FOnPaint               : TNotifyEvent;

        FOnCreateRenderTarget  : TNotifyEvent;

        function  CreateD2DCanvas: Boolean;

        procedure WMEraseBkGnd(var Msg: TMessage); message WM_ERASEBKGND;

        procedure WMPaint(var Msg: TWMPaint); message WM_PAINT;

        procedure WMSize(var Msg: TWMSize); message WM_SIZE;

    protected

        procedure CreateWnd; override;

        function  GetRenderTarget : ID2D1HwndRenderTarget;

        procedure TriggerCreateRenderTarget; virtual;

    public

        destructor Destroy; override;

        procedure Paint; override;

        property D2DCanvas             : TDirect2DCanvas

                                                   read  FD2DCanvas;

        property RenderTarget          : ID2D1HwndRenderTarget

                                                   read  GetRenderTarget;

        property OnPaint               : TNotifyEvent

                                                   read  FOnPaint

                                                   write FOnPaint;

        property OnCreateRenderTarget  : TNotifyEvent

                                                   read  FOnCreateRenderTarget

                                                   write FOnCreateRenderTarget;

    end;


implementation


uses

    Windows, SysUtils, Controls;


destructor TCustomAcceleratedPaintPanel.Destroy;

begin

    FreeAndNil(FD2DCanvas);

    inherited Destroy;

end;


function TCustomAcceleratedPaintPanel.CreateD2DCanvas: Boolean;

begin

    try

        FD2DCanvas := TDirect2DCanvas.Create(Handle);

        Result     := TRUE;

    except

        Result     := FALSE;

    end;

    TriggerCreateRenderTarget;

end;


procedure TCustomAcceleratedPaintPanel.CreateWnd;

begin

    inherited;

    if (Win32MajorVersion < 6) or (Win32Platform <> VER_PLATFORM_WIN32_NT) then

        raise Exception.Create('Your Windows version do not support Direct2D');

    if not CreateD2DCanvas then

        raise Exception.Create('Unable to create Direct2D canvas');

end;


function TCustomAcceleratedPaintPanel.GetRenderTarget: ID2D1HwndRenderTarget;

begin

    if FD2DCanvas <> nil then begin

        Result := FD2DCanvas.RenderTarget as ID2D1HwndRenderTarget;

        if FPrevRenderTarget <> IntPtr(Result) then begin

            FPrevRenderTarget := IntPtr(Result);

            TriggerCreateRenderTarget;

        end;

    end

    else

        Result := nil;

end;


procedure TCustomAcceleratedPaintPanel.TriggerCreateRenderTarget;

begin

    if Assigned(FOnCreateRenderTarget) then

        FOnCreateRenderTarget(Self);

end;


procedure TCustomAcceleratedPaintPanel.Paint;

begin

    D2DCanvas.Font.Assign(Font);

    D2DCanvas.Brush.Color := Color;

    if csDesigning in ComponentState then begin

        D2DCanvas.Pen.Style   := psDash;

        D2DCanvas.Brush.Style := bsSolid;

        D2DCanvas.Rectangle(0, 0, Width, Height);

    end;

    if Assigned(FOnPaint) then

        FOnPaint(Self);

end;


procedure TCustomAcceleratedPaintPanel.WMEraseBkGnd(var Msg: TMessage);

begin

    Msg.Result := 1;

end;


procedure TCustomAcceleratedPaintPanel.WMPaint(var Msg: TWMPaint);

var

    PaintStruct: TPaintStruct;

begin

    BeginPaint(Handle, PaintStruct);

    try

        FD2DCanvas.BeginDraw;

        try

            Paint;

        finally

            FD2DCanvas.EndDraw;

        end;

    finally

        EndPaint(Handle, PaintStruct);

    end;

end;


procedure TCustomAcceleratedPaintPanel.WMSize(var Msg: TWMSize);

var

    Size: D2D1_SIZE_U;

begin

    if FD2DCanvas <> nil then begin

        Size := D2D1SizeU(Width, Height);

        ID2D1HwndRenderTarget(FD2DCanvas.RenderTarget).Resize(Size);

    end;

    inherited;

end;


end


TAcceleratedPaintPanel is the actual component. As always with Delphi, it is the “custom” version with properties and event published. This is the one registered in the component package.

In the source code for this article, you’ll find two packages: one run time and one design time. Before opening the demo project, you must compile both packages and install the design time package. If you open the demo project before installing the design time package, Delphi will complain about mission TAcceleratedPaintPanel component, asking if you want to cancel or ignore. Be sure to select “cancel” because if you select “ignore”, Delphi will remove the missing component from the form and obviously this will break the code!


Loading an image file with GDI+

It is obvious that to display an image file (A picture stored in a JPG or PNG file), the file must be read in memory and then transferred to video memory.

A JPG or PNG file is not a simple array of pixel: they are compressed with or without loss. GDI+ offer functions to read and decompress image files of various well-known formats.

In Delphi winapi implementation there is a class named TGPBitmap which handle reading and writing image file in all supported formats. Opening an image file is as simple as creating an instance of TGPBitmap, passing the file name as argument. When done, to close the file, just destroy the class instance.

This looks like this:

    GPBitmap := TGPBitmap.Create(FileName);

    try

        // Code making use of the image data, removed for

        // simplicity here.

    finally

       FreeAndNil(GPBitmap);

    end;


Preparing a Direct2D bitmap

Once the image file is opened by GDI+, we can ask to access to the uncompressed image data. On the fly, the GDI+ can change the pixel format to suit any need. Direct2D needs pixel in ARGB format, that is 4 bytes per pixel: the usual 3 bytes for red, green and blue components and a 4th byte for the so-called alpha-channel used for transparency.

A simple call to TGPBitmap.LockBits will do:

var

    BitmapBuf        : array of Byte;  

    BmpData          : TBitmapData;

begin

    SetLength(BitmapBuf, GPBitmap.GetHeight * GPBitmap.GetWidth * 4);

    if GPBitmap.LockBits(MakeRect(0, 0,

                                  Integer(GPBitmap.GetWidth),

                                  Integer(GPBitmap.GetHeight)),

                         ImageLockModeRead,

                         PixelFormat32bppARGB,

                         BmpData) <> TStatus.Ok then

        raise Exception.Create('GPBitmap.LockBits failed');

end;


Obviously we need a buffer to hold the pixel data. We use an array of byte for the purpose and we need 4 bytes per pixel (4 x width x height).

When calling LockBits, we have to pass 4 arguments. The first is the region of interest rectangle, which is the full bitmap here. The second is how we intend to use the data in memory, here just read the pixels. The third is the pixel format that we need, not necessarily the same as the pixel format inside the file. LockBits is smart enough to do the conversion for us. And finally, the fourth argument is the buffer where data will be loaded.

Now having the data in memory, we can tell Direct2D to take it into his own bitmap format:

var

    BitmapProp : TD2D1BitmapProperties;

begin

    BitmapProp.DpiX                  := 0;

    BitmapProp.DpiY                  := 0;

    BitmapProp.PixelFormat.Format    := DXGI_FORMAT_B8G8R8A8_UNORM;

    BitmapProp.PixelFormat.AlphaMode := D2D1_ALPHA_MODE_PREMULTIPLIED;


    RenderTarget.CreateBitmap(D2D1SizeU(GPBitmap.GetWidth,

                                        GPBitmap.GetHeight),

                              BmpData.Scan0,

                              BmpData.Stride,

                              BitmapProp,

                              Result);

end;


RenderTarget is an interface from Direct2D API. It is accessible thru Delphi VCL D2DCanvas.


Encapsulating LoadBitmap in a function

What we saw in the previous sections can be encapsulated in a single function taking the filename and producing the Direct2D bitmap we will use each time the UI needs to be redrawn.

It the code below, you’ll notice reference to “transform”. This will be explained later in this document.

function TDirect2DDemoMainForm.LoadBitmap(const FileName : String): Boolean;

var

    GPBitmap     : TGPBitmap;

begin

    try

        GPBitmap := TGPBitmap.Create(FileName);

        try

            if (GPBitmap.GetWidth = 0) or (GPBitmap.GetHeight = 0) then begin

                ShowMessage('Invalid file');

                Result := FALSE;

                Exit;

            end;


            FBitmapToPaint := CreateDirect2DBitmap(

                            AcceleratedPaintPanel1.D2DCanvas.RenderTarget,

                            GPBitmap);

            // Reset transforms to something the user expects

            FFlipHoriz    := FALSE;

            FFlipVert     := FALSE;

            FRotateFactor := 0.0;

            ZoomFull;

            PanCenter;

            ComputeTransform;


            // Calling Invalidate will force the component to redraw which

            // in turn will trigger OnPaint event and our paint handler which

            // draw the image on screen

            AcceleratedPaintPanel1.Invalidate;

            Result := TRUE;

        finally

            FreeAndNil(GPBitmap);

        end;

    except

        on E:Exception do begin

            ShowMessage(E.ClassName + ': ' + E.Message);

            Result := FALSE;

        end;

    end;

end;


Drawing the image

TAcceleratedPaintPanel has an OnPaint event which is triggered each time Windows request the repaint the window. The OnPaint event handler has to draw the image. This is done by calling a single method:

    AcceleratedPaintPanel1.RenderTarget.DrawBitmap(FBitmapToPaint);

RenderTarget is a Direct2D interface exposed by TAcceleratedPaintPanel. This interface has a lot of methods among which DrawBitmap that can draw a Direct2D bitmap on screen. When called with a single argument, the full bitmap is drawn as is on the window top left corner.


Direct2D transforms

Drawing a bitmap on the top left corner is not what we need in most application. We probably want to resize the bitmap so that it fits the window, maybe we want to move it left or right (panning), rotate it, flip it, zoom it. This is where Direct2D transforms comes in play.

A transform specifies how to map the points of an object from one coordinate space to another or from one position to another within the same coordinate space. This mapping is described by a transformation matrix, defined as a collection of three rows with three columns of floating-point values.

This may sound complex. Actually, it is not. We don’t really care about that matrix. It is enough to know how to initialize it to pan, zoom, rotate and flip the bitmap. To combine several transforms, we just have to multiply each individual transform involved. For example, to center a bitmap into the window, we use a scaling matrix (Zoom) and a translation matrix (panning).

Technically, a transform matrix is a record having nine single precision floating point and an operator overloading to multiply two matrices. The result is a matrix representing the two transforms.

There also some record helpers to easily create common transform matrices. For example, to create a transform matrix to zoom by a factor of 2, you write:

    Zoom := TD2DMatrix3x2F.Scale(D2D1SizeF(2.0, 2.0), D2D1PointF(0, 0));

The first argument is the zoom factor along width and height. The second argument is the zoom center.

There are similar helpers for the other transforms. Missing is the flip transform. Flipping around a horizontal or vertical axis is like looking a printed picture by transparency from the reverse side. The matrices are:

    const FlipH : D2D_MATRIX_3X2_F = (

      _11:-1.0; _12: 0.0;

      _21: 0.0; _22: 1.0;

      _31: 0.0; _32: 0.0;);

    const FlipV : D2D_MATRIX_3X2_F = (

      _11: 1.0; _12: 0.0;

      _21: 0.0; _22:-1.0;

      _31: 0.0; _32: 0.0;);


An important note: transform can be combined using the multiply operator but the order is important! Scaling * translating is not the same as translating * scaling.

In the demo application, I use a number of fields to specify the transform to be applied to the bitmap when displayed:

        FRotateFactor : Double;

        FZoomFactor   : Double;

        FPanLeft      : Integer;

        FPanTop       : Integer;

        FFlipHoriz    : Boolean;

        FFlipVert     : Boolean;


And one more field is used to store the result of all combined transforms:

       FTransform   : TD2DMatrix3X2F;

Each time one of the fields value is changed, a new combined transform is computed and then the paint panel is invalidated, forcing a redraw.

procedure TDirect2DDemoMainForm.ComputeTransform;

var

    Scaling      : TD2DMatrix3X2F;

    Translation  : TD2DMatrix3X2F;

    Rotation     : TD2DMatrix3X2F;

    FlippingH    : TD2DMatrix3X2F;

    FlippingHT   : TD2DMatrix3X2F;

    FlippingV    : TD2DMatrix3X2F;

    FlippingVT   : TD2DMatrix3X2F;

    Size         : TD2D1SizeU;

begin

    if not Assigned(FBitmapToPaint) then begin

        FTransform := TD2DMatrix3x2F.Identity;

        Exit;

    end;


    FBitmapToPaint.GetPixelSize(Size);


    if Abs(FZoomFactor - 1.0) <= 1E-5 then

        Scaling := TD2DMatrix3x2F.Identity

    else

        Scaling := TD2DMatrix3x2F.Scale(D2D1SizeF(FZoomFactor, FZoomFactor),

                                        D2D1PointF(0, 0));

    if Abs(FRotateFactor) <= 1E-5 then

        Rotation := TD2DMatrix3x2F.Identity

    else

        Rotation := TD2DMatrix3x2F.Rotation(FRotateFactor,

                                            Size.Width div 2,

                                            Size.Height div 2);


    Translation := TD2DMatrix3x2F.Translation(FPanLeft, FPanTop);


    if not FFlipHoriz then begin

        FlippingH     := TD2DMatrix3x2F.Identity;

        FlippingHT    := TD2DMatrix3x2F.Identity;

    end

    else begin

        FlippingH._11 := -1.0;   FlippingH._12 := 0.0;

        FlippingH._21 :=  0.0;   FlippingH._22 := 1.0;

        FlippingH._31 :=  0.0;   FlippingH._32 := 0.0;

        FlippingHT    := TD2DMatrix3x2F.Translation(-Size.width, 0);

    end;


    if not FFlipVert then begin

        FlippingV     := TD2DMatrix3x2F.Identity;

        FlippingVT    := TD2DMatrix3x2F.Identity;

    end

    else begin

        FlippingV._11 :=  1.0;   FlippingV._12 :=  0.0;

        FlippingV._21 :=  0.0;   FlippingV._22 := -1.0;

        FlippingV._31 :=  0.0;   FlippingV._32 :=  0.0;

        FlippingVT    := TD2DMatrix3x2F.Translation(0, -Size.Height);

    end;


    FTransform := FlippingVT * FlippingV *

                  FlippingHT * FlippingH *

                  Rotation   * Scaling   * Translation;

end;


Flipping are combined with a translation so that the flip axis remains in the middle of the bitmap.

Now that we have transforms, we can see the complete OnPaint event handler:

procedure TDirect2DDemoMainForm.AcceleratedPaintBox1Paint(Sender : TObject);

begin

    // Paint background

    AcceleratedPaintPanel1.RenderTarget.Clear(D2D1ColorF(clSilver));

    // Paint bitmap, if any

    if FBitmapToPaint <> nil then begin

        AcceleratedPaintPanel1.RenderTarget.SetTransform(FTransform);

        AcceleratedPaintPanel1.RenderTarget.DrawBitmap(FBitmapToPaint);

    end;

end;

 


Source code

Full source code is available on Github. It is subject to Mozilla Public License V2.0.

https://github.com/fpiette/Direct2DDemo

François Piette

Embarcadero MVP