Вступление
Добрый день, уважаемые читатели! Не так давно передо мной встала задача реализовать несложный графический редактор под Windows, при этом в перспективе он должен поддерживать как двухмерную, так и трёхмерную графику. Задача непростая, особенно если учесть, что наряду с окном просмотра результата рисования непременно должен быть качественный интерфейс пользователя. После некоторых раздумий были выделены два инструмента: Qt и WPF. Технология Qt может похвастаться хорошим API и неплохой поддержкой OpenGL. Однако она обладает и рядом недостатков, с которыми сложно мириться. Во-первых, большое приложение на Qt Widgets выйдет довольно дорогим в обслуживании, а в Qt Quick тяжело интегрировать графику. Во-вторых, в OpenGL нет развитого интерфейса для двухмерного рисования. Таким образом, я остановился на WPF. Здесь меня всё устраивало: мощные инструменты создания GUI, язык программирования C# и большой опыт работы с этой технологией. К тому же было принято решение использовать Direct3D и Direct2D для рисования. Осталась всего одна проблема — нужно было разместить результаты рендеринга, выполненного на C++, в окне WPF. Эта статья посвящена решению данной проблемы. Итак, вот план руководства:
- Разработка компонента просмотра рендеринга на C#
- Создание примера проекта с использованием DirectX на C++
- Вывод результата рисования в окне WPF
Не будем терять времени и немедленно приступим к работе.
1. Разработка компонента просмотра рендеринга на C#
Для начала создадим проект приложения WPF в Visual Studio. Затем добавим в проект новый класс C#. Пусть его имя будет NativeWindow. Ниже приведён код этого класса:
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);
}
}
}
Данный класс работает очень просто: чтобы получить доступ к очереди сообщений и оконному дескриптору, переопределяется метод WndProc из родительского класса HwndHost. Метод BuildWindowCore используется в качестве конструктора нового окна. Он принимает дескриптор родительского окна, а возвращает дескриптор нового окна. Создание окна и его обслуживание возможно лишь с помощью системных функций, управляемых аналогов которых в платформе .NET не существует. Доступ к средствам WinAPI предоставляют Platform Invocation Services (PInvoke), реализованные в рамках Common Language Infrastructure (CLI). Сведения о работе с PInvoke можно получить из многочисленных книг по .NET Framework, здесь же я хочу обратить ваше внимание на сайт PInvoke.net, на котором можно найти корректные объявления всех функций и структур. Работа с очередью сообщений заключается в обработке нужного события. Обычно достаточно обрабатывать перерисовку содержимого окна и изменение его размеров. Самое главное, что выполняет этот код — создание дескриптора окна, который можно использовать также, как и в обычном приложении WinAPI. Для того, чтобы работа в дизайнере WPF была удобной, нужно поместить компонент окна на главную форму приложения. Ниже приведена разметка XAML главного окна приложения:
<Window x:Class="app.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="clr-namespace:app"
Title="MainWindow" Height="350" Width="525">
<Grid>
<i:NativeWindow></i:NativeWindow>
</Grid>
</Window>
Для того, чтобы поместить компонент на форму, необходимо указать пространство имён, в котором он находится. Затем его можно использовать как заполнитель, чтобы точно представлять положение каждого элемента на форме. Перед тем как переключиться из режима редактирования в режим конструктора, проект нужно перестроить. На рисунке ниже показано окно Visual Studio с открытым конструктором главного окна приложения, в котором заполнитель имеет серый фон:
2. Создание примера проекта с использованием DirectX на C++
В качестве примера использования компонента создадим простой проект на C++, в котором средствами Direct2D окно рисования будет залито определённым фоном. Для связи управляемого и неуправляемого кода можно использовать привязку C++/CLI, однако в реальных проектах делать это совсем необязательно. Добавим в решение Visual Studio проект C++ CLR Class Library. В проекте будут присутствовать исходные файлы по умолчанию, их можно удалить. Для эксперимента понадобится только один исходный файл, его содержимое приведено ниже:
#include <d2d1.h>
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;
};
}
Класс Scene связывает код приложения на C# и класс Renderer. Последний использует Direct2D API для заливки фона окна оранжевым цветом. Стоит отметить, что на практике рендеринг полностью выполняется в неуправляемом коде, для вывода результата необходим лишь дескриптор окна (HWND). Также необходимо учесть, что оба проекта в решении теперь должны иметь одинаковую конфигурацию при сборке, например, «Release x86».
3. Вывод результата рисования в окне WPF
Для того, чтобы вывести результат рисования на форму, необходимо добавить ссылку на сборку библиотеки рисования в проекте приложения WPF и вызвать соответствующие функции из библиотеки при обработке оконных сообщений компонента. На рисунке ниже показано окно добавления ссылки на библиотеку рисования и структура решения:
Ниже приведён изменённый код класса NativeWindow:
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);
}
}
}
При обработке оконного сообщения WM_PAINT происходит перерисовка содержимого компонента. Данное сообщение также поступает в очередь при изменении размеров окна (сообщение WM_SIZE). На рисунке ниже показано залитое оранжевым цветом окно готового приложения:
Заключение
Изложенный в статье способ рисования в окне WPF хорошо подходит для создания приложений, в которых интерфейс пользователя должен быть совмещён с окном просмотра. Технология WPF на сегодняшний день является самым развитым инструментом создания GUI для Windows, а возможность использования системных функций порой делает работу программиста проще. Чтобы поскорее испытать работу приложения, мной был создан репозиторий на Github. Там всегда можно найти свежую версию данного решения.
Комментарии (10)
northicewind
06.07.2015 22:45Всю статью не читал, но из того что видно пахнет велосипедом. Если взять SharpDX, для которого и WPF Control есть, то вся эта жесть не нужна. К тому же производительности у SharpDX хватает для большинства задач.
QtRoS
07.07.2015 08:41+2Как по мне, так названные минусы Qt'a просто от поверхностного знания библиотеки.
andlom Автор
07.07.2015 16:10Верно, больших серьёзных программ на Qt Quick я не писал. Работа над одним сложным проектом с использованием Qt Widgets прошла достаточно гладко. Однако, приложение должно быть заточено под Windows, и я выбрал C# с несложной привязкой в движку рисования.
xtraroman
07.07.2015 15:39Если будете выводить текст через DirectDraw и через WPF то пользователи заметят разницу и части программы программы не будут смотреться единым целым. Еще прозрачность скорее всего не будет работать.
kekekeks
07.07.2015 21:14DirectDraw закопали вместе с DirectX 7 много лет назад. Сейчас Direct2D для этого используется, он всё умеет как надо.
xtraroman
08.07.2015 00:13Неточно написал. На картинке нижний текст выведен вот этой функцией ID2D1RenderTarget::DrawText. Мне не удалось подобрать параметры чтобы текст выводился одинаково.
kekekeks
Извращение-то какое. Что мешало взять WindowsFormsHost, положить в него System.Windows.Forms.Control, извлечь hWnd и уже на нём рисовать? Без всей этой кучи мусора с P/Invoke. Не говоря уже о излишестве в виде плюсового проекта при наличии вполне живого SlimDX.
Zanael
Все верно. SharpDX и SlimDX.
AxisPod
Живой SlimDX, ничего, что последний релиз аж 2012 года? SharpDX жив конечно, но вроде как развивается силами 2х разработчиков и после того как они еще и выкинули toolkit из поставки, стало грустно.
andlom Автор
Добрый день. Я полностью согласен с вашими замечаниями. Однако, не так давно я заинтересовался поддержкой DirectX 12 и поэтому такие инструменты, как SlimDX и SharpDX меня не устроили. Об использовании WindowsFormsHost я не знал, да и статья рассчитана скорее на расширение кругозора.