
Вступление
Добрый день, уважаемые читатели! Не так давно передо мной встала задача реализовать несложный графический редактор под 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. Ниже приведён код этого класса:
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); } } }
Данный класс работает очень просто: чтобы получить доступ к очереди сообщений и оконному дескриптору, переопределяется метод WndProc из родительского класса HwndHost. Метод BuildWindowCore используется в качестве конструктора нового окна. Он принимает дескриптор родительского окна, а возвращает дескриптор нового окна. Создание окна и его обслуживание возможно лишь с помощью системных функций, управляемых аналогов которых в платформе .NET не существует. Доступ к средствам WinAPI предоставляют Platform Invocation Services (PInvoke), реализованные в рамках Common Language Infrastructure (CLI). Сведения о работе с PInvoke можно получить из многочисленных книг по .NET Framework, здесь же я хочу обратить ваше внимание на сайт PInvoke.net, на котором можно найти корректные объявления всех функций и структур. Работа с очередью сообщений заключается в обработке нужного события. Обычно достаточно обрабатывать перерисовку содержимого окна и изменение его размеров. Самое главное, что выполняет этот код — создание дескриптора окна, который можно использовать также, как и в обычном приложении WinAPI. Для того, чтобы работа в дизайнере WPF была удобной, нужно поместить компонент окна на главную форму приложения. Ниже приведена разметка XAML главного окна приложения:
MainWindow.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. В проекте будут присутствовать исходные файлы по умолчанию, их можно удалить. Для эксперимента понадобится только один исходный файл, его содержимое приведено ниже:
Renderer.cpp
#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:
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); } } }
При обработке оконного сообщения WM_PAINT происходит перерисовка содержимого компонента. Данное сообщение также поступает в очередь при изменении размеров окна (сообщение WM_SIZE). На рисунке ниже показано залитое оранжевым цветом окно готового приложения:

Заключение
Изложенный в статье способ рисования в окне WPF хорошо подходит для создания приложений, в которых интерфейс пользователя должен быть совмещён с окном просмотра. Технология WPF на сегодняшний день является самым развитым инструментом создания GUI для Windows, а возможность использования системных функций порой делает работу программиста проще. Чтобы поскорее испытать работу приложения, мной был создан репозиторий на Github. Там всегда можно найти свежую версию данного решения.
