Using the Direct2D Canvas

From RAD Studio
Jump to: navigation, search

Go Up to Using Canvases for Drawing


Microsoft Windows 7 introduced a new feature that allows creating canvas objects that use the Direct2D technology for rendering and drawing. Using Direct2D may be beneficial in certain circumstances where you have an application that does a lot of custom drawing. Direct2D forwards all drawing operations to the GPU instead of the CPU and that means more power available to your application. This topic discusses how to take advantage of the new Direct2D canvas in your Delphi application.


First steps

The Direct2D canvas is only supported on Windows 7, so be sure that you are developing your Direct2D-enabled application on this version of Windows. To use the Direct2D, you must include the following units in your application:

  • Vcl.Direct2D, which exposes VCL wrapper classes such as TDirect2DCanvas.
  • D2D1, which contains header translations for Microsoft Direct2D API.

It is important to note that even though the VCL provides the necessary classes, you still need to use Direct2D interfaces to benefit from the most advanced features that make the Direct2D canvas useful.

Normal VCL controls are not Direct2D-enabled by default. They are using the old GDI-based Vcl.Graphics.TCanvas object. To use the new technology, you must explicitly add support for Vcl.Direct2D.TDirect2DCanvas to your controls.

Checking for Direct2D canvas support

For your convenience, the TDirect2DCanvas supports a class method called Supported that can be used to determine whether the operating system on which the application runs supports the Direct2D canvas. Even so, an exception can be thrown in certain circumstances while creating the Direct2D canvas. The best way to check for Direct2D availability is when the Direct2D canvas is about to be created:

 function Create2D2Canvas(): Boolean;
 begin
  Result := false;
 
  if TDirect2DCanvas.Supported then
  begin
    try
      FD2DCanvas := TDirect2DCanvas.Create(Handle);
      Result := true;
    except
    end;
  end;
 
 end;

Using Direct2D Canvas

There are two practical ways in which you can take advantage of Direct2D:

  • Sharing the drawing surface between the original GDI canvas and the new Direct2D canvas.
  • Using the Direct2D canvas exclusively.

Both methods have advantages and disadvantages of their own, so you must decide on the best solution to use at the moment.


Using Direct2D canvas alongside GDI canvas

The easiest way to get started using Direct2D is by implementing custom painting on your form or control. This is achieved by writing a OnPaint event handler or overriding the Paint method:

 procedure T2D2Form.FormPaint(Sender: TObject);
 var
   LCanvas: TDirect2DCanvas;
 begin
   LCanvas := TDirect2DCanvas.Create(Canvas, ClientRect);
   LCanvas.BeginDraw;
 
   try
     { Drawing goes here }
     LCanvas.Brush.Color := clRed;
     LCanvas.Pen.Color := clBlue;
     LCanvas.Rectangle(100, 100, 200, 200);
 
   finally
     LCanvas.EndDraw;
     LCanvas.Free;
   end;
 end;

The previous example uses an event handler FormPaint assigned to the form's OnPaint event. OnPaint is triggered each time the form must paint its surface. Note that, for each repaint, a new TDirect2DCanvas instance is created. This is necessary because the actual device context (HDC) associated with the form may change each time, invalidating any TDirect2DCanvas instances previously created.

While this method is the simplest one, in practice it may be unacceptable, because Direct2D resources have to be acquired and released on each repaint. The associated Pen, Brush, and Font objects have to be released, because they can only be used with the original TDirect2DCanvas instance that created them. The advantages of this method is that you can use the original TCanvas alongside with TDirect2DCanvas. Even though both canvas objects paint on the same surface, you can use them both at the same time without much hassle.

Using Direct2D canvas exclusively

The second method is useful when you want to create a single instance of TDirect2DCanvas, whose lifetime is tied to the owning component. In this case, you must catch the WM_PAINT and WM_SIZE windows messages to perform your own drawing routines:

 type
   T2D2Form = class(TForm)
   private
     FCanvas: TDirect2DCanvas;
 
   protected
     procedure CreateWnd; override; 
 
     procedure WMPaint(var Message: TWMPaint); message WM_PAINT; 
     procedure WMSize(var Message: TWMSize); message WM_SIZE;
   public
     property Canvas: TDirect2DCanvas read FCanvas;
 
   end;
 
 implementation
 
 {$R *.dfm}
 
 procedure T2D2Form.CreateWnd;
 begin
   inherited;
   FCanvas := TDirect2DCanvas.Create(Handle);
 end;
 
 procedure T2D2Form.WMPaint(var Message: TWMPaint);
 var
   PaintStruct: TPaintStruct;
 begin
   BeginPaint(Handle, PaintStruct);
 
   try
     FCanvas.BeginDraw;
     try
       Paint;
     finally
       FCanvas.EndDraw;
     end;
   finally
     EndPaint(Handle, PaintStruct); 
   end; 
 end;
 
 procedure T2D2Form.WMSize(var Message: TWMSize);
 begin
   if Assigned(FCanvas) then
     ID2D1HwndRenderTarget(FCanvas.RenderTarget).Resize(D2D1SizeU(ClientWidth, ClientHeight)); 
 
   inherited;
 end;

In the previous example, we wrote custom message handlers for WM_PAINT and WM_SIZE Windows messages. WM_PAINT must be custom processed, because we override VCL painting methods directly. WM_SIZE is necessary to adjust the size of the Direct2D canvas before VCL methods are called. And finally, we create the Vcl.Direct2D.TDirect2DCanvas instance in the CreateWnd method, because we must have a valid window handle to associate the canvas with.

API Compatibility

Both TCanvas and TDirect2DCanvas derive from a common ancestor, TCustomCanvas, that defines the high-level functionality that both canvas implementations must support. In some cases though, TDirect2DCanvas does not implement some functions--for example, the TCustomCanvas.Pixels property; there is no way to obtain pixel information for Direct2D canvases. You should consult the API documentation for Vcl.Direct2D.TDirect2DCanvas to see the functions that are supported and those that are not.

Both canvases have the notion of associated graphics objects like Pen, Brush, and Font. While both canvas implementations expose these objects, they are implemented in completely separate ways, and thus TCustomCanvas does not expose graphical objects at all. This makes it unpractical to expose a TCustomCanvas object if you are designing a component that would support GDI and Direct2D. The following example demonstrates the limitation:

 procedure TForm1.MyBoxPaint(Sender: TObject);
 begin
   { Assuming the Canvas property of TMyPaintBox is TCustomCanvas, 
     to support both canvas implementations }
 
   { Intermediate API to access graphic objects is different }
   if (MyBox.Canvas is TDirect2DCanvas) then
     (MyBox.Canvas as TDirect2DCanvas).Pen.Color := clRed
   else
     (MyBox.Canvas as TCanvas).Pen.Color := clRed;
 
   { Normal high-level API is available }
   MyBox.Canvas.MoveTo(0, 0);
   MyBox.Canvas.LineTo(MyBox.Width, MyBox.Height);
 end;

In the previous example, we had to set the color of the Pen object. Because TCustomCanvas does not expose a Pen property, we had to resort to object casting to access this functionality. Note that GDI Pen and Direct2D Pen are completely different. They do not share a common ancestor; but you can assign a GDI pen to a Direct2D pen, for example.

Developing a Direct2D-enabled component

This section shows how to develop a custom paint box that uses either the Direct2D or the GDI canvas. The component, called TAcceleratedPaintBox, does not derive from Vcl.ExtCtrls.TPaintBox, because TPaintBox itself derives from TControl and not TWinControl. This is an important decision factor--controls derived from TControl do not have a Window Handle, which is required to create a dedicated Direct2D canvas.

The Class Definition

The developed control is derived from Vcl.Controls.TCustomControl (that, in turn, derives from Vcl.Controls.TWinControl) and looks like this:

  TCustomAcceleratedPaintBox = class(TCustomControl)
  private
    FOnPaint: TNotifyEvent;
    FUseD2D: Boolean;
    FD2DCanvas: TDirect2DCanvas;
 
    function CreateD2DCanvas: Boolean;
 
    { Catching paint events }
    procedure WMPaint(var Message: TWMPaint); message WM_PAINT;
    procedure WMSize(var Message: TWMSize); message WM_SIZE;
 
    { Set/Get stuff }
    procedure SetAccelerated(const Value: Boolean);
    function GetGDICanvas: TCanvas;
    function GetOSCanvas: TCustomCanvas;
  protected
    procedure CreateWnd; override;
    procedure Paint; override;
 
  public
    { Life-time management }
    constructor Create(AOwner: TComponent); override;
    destructor Destroy(); override;
 
    { Public properties }
    property Accelerated: Boolean read FUseD2D write SetAccelerated;
    property Canvas: TCustomCanvas read GetOSCanvas;
    property GDICanvas: TCanvas read GetGDICanvas;
    property D2DCanvas: TDirect2DCanvas read FD2DCanvas;
 
    { The Paint event }
    property OnPaint: TNotifyEvent read FOnPaint write FOnPaint;
  end;

The general layout and public interface try to mimic the ones exposed by Vcl.ExtCtrls.TPaintBox, because we are developing a drop-in replacement for it.

Implementing the initialization functionality

The CreateD2DCanvas method attempts to create a TDirect2DCanvas instance and returns a Boolean value indicating whether this went fine or failed. CreateD2DCanvas is later used to initialize the Direct2D canvas.

 function TCustomAcceleratedPaintBox.CreateD2DCanvas: Boolean;
 begin
   try
     FD2DCanvas := TDirect2DCanvas.Create(Handle);
   except
     { Failed creating the D2D canvas, halt! }
     Exit(false);
   end;
 
   Result := true;
 end;

The CreateWnd is overridden from the ancestor class. CreateWnd is invoked automatically in the component creation process. This is the perfect spot to implement the creation of the Direct2D-enabled canvas.

 procedure TCustomAcceleratedPaintBox.CreateWnd;
 begin
   inherited;
 
   { Try to create the custom canvas }
   if (Win32MajorVersion >= 6) and (Win32Platform = VER_PLATFORM_WIN32_NT) then
     FUseD2D := CreateD2DCanvas
   else
     FUseD2D := false;
 end;

The FUseD2D Boolean field is used later in the component to select either the GDI or the Direct2D canvas.

Property getter methods

GetGDICanvas is used internally to access the inherited Canvas property that returns the original GDI canvas. It also serves as a getter method for the GDICanvas property.

 function TCustomAcceleratedPaintBox.GetGDICanvas: TCanvas;
 begin
   if FUseD2D then
     Result := nil
   else
     Result := inherited Canvas;
 end;

GetOSCanvas is the getter method for the Canvas property exposed by this component. It checks whether the Direct2D support is available and returns the appropriate canvas object.

 function TCustomAcceleratedPaintBox.GetOSCanvas: TCustomCanvas;
 begin
   if FUseD2D then
     Result := FD2DCanvas
   else
     Result := inherited Canvas;
 end;

SetAccelerated is the setter method for the Accelerated property. The user can either disable or enable the Direct2D canvas support at run time.

 procedure TCustomAcceleratedPaintBox.SetAccelerated(const Value: Boolean);
 begin
   { Same value? }
   if Value = FUseD2D then
     Exit;
 
   if not Value then
   begin
     FUseD2D := false;
     Repaint;
   end else
   begin
     FUseD2D := FD2DCanvas <> nil;
 
     if FUseD2D then
       Repaint;
   end;
 end;

Painting and Resizing

The Paint method basically replicates the TPaintBox.Paint method's behavior. It prepares the canvas object for painting and invokes the user-supplied event handler that will do the actual drawing. One thing to note in this implementation is that it verifies whether the Direct2D is enabled and uses the D2DCanvas property, falling back to GDICanvas if Direct2D is not supported.

 procedure TCustomAcceleratedPaintBox.Paint;
 begin
   if FUseD2D then
   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;
   end else
   begin
     GDICanvas.Font.Assign(Font);
     GDICanvas.Brush.Color := Color;
  
     if csDesigning in ComponentState then
     begin
       GDICanvas.Pen.Style := psDash;
       GDICanvas.Brush.Style := bsSolid;
  
       GDICanvas.Rectangle(0, 0, Width, Height);
     end;
   end;
  
   if Assigned(FOnPaint) then FOnPaint(Self);
 end;

As described in previous sections, it is important to implement a message handler for the WM_PAINT Windows message, in order to override the VCL-supplied painting routines. Our WMPaint implementation processes the WM_PAINT message only if the Direct2D canvas is enabled; otherwise, the message is passed to the inherited method.

 procedure TCustomAcceleratedPaintBox.WMPaint(var Message: TWMPaint);
 var
   PaintStruct: TPaintStruct;
 begin
   if FUseD2D then
   begin
     BeginPaint(Handle, PaintStruct);
     try
       FD2DCanvas.BeginDraw;
  
       try
         Paint;
       finally
         FD2DCanvas.EndDraw;
       end;
  
     finally
       EndPaint(Handle, PaintStruct);
     end;
   end else
     inherited;
 end;

The WMSize method is used to process the WM_SIZE Windows message. Its purpose is to update the size of the Direct2D canvas.

 procedure TCustomAcceleratedPaintBox.WMSize(var Message: TWMSize);
 begin
   if FD2DCanvas <> nil then
     ID2D1HwndRenderTarget(FD2DCanvas.RenderTarget).Resize(D2D1SizeU(Width, Height));
  
   inherited;
 end;

See Also