Рендеринг DirectX в окне WPF

от автора

Всупление

Добрый день, уважаемые читатели! Не так давно передо мной встала задача реализовать несложный графический редактор под Windows, при этом в перспективе он должен поддерживать как двухмерную, так и трёхмерную графику. Задача непростая, особенно если учесть, что наряду с окном просмотра результата рисования непременно должен быть качественный интерфейс пользователя. После некоторых раздумий были выделены два инструмента: Qt и WPF. Технология Qt может похвастаться хорошим API и неплохой поддержкой OpenGL. Однако она обладает и рядом недостатков, с которыми сложно мириться. Во-первых, большое приложение на Qt Widgets выйдет довольно дорогим в обслуживании, а в Qt Quick тяжело интегрировать графику. Во-вторых, в OpenGL нет развитого интерфейса для двухмерного рисования. Таким образом, я остановился на WPF. Здесь меня всё устраивало: мощные инструменты создания GUI, язык программирования C# и большой опыт работы с этой технологией. К тому же было принято решение использовать Direct3D и Direct2D для рисования. Осталась всего одна проблема — нужно было разместить результаты рендеринга, выполненного на C++, в окне WPF. Эта статья посвящена решению данной проблемы. Итак, вот план руководства:

  1. Разработка компонента просмотра рендеринга на C#
  2. Создание примера проекта с использованием DirectX на C++
  3. Вывод результата рисования в окне 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. Там всегда можно найти свежую версию данного решения.

ссылка на оригинал статьи http://habrahabr.ru/post/261927/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *