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