[Из песочницы] Рендеринг DirectX в окне WPF

86f58aabb4f94427b56e496eb38c3b57.png

Всупление


Добрый день, уважаемые читатели! Не так давно передо мной встала задача реализовать несложный графический редактор под 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

    
        
    




Для того, чтобы поместить компонент на форму, необходимо указать пространство имён, в котором он находится. Затем его можно использовать как заполнитель, чтобы точно представлять положение каждого элемента на форме. Перед тем как переключиться из режима редактирования в режим конструктора, проект нужно пересобрать. На рисунке ниже показано окно Visual Studio с открытым конструктором главного окна приложения, в котором заполнитель имеет серый фон:

338ad4f0d63b4cc895e5a9adcf91f48d.png

2. Создание примера проекта с использованием DirectX на C++


В качестве примера использования компонента создадим простой проект на C++, в котором средствами Direct2D окно рисования будет залито определённым фоном. Для связи управляемого и неуправляемого кода можно использовать привязку C++/CLI, однако в реальных проектах делать это совсем необязательно. Добавим в решение Visual Studio проект C++ CLR Class Library. В проекте будут присутствовать исходные файлы по умолчанию, их можно удалить. Для эсперимента понадобится только один исходный файл, его содержимое приведено ниже:

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;
        };
}



Класс Scene связывает код приложения на C# и класс Renderer. Последний использует Direct2D API для заливки фона окна оранжевым цветом. Стоит отметить, что на практике рендеринг полностью выполняется в неуправляемом коде, для вывода результата необходим лишь дескриптор окна (HWND). Также необходимо учесть, что оба проекта в решении теперь должны иметь одинаковую конфигурацию при сборке, например, «Release x86».

3. Вывод результата рисования в окне WPF


Для того, чтобы вывести результат рисования на форму, необходимо добавить ссылку на сборку библиотки рисования в проекте приложения WPF и вызвать соответствующие функции из библиотеки при обработке оконных сообщений компонента. На рисунке ниже показано окно добавления ссылки на библиотеку рисования и структура решения:

a4c6f3b26e0a41f9a6b3f791f24f53cf.png

Ниже приведён изменённый код класса 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). На рисунке ниже показано залитое оранжевым цветом окно готового приложения:

ecab8c1f62a34cf2b772fc4e8eff5c35.png

Заключение


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

© Habrahabr.ru