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


2 comments:

Сергей О. said...

I tried to use this component for drawing spectra (just general view, not in smaller detailes). PaintBox was good for this, but I needed Antialiasing to make the picture to look smoother.
I could not solve 2 problems:
- The control looks white even if I set its color black. What I can't understand, when I load a spectrum, it draws it OK - yellow spectrum on black background. But the same procedure (with either empty spectrum or spectrum with somw data) is triggered in OnPaint event of my instance of TAcceleratedPaintPanel. But the control is not black but white.
-The other problem, I saw it on your Demo too, is that when resizing the panel, the picture does not change its size. I looked in debugger, the line
ID2D1HwndRenderTarget(FD2DCanvas.RenderTarget).Resize(Size);
is executed when resizing the control (really resizing the form, the control is resizing due to its abchors). And the procedure in OnPaint is executed too, which must draw pic with the new size, but really resizing does not work. In your demo too btw.
Now I use a TPaintBox and the code from Embarcadero docs, where we use both usual Canvas and Direct2D canvas. It works OK, the speed is ok too - about 2-3-4 ms to draw a spectrum. But in the case of using the component it draws about 10 times faster. Maybe because we don't create and then destroy Direct2D canvas when we draw the picture.

Anonymous said...

The component seems not paint correctly. The color is ignored, the caption of Panel is ignored and so on.
Using the component, it always paint black, seems like the D2DCanvas.Brush.Color can't be setted correctly by the inherited Color property, but I don't know how.

Will be nice if we have a update of this post.