
DirectX rendering in a WPF window

Introduction
Good afternoon, dear readers! Not so long ago, I faced the task of implementing a simple graphical editor for Windows, while in the future it should support both two-dimensional and three-dimensional graphics. The task is not easy, especially when you consider that, along with a window for viewing the result of drawing, there must certainly be a high-quality user interface. After some thought, two tools were highlighted: Qt and WPF. Qt technology boasts good APIs and good OpenGL support. However, it also has a number of disadvantages that are difficult to put up with. Firstly, a large application on Qt Widgets will be quite expensive to maintain, and it’s hard to integrate graphics in Qt Quick. Secondly, in OpenGL there is no developed interface for two-dimensional drawing. So I settled on WPF. Everything suited me here: powerful GUI creation tools, C # programming language and extensive experience with this technology. In addition, it was decided to use Direct3D and Direct2D for drawing. There was only one problem left - it was necessary to place the results of the rendering, performed in C ++, in the WPF window. This article is dedicated to solving this problem. So, here is a leadership plan:
- C # rendering rendering component development
- Creating a sample project using DirectX in C ++
- Displaying the result of drawing in a WPF window
We will not lose time and immediately get to work.
1. Development of a component of viewing rendering in C #
First, create a WPF application project in Visual Studio. Then add a new C # class to the project. Let his name be NativeWindow . The following is the code for this class:
NativeWindow.cs
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace app
{
public class NativeWindow : HwndHost
{
public new IntPtr Handle { get; private set; }
Procedure procedure;
const int WM_PAINT = 0x000F;
const int WM_SIZE = 0x0005;
[StructLayout(LayoutKind.Sequential)]
struct WindowClass
{
public uint Style;
public IntPtr Callback;
public int ClassExtra;
public int WindowExtra;
public IntPtr Instance;
public IntPtr Icon;
public IntPtr Cursor;
public IntPtr Background;
[MarshalAs(UnmanagedType.LPWStr)]
public string Menu;
[MarshalAs(UnmanagedType.LPWStr)]
public string Class;
}
[StructLayout(LayoutKind.Sequential)]
struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
struct Paint
{
public IntPtr Context;
public bool Erase;
public Rect Area;
public bool Restore;
public bool Update;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] Reserved;
}
delegate IntPtr Procedure
(IntPtr handle,
uint message,
IntPtr wparam,
IntPtr lparam);
[DllImport("user32.dll")]
static extern IntPtr CreateWindowEx
(uint extended,
[MarshalAs(UnmanagedType.LPWStr)]
string name,
[MarshalAs(UnmanagedType.LPWStr)]
string caption,
uint style,
int x,
int y,
int width,
int height,
IntPtr parent,
IntPtr menu,
IntPtr instance,
IntPtr param);
[DllImport("user32.dll")]
static extern IntPtr LoadCursor
(IntPtr instance,
int name);
[DllImport("user32.dll")]
static extern IntPtr DefWindowProc
(IntPtr handle,
uint message,
IntPtr wparam,
IntPtr lparam);
[DllImport("user32.dll")]
static extern ushort RegisterClass
([In]
ref WindowClass register);
[DllImport("user32.dll")]
static extern bool DestroyWindow
(IntPtr handle);
[DllImport("user32.dll")]
static extern IntPtr BeginPaint
(IntPtr handle,
out Paint paint);
[DllImport("user32.dll")]
static extern bool EndPaint
(IntPtr handle,
[In] ref Paint paint);
protected override HandleRef BuildWindowCore(HandleRef parent)
{
var callback = Marshal.GetFunctionPointerForDelegate(procedure = WndProc);
var width = Convert.ToInt32(ActualWidth);
var height = Convert.ToInt32(ActualHeight);
var cursor = LoadCursor(IntPtr.Zero, 32512);
var menu = string.Empty;
var background = new IntPtr(1);
var zero = IntPtr.Zero;
var caption = string.Empty;
var style = 3u;
var extra = 0;
var extended = 0u;
var window = 0x50000000u;
var point = 0;
var name = "Win32";
var wnd = new WindowClass
{
Style = style,
Callback = callback,
ClassExtra = extra,
WindowExtra = extra,
Instance = zero,
Icon = zero,
Cursor = cursor,
Background = background,
Menu = menu,
Class = name
};
RegisterClass(ref wnd);
Handle = CreateWindowEx(extended, name, caption,
window, point, point, width, height,
parent.Handle, zero, zero, zero);
return new HandleRef(this, Handle);
}
protected override void DestroyWindowCore(HandleRef handle)
{
DestroyWindow(handle.Handle);
}
protected override IntPtr WndProc(IntPtr handle, int message, IntPtr wparam, IntPtr lparam, ref bool handled)
{
try
{
if (message == WM_PAINT)
{
Paint paint;
BeginPaint(handle, out paint);
EndPaint(handle, ref paint);
handled = true;
}
if (message == WM_SIZE)
{
handled = true;
}
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
return base.WndProc(handle, message, wparam, lparam, ref handled);
}
static IntPtr WndProc(IntPtr handle, uint message, IntPtr wparam, IntPtr lparam)
{
return DefWindowProc(handle, message, wparam, lparam);
}
}
}
This class works very simply: to access the message queue and window handle, the WndProc method from the parent class HwndHost is redefined . The BuildWindowCore method is used as the constructor for a new window. It takes a handle to the parent window, and returns a handle to the new window. Creating a window and maintaining it is possible only with the help of system functions, controlled analogs of which do not exist in the .NET platform. Access to WinAPI tools is provided by Platform Invocation Services (PInvoke) , implemented as part of the Common Language Infrastructure (CLI). Information about working with PInvoke can be obtained from numerous books on the .NET Framework, here I want to draw your attention to the sitePInvoke.net , where you can find the correct declarations of all functions and structures. Work with the message queue is to process the desired event. It is usually enough to handle redrawing the contents of the window and resizing it. The most important thing that runs this code is creating a window handle that can be used just like in a regular WinAPI application. In order to make the work in the WPF designer convenient, you need to place the window component on the main form of the application. The following is the XAML markup for the main application window:
MainWindow.xaml
In order to place a component on a form, you must specify the namespace in which it is located. Then it can be used as a placeholder to accurately represent the position of each element on the form. Before switching from edit mode to design mode, the project must be rebuilt. The figure below shows the Visual Studio window with the open designer of the main application window, in which the placeholder has a gray background:

2. Creating a sample project using DirectX in C ++
As an example of using the component, we will create a simple C ++ project in which the drawing window will be filled with a certain background using Direct2D tools. You can use the C ++ / CLI binding to connect managed and unmanaged code, but this is not necessary in real projects. Add the C ++ CLR Class Library project to the Visual Studio solution. The project will contain the default source files, you can delete them. For the experiment you will need only one source file, its contents are given below:
Renderer.cpp
#include
namespace lib
{
class Renderer
{
public:
~Renderer()
{
if (factory) factory->Release();
if (target) target->Release();
}
bool Initialize(HWND handle)
{
RECT rect;
if (!GetClientRect(handle, &rect)) return false;
if (FAILED(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &factory)))
return false;
return SUCCEEDED(factory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(handle, D2D1::SizeU(rect.right - rect.left,
rect.bottom - rect.top)), &target));
}
void Render()
{
if (!target) return;
target->BeginDraw();
target->Clear(D2D1::ColorF(D2D1::ColorF::Orange));
target->EndDraw();
}
void Resize(HWND handle)
{
if (!target) return;
RECT rect;
if (!GetClientRect(handle, &rect)) return;
D2D1_SIZE_U size = D2D1::SizeU(rect.right - rect.left, rect.bottom - rect.top);
target->Resize(size);
}
private:
ID2D1Factory* factory;
ID2D1HwndRenderTarget* target;
};
public ref class Scene
{
public:
Scene(System::IntPtr handle)
{
renderer = new Renderer;
if (renderer) renderer->Initialize((HWND)handle.ToPointer());
}
~Scene()
{
delete renderer;
}
void Resize(System::IntPtr handle)
{
HWND hwnd = (HWND)handle.ToPointer();
if (renderer) renderer->Resize(hwnd);
}
void Draw()
{
if (renderer) renderer->Render();
}
private:
Renderer* renderer;
};
}
The Scene class binds C # application code and the Renderer class . The latter uses the Direct2D API to fill the window background with orange. It is worth noting that in practice, rendering is completely performed in unmanaged code, only a window handle (HWND) is required to display the result. It is also necessary to take into account that both projects in the solution should now have the same configuration during assembly, for example, “Release x86”.
3. Displaying the result of drawing in the WPF window
In order to display the result of drawing on a form, you must add a link to the assembly of the drawing library in the WPF application project and call the corresponding functions from the library when processing component window messages. The figure below shows the window for adding a link to the drawing library and the solution structure:

Below is the modified code of the NativeWindow class :
NativeWindow.cs
using lib; // Ссылка на пространство имён классов рисования
using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace app
{
public class NativeWindow : HwndHost
{
public new IntPtr Handle { get; private set; }
Procedure procedure;
Scene scene; // Объект класса Scene для рисования
const int WM_PAINT = 0x000F;
const int WM_SIZE = 0x0005;
[StructLayout(LayoutKind.Sequential)]
struct WindowClass
{
public uint Style;
public IntPtr Callback;
public int ClassExtra;
public int WindowExtra;
public IntPtr Instance;
public IntPtr Icon;
public IntPtr Cursor;
public IntPtr Background;
[MarshalAs(UnmanagedType.LPWStr)]
public string Menu;
[MarshalAs(UnmanagedType.LPWStr)]
public string Class;
}
[StructLayout(LayoutKind.Sequential)]
struct Rect
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
[StructLayout(LayoutKind.Sequential)]
struct Paint
{
public IntPtr Context;
public bool Erase;
public Rect Area;
public bool Restore;
public bool Update;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)]
public byte[] Reserved;
}
delegate IntPtr Procedure
(IntPtr handle,
uint message,
IntPtr wparam,
IntPtr lparam);
[DllImport("user32.dll")]
static extern IntPtr CreateWindowEx
(uint extended,
[MarshalAs(UnmanagedType.LPWStr)]
string name,
[MarshalAs(UnmanagedType.LPWStr)]
string caption,
uint style,
int x,
int y,
int width,
int height,
IntPtr parent,
IntPtr menu,
IntPtr instance,
IntPtr param);
[DllImport("user32.dll")]
static extern IntPtr LoadCursor
(IntPtr instance,
int name);
[DllImport("user32.dll")]
static extern IntPtr DefWindowProc
(IntPtr handle,
uint message,
IntPtr wparam,
IntPtr lparam);
[DllImport("user32.dll")]
static extern ushort RegisterClass
([In]
ref WindowClass register);
[DllImport("user32.dll")]
static extern bool DestroyWindow
(IntPtr handle);
[DllImport("user32.dll")]
static extern IntPtr BeginPaint
(IntPtr handle,
out Paint paint);
[DllImport("user32.dll")]
static extern bool EndPaint
(IntPtr handle,
[In] ref Paint paint);
protected override HandleRef BuildWindowCore(HandleRef parent)
{
var callback = Marshal.GetFunctionPointerForDelegate(procedure = WndProc);
var width = Convert.ToInt32(ActualWidth);
var height = Convert.ToInt32(ActualHeight);
var cursor = LoadCursor(IntPtr.Zero, 32512);
var menu = string.Empty;
var background = new IntPtr(1);
var zero = IntPtr.Zero;
var caption = string.Empty;
var style = 3u;
var extra = 0;
var extended = 0u;
var window = 0x50000000u;
var point = 0;
var name = "Win32";
var wnd = new WindowClass
{
Style = style,
Callback = callback,
ClassExtra = extra,
WindowExtra = extra,
Instance = zero,
Icon = zero,
Cursor = cursor,
Background = background,
Menu = menu,
Class = name
};
RegisterClass(ref wnd);
Handle = CreateWindowEx(extended, name, caption,
window, point, point, width, height,
parent.Handle, zero, zero, zero);
scene = new Scene(Handle); // Создание нового объекта Scene
return new HandleRef(this, Handle);
}
protected override void DestroyWindowCore(HandleRef handle)
{
DestroyWindow(handle.Handle);
}
protected override IntPtr WndProc(IntPtr handle, int message, IntPtr wparam, IntPtr lparam, ref bool handled)
{
try
{
if (message == WM_PAINT)
{
Paint paint;
BeginPaint(handle, out paint);
scene.Draw(); // Перерисовка содержимого
EndPaint(handle, ref paint);
handled = true;
}
if (message == WM_SIZE)
{
scene.Resize(handle); // Обработка изменения размеров
handled = true;
}
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}
return base.WndProc(handle, message, wparam, lparam, ref handled);
}
static IntPtr WndProc(IntPtr handle, uint message, IntPtr wparam, IntPtr lparam)
{
return DefWindowProc(handle, message, wparam, lparam);
}
}
}
When processing the WM_PAINT window message, the content of the component is redrawn. This message also enters the queue when the window is resized (WM_SIZE message). The figure below shows the orange-filled window of the finished application:

Conclusion
The method of drawing in the WPF window described in the article is well suited for creating applications in which the user interface should be combined with the viewport. WPF technology is by far the most developed GUI tool for Windows, and the ability to use system functions sometimes makes the programmer’s job easier. In order to quickly test the application, I created a repository on Github. There you can always find the latest version of this solution.