エントリー
こんにちは、読者の皆さん! 少し前までは、Windows用のシンプルなグラフィカルエディタを実装するという課題に直面していましたが、将来は2次元グラフィックスと3次元グラフィックスの両方をサポートする必要があります。 タスクは簡単ではありません。特に、描画の結果を表示するウィンドウとともに、高品質のユーザーインターフェイスが必要であると考える場合は特にそうです。 少し考えた後、QtとWPFの2つのツールが強調されました。 Qtテクノロジーは、優れたAPIと優れたOpenGLサポートを誇っています。 しかし、それには我慢するのが難しいいくつかの欠点もあります。 まず、Qt Widgets上の大規模なアプリケーションは、維持にかなりの費用がかかり、Qt Quickにグラフィックを統合するのは困難です。 第二に、OpenGLには2次元描画用の開発されたインターフェイスがありません。 だから私はWPFに決めました。 強力なGUI作成ツール、C#プログラミング言語、このテクノロジーの豊富な経験など、すべてがここで私に合っていました。 さらに、描画にDirect3DとDirect2Dを使用することが決定されました。 残っている問題は1つだけです。C++で実行されたレンダリングの結果をWPFウィンドウに配置する必要がありました。 この記事は、この問題を解決することに専念しています。 だから、ここにリーダーシップ計画があります:
- C#レンダリングレンダリングコンポーネントの開発
- C ++でDirectXを使用してサンプルプロジェクトを作成する
- WPFウィンドウでの描画結果の表示
時間を無駄にすることなく、すぐに仕事に取り掛かります。
1. Cでレンダリングを表示するコンポーネントの開発#
まず、Visual StudioでWPFアプリケーションプロジェクトを作成します。 次に、プロジェクトに新しい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); } } }
このクラスは非常に簡単に機能します。メッセージキューとウィンドウハンドルにアクセスするために、親クラスHwndHostの WndProcメソッドが再定義されます。 BuildWindowCoreメソッドは 、新しいウィンドウのコンストラクターとして使用されます。 親ウィンドウへのハンドルを取得し、新しいウィンドウへのハンドルを返します。 ウィンドウを作成して維持することは、.NETプラットフォームには存在しない管理機能に類似したシステム機能の助けを借りてのみ可能です。 WinAPIツールへのアクセスは、共通言語インフラストラクチャ(CLI)の一部として実装されるPlatform Invocation Services(PInvoke)によって提供されます。 PInvokeの使用に関する情報は、.NET Frameworkの多数の書籍から入手できます。ここでは、すべての関数と構造の正しい宣言を見つけることができるPInvoke.net Webサイトに注目してください。 メッセージキューの操作は、目的のイベントを処理することです。 通常は、ウィンドウのコンテンツの再描画とサイズ変更を処理するだけで十分です。 このコードを実行する最も重要なことは、通常の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. C ++でDirectXを使用してサンプルプロジェクトを作成する
コンポーネントの使用例として、Direct2Dツールを使用して描画ウィンドウに特定の背景を塗りつぶす単純なC ++プロジェクトを作成します。 C ++ / CLIバインディングを使用してマネージコードとアンマネージコードを接続できますが、これは実際のプロジェクトでは必要ありません。 C ++ CLRクラスライブラリプロジェクトをVisual Studioソリューションに追加します。 プロジェクトにはデフォルトのソースファイルが含まれますが、削除できます。 実験のために必要なソースファイルは1つだけです。その内容は次のとおりです。
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テクノロジは、Windows用の群を抜いて最も開発されたGUIツールであり、システム機能を使用できるため、プログラマの仕事が楽になる場合があります。 アプリケーションをすばやくテストするために、Githubにリポジトリを作成しました。 そこで、このソリューションの最新バージョンをいつでも見つけることができます。