Click here to Skip to main content
15,867,568 members
Articles / Desktop Programming / WPF

Using Direct2D with WPF

Rate me:
Please Sign up or sign in to vote.
4.96/5 (28 votes)
3 Nov 2010CPOL11 min read 188.1K   6.1K   65   38
Hosting Direct2D content in WPF controls.

Introduction

With Windows 7, Microsoft introduced a new technology called Direct2D (which is also supported on Windows Vista SP2 with the Platform Update installed). Looking through all its documentation, you'll notice it's aimed at Win32 developers; however, the Windows API Code Pack allows .NET developers to use the features of Windows 7 easily, with Direct2D being one of the features supported. Unfortunately, all the WPF examples included with the Code Pack require hosting the control in a HwndHost, which is a problem as it has airspace issues. This basically means that the Direct2D control needs to be separated from the rest of the WPF controls, which means no overlapping controls with transparency.

The attached code allows Direct2D to be treated as a normal WPF control and, thanks to some COM interfaces, doesn't require you to download the DirectX SDK or even play around with any C++ - the only dependency is the aforementioned Code Pack (the binaries of which are included in the attached file). This article is more about the problems found along the way the challenges involved in creating the control, so feel free to skip to the Using the code section if you want to jump right in.

Background

WPF architecture

WPF is built on top of DirectX 9, and uses a retained rendering system. What this means is that you don't draw anything to the screen, but instead create a tree of visual objects; their drawing instructions are cached and later rendered automatically by the framework. This, coupled with using DirectX to do the graphics processing, enables WPF applications not only to remain responsive when they have to be redrawn, but also allows WPF to use a "painter's algorithm" painting model. In this model, each component (starting at the back of the display, going towards the front) is asked to draw itself, allowing them to paint over the previous component's display. This is the reason it's so easy to have complex and/or partially transparent shapes with WPF - because it was designed taking this scenario into account. For more information, check out the MSDN article.

Direct2D architecture

In contrast to the managed WPF model, Direct2D is immediate-mode where the developer is responsible for everything. This means you are responsible for creating your resources, refreshing the screen, and cleaning up after yourself. It's built on top of Direct3D 10.1, which gives it high-performance rendering, but provides several of the advantages of WPF (such as device independent units, ClearType text rendering, per primitive anti-aliasing, and solid/linear/radial/bitmap brushes). MSDN has a more in-depth introduction; however, it's more aimed at native developers.

Interoperability

Direct2D has been designed to be easily integrated into existing projects that use GDI, GDI+, or Direct3D, with multiple options available for incorporating Direct2D content with Direct3D 10.1 or above. The Direct2D SDK even includes a nice sample called DXGI Interop to show how to do this.

To host Direct3D content inside WPF, the D3DImage class was introduced in .NET 3.5 SP1. This allows you to host Direct3D 9 content as an ImageSource, enabling it to be used inside an Image control, or as an ImageBrush etc. There's a great article here on CodeProject with more information and examples.

The astute would have noticed that whilst both technologies can work with Direct3D, Direct2D requires version 10.1 or later, whilst the D3DImage in WPF only supports version 9. A quick internet search resulted in this blog post by Jeremiah Morrill. He explains that an IDirect3DDevice9Ex (which is supported by D3DImage) supports sharing resources between devices. A shared render target created in Direct3D 10.1 can therefore be pulled into a D3DImage via an intermediate IDirect3DDevice9Ex device. He also includes example source code which does exactly this, and the attached code is derived from his work.

So, we now have a way of getting Direct2D working with Direct3D 10.1, and we can get WPF working with Direct3D 10.1; the only problem is the dependency of both of the examples on unmanaged C++ code and the DirectX SDK. To get around this problem, we'll access DirectX through its COM interface.

Component Object Model

I'll admit I know nothing about COM, apart from to avoid it! However, there's an article here on CodeProject that helped to make it a bit less scary. To use COM, we have to use low level techniques, and I was surprised (and relieved!) to find that the Marshal class has methods which could mimic anything that would normally have to be done in unmanaged code.

Since there are only a few objects we need from Direct3D 9, and there are only one or two functions in each object that are of interest to us, instead of trying to convert all the interfaces and their functions to their C# equivalent, we'll manually map the V-table as discussed in the linked article. To do this, we'll create a helper function that will extract a method from the specified slot in the V-table:

C#
public static bool GetComMethod<T, U>(T comObj, int slot, out U method) where U : class
{
    IntPtr objectAddress = Marshal.GetComInterfaceForObject(comObj, typeof(T));
    if (objectAddress == IntPtr.Zero)
    {
        method = null;
        return false;
    }

    try
    {
        IntPtr vTable = Marshal.ReadIntPtr(objectAddress, 0);
        IntPtr methodAddress = Marshal.ReadIntPtr(vTable, slot * IntPtr.Size);

        // We can't have a Delegate constraint, so we have to cast to
        // object then to our desired delegate
        method = (U)((object)Marshal.GetDelegateForFunctionPointer(
                             methodAddress, typeof(U)));
        return true;
    }
    finally
    {
        Marshal.Release(objectAddress); // Prevent memory leak
    }
}

This code first gets the address of the COM object (using Marshal.GetComInterfaceForObject), then gets the location of the V-table stored at the start of the COM object (using Marshal.ReadIntPtr), then gets the address of the method at the specified slot from the V-table (multiplying by the system size of a pointer, as Marshal.ReadIntPtr specifies the offset in bytes), then finally creates a callable delegate to the returned function pointer (Marshal.GetDelegateForFunctionPointer). Simple!

An important thing to note is that the IntPtr returned by the call to Marshal.GetComInterfaceForObject must be released; I wasn't aware of this, and found my program leaking memory when the resources were being re-created. Also, the function uses an out parameter for the delegate so we get all the nice benefits of type inference and, therefore, reduces the amount of typing required for the caller. Finally, you'll notice there's some nasty casting to object and then to the delegate type. This is unfortunate but necessary, as there's no way to specify a delegate generic constraint in C# (the CLI does actually allow this constraint, as mentioned by Jon Skeet in his blog). Since this is an internal class, we'll assume that the caller of the function knows this constraint.

With this helper function, it becomes a lot easier to create a wrapper around the COM interfaces, so let's take a look at how to provide a wrapper around the IDirect3DTexture9 interface. First, we'll create an internal interface with the ComImport, Guid, and InterfaceType attributes attached so that the Marshal class knows how to use the object. For guid, we'll need to look inside the DirectX SDK header files, in particular d3d9.h:

C++
interface DECLSPEC_UUID("85C31227-3DE5-4f00-9B3A-F11AC38C18B5") IDirect3DTexture9;

With the same header open, we can also look for the interface's declaration, which looks like this after running it through the pre-processor and removing the __declspec and __stdcall attributes:

C++
struct IDirect3DTexture9 : public IDirect3DBaseTexture9
{
    virtual HRESULT QueryInterface( const IID & riid, void** ppvObj) = 0;
    virtual ULONG AddRef(void) = 0;
    virtual ULONG Release(void) = 0;
    
    virtual HRESULT GetDevice( IDirect3DDevice9** ppDevice) = 0;
    virtual HRESULT SetPrivateData( const GUID & refguid, 
            const void* pData,DWORD SizeOfData,DWORD Flags) = 0;
    virtual HRESULT GetPrivateData( const GUID & refguid, 
            void* pData,DWORD* pSizeOfData) = 0;
    virtual HRESULT FreePrivateData( const GUID & refguid) = 0;
    virtual DWORD SetPriority( DWORD PriorityNew) = 0;
    virtual DWORD GetPriority(void) = 0;
    virtual void PreLoad(void) = 0;
    virtual D3DRESOURCETYPE GetType(void) = 0;
    virtual DWORD SetLOD( DWORD LODNew) = 0;
    virtual DWORD GetLOD(void) = 0;
    virtual DWORD GetLevelCount(void) = 0;
    virtual HRESULT SetAutoGenFilterType( D3DTEXTUREFILTERTYPE FilterType) = 0;
    virtual D3DTEXTUREFILTERTYPE GetAutoGenFilterType(void) = 0;
    virtual void GenerateMipSubLevels(void) = 0;
    virtual HRESULT GetLevelDesc( UINT Level,D3DSURFACE_DESC *pDesc) = 0;
    virtual HRESULT GetSurfaceLevel( UINT Level,IDirect3DSurface9** ppSurfaceLevel) = 0;
    virtual HRESULT LockRect( UINT Level,D3DLOCKED_RECT* pLockedRect, 
            const RECT* pRect,DWORD Flags) = 0;
    virtual HRESULT UnlockRect( UINT Level) = 0;
    virtual HRESULT AddDirtyRect( const RECT* pDirtyRect) = 0;
};

We only need one of these methods for our code, which is the GetSurfaceLevel method. Starting from the top and counting down, we can see that this is the 19th method, so will therefore be at slot 18 in the V-table. We can now create a wrapper class around this interface.

C#
internal sealed class Direct3DTexture9 : IDisposable
{
    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    private delegate int GetSurfaceLevelSignature(IDirect3DTexture9 texture, 
                         uint Level, out IntPtr ppSurfaceLevel);

    [ComImport, Guid("85C31227-3DE5-4f00-9B3A-F11AC38C18B5"), 
                InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    internal interface IDirect3DTexture9
    {
    }

    private IDirect3DTexture9 comObject;
    private GetSurfaceLevelSignature getSurfaceLevel;

    internal Direct3DTexture9(IDirect3DTexture9 obj)
    {
        this.comObject = obj;
        HelperMethods.GetComMethod(this.comObject, 18, 
                                   out this.getSurfaceLevel);
    }

    ~Direct3DTexture9()
    {
        this.Release();
    }

    public void Dispose()
    {
        this.Release();
        GC.SuppressFinalize(this);
    }

    public IntPtr GetSurfaceLevel(uint Level)
    {
        IntPtr surface;
        Marshal.ThrowExceptionForHR(this.getSurfaceLevel(
                              this.comObject, Level, out surface));
        return surface;
    }

    private void Release()
    {
        if (this.comObject != null)
        {
            Marshal.ReleaseComObject(this.comObject);
            this.comObject = null;
            this.getSurfaceLevel = null;
        }
    }
}

In the code, I've used Marshal.ThrowExceptionForHR to make sure that the call succeeds - if there's an error, then it will throw the relevant .NET type (e.g., a result of E_NOTIMPL will result in a NotImplementedException being thrown).

Using the code

To use the attached code, you can either include the compiled binary into your project, or include the code as there's not a lot of it (despite the time spent on creating it!). Either way, you'll need to make sure you reference the Windows API Code Pack DirectX library in your project.

In the code, there are three classes of interest: D3D10Image, Direct2DControl, and Scene.

The D3D10Image class inherits from D3DImage, and adds an override of the SetBackBuffer method that accepts a Direct3D 10 texture (in the form of a Microsoft.WindowsAPICodePack.DirectX.Direct3D10.Texture2D object). As the code is written, the texture must be in the DXGI_FORMAT_B8G8R8A8_UNORM format; however, feel free to edit the code inside the GetSharedSurface function to whatever format you want (in fact, the original code by Jeremiah Morrill did allow for different formats, so take a look at that for inspiration).

Direct2DControl is a wrapper around the D3D10Image control, and provides an easy way to display a Scene. The control takes care of redrawing the Scene and D3D10Image when it's invalidated, and also resizes their contents. To help improve performance, the control uses a timer to resize the contents 100ms after the resize event has been received. If another request to be resized occurs during this time, the timer is reset to 100ms again. This might sound like it could cause problems when resizing, but internally, the control uses an Image control, which will stretch its contents when it's resized so the contents will always be visible; they just might get temporarily blurry. Once resizing has finished, the control will redraw its contents at the correct resolution. Sometimes, for reasons unknown to me, there will be a flicker when this happens, but by using the timer, this will occur infrequently.

The Scene class is an abstract class containing three main functions for you to override: OnCreateResources, OnFreeResources, and OnRender. The reason for the first two functions is that a DirectX device can get destroyed (for example, if you switch users), and afterwards, you will need to create a new device. These methods allow you to create/free device dependent resources, such as brushes for example. The OnRender method, as the name implies, is where you do the actual drawing.

Putting this together gives us this code to create a simple rectangle on a semi-transparent blue background:

XML
<!-- Inside your main window XAML code -->
<!-- Make sure you put a reference to this at the top of the file:
        xmlns:d2d="clr-namespace:Direct2D;assembly=Direct2D"
 -->

<d2d:Direct2DControl x:Name="d2DControl" />
C#
using D2D = Microsoft.WindowsAPICodePack.DirectX.Direct2D1;

internal sealed class MyScene : Direct2D.Scene
{
    private D2D.SolidColorBrush redBrush;

    protected override void OnCreateResources()
    {
        // We'll fill our rectangle with this brush
        this.redBrush = this.RenderTarget.CreateSolidColorBrush(
                             new D2D.ColorF(1, 0, 0));
    }

    protected override void OnFreeResources()
    {
        if (this.redBrush != null)
        {
            this.redBrush.Dispose();
            this.redBrush = null;
        }
    }

    protected override void OnRender()
    {
        // This is what we're going to draw
        var size = this.RenderTarget.Size;
        var rect = new D2D.Rect
            (
                5,
                5,
                (int)size.Width - 10,
                (int)size.Height - 10
            );

        // This actually draws the rectangle
        this.RenderTarget.BeginDraw();
        this.RenderTarget.Clear(new D2D.ColorF(0, 0, 1, 0.5f));
        this.RenderTarget.FillRectangle(rect, this.redBrush);
        this.RenderTarget.EndDraw();
    }
}

// This is the code behind class for the XAML
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // Add this after the call to InitializeComponent. Really you should
        // store this object as a member so you can dispose of it, but in our
        // example it will get disposed when the window is closed.
        this.d2DControl.Scene = new MyScene();
    }
}

Updating the Scene

In the original code to update the Scene, you needed to call Direct2DControl.InvalidateVisual. This has now been changed so that calling the Render method on Scene will cause the new Updated event to be fired, which the Direct2DControl subscribes to and invalidates its area accordingly.

Also discovered was that the Scene would sometimes flicker when redrawn. This seems to be an issue with the D3DImage control, and the solution (whilst not 100%) is to synchronize the AddDirtyRect call with when WPF is rendering (by subscribing to the CompositionTarget.Rendering event). This is all handled by the Direct2DControl for you.

To make things easier still, there's a new class deriving from Scene called AnimatableScene. After releasing the first version, there was some confusion with how to do continuous scene updates, so hopefully this class should make it easier - you use it the same as the Scene class, but your OnRender code will be called, when required, by setting the desired frames per second in the constructor (though see the Limitations section). Also note that if you override the OnCreateResources method, you need to make sure to call the base's version at the end of your code to start the animation, and when you override the OnFreeResources method, you need to call the base's version first to stop the animation (see the example in the attached code).

Mixed mode assembly is built against version 'v2.0.50727'

The attached code is compiled against .NET 4.0 (though it could probably be retargeted to work under .NET 2.0), but the Code Pack is compiled against .NET 2.0. When I first referenced the Code Pack and tried running the application, the above exception kept getting raised. The solution, found here, is to include an app.config file in the project with the following startup information:

XML
<?xml version="1.0"?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0"/>
  </startup>
</configuration>

Limitations

Direct2D will work over remote desktop; however (as far as I can tell), the D3DImage control is not rendered. Unfortunately, I only have a Home Premium version of Windows 7, so cannot test any workarounds, but would welcome feedback in the comments.

The code written will work with targeting either x86 or x64 platforms (or even using the Any CPU setting); however, you'll need to use the correct version of Microsoft.WindowsAPICodePack.DirectX.dll; I couldn't find a way of making this automatic, and I don't think the Code Pack can be compiled to use Any CPU as it uses unmanaged code.

The timer used in the AnimatableScene is a DispatchTimer. MSDN states:

[The DispatcherTimer is] not guaranteed to execute exactly when the time interval occurs [...]. This is because DispatcherTimer operations are placed on the Dispatcher queue like other operations. When the DispatcherTimer operation executes is dependent on the other jobs in the queue and their priorities.

History

  • 02/11/10 - Direct2DControl has been changed to use a DispatchTimer so that it doesn't contain any controls needing to be disposed of (makes FxCop a little happier), and the control is now synchronized with WPF's CompositionTarget.Rendering event to reduce flickering. Scene has been changed to include an Updated event and to allow access to its D2DFactory to derived classes. Also, the AnimatedScene class has been added.
  • 21/09/10 - Initial version.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHow may I locate your control on item wpf tabcontrol? You control has zero size before I resize window. Pin
FreeCoderMan18-Sep-16 20:17
FreeCoderMan18-Sep-16 20:17 
AnswerMessage Closed Pin
9-Sep-19 16:23
Member 145849899-Sep-19 16:23 
QuestionDifferent PC = different behavior? Pin
René Greiner25-Mar-13 10:15
René Greiner25-Mar-13 10:15 
QuestionHow to capture an Resize() event Pin
Tony Teveris13-Dec-12 9:35
Tony Teveris13-Dec-12 9:35 
QuestionHigh Speed Charts Pin
David Roh18-Sep-12 2:26
David Roh18-Sep-12 2:26 
AnswerRe: High Speed Charts Pin
Samuel Cragg18-Sep-12 13:54
Samuel Cragg18-Sep-12 13:54 
GeneralRe: High Speed Charts Pin
LGTCO8-Nov-22 23:45
LGTCO8-Nov-22 23:45 
Question"Unable to create a Direct2D and/or Direct3D device." [modified] Pin
Yves Goergen26-Jul-11 1:14
Yves Goergen26-Jul-11 1:14 
AnswerRe: "Unable to create a Direct2D and/or Direct3D device." Pin
Samuel Cragg26-Jul-11 8:17
Samuel Cragg26-Jul-11 8:17 
GeneralProblem with small resolution Pin
nout13-Apr-11 9:21
nout13-Apr-11 9:21 
GeneralRe: Problem with small resolution Pin
Samuel Cragg13-Apr-11 9:43
Samuel Cragg13-Apr-11 9:43 
GeneralRe: Problem with small resolution Pin
nout13-Apr-11 11:49
nout13-Apr-11 11:49 
GeneralRe: Problem with small resolution Pin
Samuel Cragg13-Apr-11 13:16
Samuel Cragg13-Apr-11 13:16 
GeneralRe: Problem with small resolution Pin
nout14-Apr-11 6:11
nout14-Apr-11 6:11 
GeneralMy vote of 5 Pin
John Schroedl9-Nov-10 2:52
professionalJohn Schroedl9-Nov-10 2:52 
GeneralProblem with bitmaps Pin
Tomi Valkeinen29-Oct-10 9:36
Tomi Valkeinen29-Oct-10 9:36 
GeneralRe: Problem with bitmaps Pin
Samuel Cragg29-Oct-10 10:21
Samuel Cragg29-Oct-10 10:21 
GeneralRe: Problem with bitmaps Pin
Tomi Valkeinen29-Oct-10 19:52
Tomi Valkeinen29-Oct-10 19:52 
GeneralRe: Problem with bitmaps Pin
Samuel Cragg30-Oct-10 10:24
Samuel Cragg30-Oct-10 10:24 
GeneralRe: Problem with bitmaps Pin
Tomi Valkeinen3-Nov-10 21:36
Tomi Valkeinen3-Nov-10 21:36 
GeneralRe: Problem with bitmaps Pin
Samuel Cragg4-Nov-10 3:28
Samuel Cragg4-Nov-10 3:28 
GeneralRe: Problem with bitmaps Pin
Tomi Valkeinen4-Nov-10 3:42
Tomi Valkeinen4-Nov-10 3:42 
GeneralPerformance and Device Lost handling Pin
Rhox26-Oct-10 1:43
Rhox26-Oct-10 1:43 
GeneralRe: Performance and Device Lost handling Pin
Samuel Cragg26-Oct-10 6:59
Samuel Cragg26-Oct-10 6:59 
QuestionHow to make use of the binaries? Pin
bimbambumbum15-Oct-10 13:19
bimbambumbum15-Oct-10 13:19 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.