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:
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.
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.
Post a Comment