WPF 托管 SharpDX





5.00/5 (10投票s)
基于 Microsoft 互操作解决方案的 WPF 应用程序示例,使用 DirectX11 托管代码(SharpDX 和 C#)
引言
不久前,我在 Google 上搜索如何在 WPF 应用程序中托管 SharpDX
Direct11 开发。我找到了许多关于这个话题的意见,从“不可能”到“可能”,再到“也许可能”,甚至还有“我的姐夫做到了,所以是可能的”。但是,到目前为止,我还没有找到任何可以工作的代码示例来展示如何做到这一点。我的第一个想法是,有人必须这样做。我的第二个想法是,也许这个人就是我。所以,我们开始吧。
我知道 SharpDX
不再维护了,但它仍然运行得很好,而且我们中的许多人都写了大量使用它的代码。所以我认为这项小工作值得分享。
Microsoft 目前正在推广 **WPF DirectX Extensions** 包,该包允许轻松地在 WPF 应用程序中托管 DirectX 10 和 DirectX 11 内容。
您可以在本文随附的 Visual Studio 解决方案中找到一个小型库,该库允许使用 Microsoft **WPF DirectX Extensions** 将 SharpDX
内容托管到 WPF 应用程序中。
该解决方案还包含一个将 SharpDX-Rastertek-Tutorials
示例从 WinForms 移植到 WPF 的示例。这是为了说明该库如何用于转换现有的 WinForms 项目。
背景
Microsoft **WPF DirectX Extensions** 可以从 Github 下载,网址为 https://github.com/microsoft/WPFDXInterop。它包含一个名为 *Microsoft.Wpf.Interop.DirectX.dll* 的非托管 DLL,您需要在 WPF 应用中引用它。请注意,它有两种版本:x86 和 x64。x64 DLL 已包含在本文附件的项目中,因此您无需安装即可运行示例。
在 MS **WPF DirectX Extensions** 中包含的示例中,D3D11 代码被打包在一个 C++ DLL 中。这个 DLL 示例名为 D3D11Visualization
。本文的开头就是使用 C# 和 SharpDX
重写这个示例。这将是我们的第一个示例。
Microsoft WPF DirectX Extensions 的通用原理是它允许我们将 D3DImage
嵌入到您的 WPF 窗口中。基本上,D3DImage
是一个 Direct3D9
图像。这个图像使用 Texture2D
内存结构来存储其数据。碰巧的是,这种内存结构与 D3D11 的 Texture2D
相同,我们可以在其中渲染 SharpDX
场景。因此,如果我们能够创建一个使用与我们的 D3DImage
相同的资源(内存切片)的 D3D11 Texture2D
,我们就完成了。
我们将在此使用的另一个资源是 SharpDX-Rastertek-Tutorials-master
。对于不知道它的人来说,它是一个由关于在 WinForm 下使用 SharpDX
构建图形应用程序的教程组成的包。您可以从 Github 此处 下载 SharpDX-Rastertek-Tutorials-master
。我们将使用 RasterTek 作为将 WinForm SharpDX
应用程序移植到 WPF 的源。
框架结构
该框架基于三个层
- WPF 应用。它是原生的 WPF 代码,声明了一个
D3DImage
和一些代码使其呈现为 DirectX 场景。 DSharpDxInterop
:这是一个允许 WPF 应用和SharpDX
图形内容之间交换的桥梁。它包含在WpfSharpDXLib
中。这个库的 C# 代码包含在解决方案存储库中,因此您可以根据需要进行修改。- 图形类,这是您的
SharpDX
内容所在的位置。它必须实现IGraphics
接口,该接口也是WpfSharpDXLib
的成员。
垂直方向上,图示显示了两个不同的阶段:Initialization
(初始化)和 Render
(渲染)。
- 初始化阶段:在此阶段,我们将在 WPF 应用中的
D3DImage
上设置一个渲染循环。 - 渲染阶段:在此阶段,我们的
bridge
类会将D3DImage
转换为 D3D11Texture2D
。此Texture2D
将用于创建RenderTargetView
。这样,我们绘制到RenderTargetView
中的场景将显示在 WPFD3DImage
中。
接下来,我们将详细研究每个层的实现。
代码 walkthrough:WpfSharpDXInterop_Master
我们将分析第一个项目 WpfSharpDXInterop_Master
。它在 WPF 应用程序中显示一个简单的旋转彩色立方体。
WPF 应用程序:MainWindow.xaml
这部分是一个基本的 WPF 应用。我们必须添加一个命名空间和对 dxExtensions
的引用,它对应于 *Microsoft.Wpf.Interop.DirectX.dll*。
xmlns:dxExtensions="clr-namespace:Microsoft.Wpf.Interop.DirectX;
assembly=Microsoft.Wpf.Interop.DirectX"
在我们的设计中,我们必须提供一个面板来托管我们的场景。在这里,我们使用一个名为 Host
的 Grid
面板,其中包含一个图像。此图像 InteropImage
的源将通过 MS dxExtensions
提供。
<Grid Name="Host" Background="DimGray">
<Image Stretch="Fill" Name="ImageHost" Margin="0">
<Image.Source>
<dxExtensions:D3D11Image x:Name="InteropImage" />
</Image.Source>
</Image>
</Grid>
WPF 应用程序:MainWindow.xaml.cs
这里的有趣之处在于如何为 InteropImage
设置渲染循环。CompositionTarget
对象提供了基于每帧回调创建自定义动画的能力。我们将使用它来渲染 DirectX
场景。
CompositionTarget.Rendering += CompositionTarget_Rendering;
...
void CompositionTarget_Rendering(object? sender, EventArgs e)
{
RenderingEventArgs args = (RenderingEventArgs)e;
// It's possible for Rendering to call back twice in the same frame
// so only render when we haven't already rendered in this frame.
if (this._lastRender != args.RenderingTime)
{
InteropImage.RequestRender();
this._lastRender = args.RenderingTime;
}
}
现在,我们将一个操作(DoRender
)关联到 Render
循环。
private void InitializeRendering()
{
...
InteropImage.OnRender = DoRender;
...
}
private void DoRender(IntPtr surface, bool isNewSurface)
{
if (Render(surface, isNewSurface)) return;
...
}
我们不必担心同步、前后缓冲区等问题,因为 CompositionTarget
的机制会为我们处理所有这些。我们唯一需要考虑的是在前端缓冲区不可用时停止渲染。例如,当计算机处于睡眠状态或显示登录屏幕时,这可能会发生。
public MainWindow(bool lastVisible)
{
InitializeComponent();
...
InteropImage.IsFrontBufferAvailableChanged += FrontBufferAvailableChanged;
...
}
private void FrontBufferAvailableChanged
(object sender, DependencyPropertyChangedEventArgs e)
{
bool isFrontBufferAvailable = (bool)e.NewValue;
if (!isFrontBufferAvailable)
{
InteropImage.OnRender = null;
}
else
{
_doRefresh = true; // force the creation of a renderTargetView
// with the new buffer.
InteropImage.OnRender = this.DoRender;
}
}
注意 _graphics
对象(SharpDX
开发),它从 IGraphics
实例化并通过 WpfSharpDXInterop
初始化。
public MainWindow()
{
InitializeComponent();
// Instantiate the graphics class.
...
_graphics = new DGraphics();
InitSharpDxInterop();
}
WpfSharpDXInterop 类
这个托管类是 WPF 和 SharpDX
图形应用程序之间的桥梁。它是解决方案存储库中提供的 WpfSharpDXLib
的成员。
初始化
初始化方法由 WPF 应用调用。它接收对图形实例的引用。该方法创建 D3D11 设备并将它们传递给图形实例。
Render
关键部分是 Render
方法。这里首先要做的是检查 RenderTargetView
是否需要创建。这发生在第一次启动程序时。也可能发生于某些事件。
- 窗口大小调整时
- 如果调用登录屏幕(Ctrl+Alt+Del)
- 如果计算机处于睡眠状态然后重启...
创建新的 RenderTargetView
从将 WPF D3DImage
转换为 D3D11 Texture2D
开始。
- 首先,我们获取
DirectX9 Texture2D
的句柄。 - 然后,我们使用这个句柄创建一个链接到相同资源的 D3D11
Texture2D
。
// Get a handle to the DirectX9 texture2D
using Resource d3dResource = (Resource)resourcePointer;
IntPtr renderTextureHandle = d3dResource.QueryInterface<Resource>().SharedHandle;
// use this handle to create a D3D11 Texture2D linked to the same resource.
using SharpDX.Direct3D11.Resource d3d11Resource1 =
_device.OpenSharedResource<SharpDX.Direct3D11.Resource>(renderTextureHandle);
var texture2D = (Texture2D)d3d11Resource1.NativePointer;
现在,我们处理 D3D11 Texture2D
,SharpDX
进程可以在其中渲染场景。考虑到相同的资源也与 WPF 图像源相同,我们就建立了 WPF 和 SharpDX
之间的桥梁。我们可以构建基于 RenderTargetView
的。
// Now, we can create the RenderTargetView
RenderTargetViewDescription targetDescription = new()
{
Format = SharpDX.DXGI.Format.B8G8R8A8_UNorm,
Dimension = RenderTargetViewDimension.Texture2D
};
targetDescription.Texture2D.MipSlice = 0;
RenderTargetView = new RenderTargetView(_device, texture2D, targetDescription);
// Set render target view to interOpImage
_deviceContext.OutputMerger.SetRenderTargets(renderTargetView: RenderTargetView);
实用程序
该类还提供了一些实用类。
UpdateViewPort
方法允许更改视角和裁剪距离。对于缩放效果很有用。GetPixelSize
方法计算图像以填充主机所需的像素大小。WPF 应用需要。
SharpDX 图形应用程序
这是由 WPF 应用实例化并由 WpfSharpDXInterop.Initialize(...)
初始化 的类。这是我们放置 D3D11 内容(如模型、纹理、着色器、动画...)的地方。一个限制是此类必须实现 IGraphics
接口,该接口也包含在 WpfSharpDXLib
中。另一个限制是该类必须在其最终渲染到 WpfSharpDXInterop
提供的 renderTargetView
中(参见下面的 Render
方法)。
public interface IGraphics
{
public bool Initialize(Device device, DeviceContext deviceContext);
public bool Render(RenderTargetView renderTargetView,
Matrix projectionMatrix, int surfaceWidth, int surfaceHeight);
}
将 WinForm SharpDx 移植到 WPF:WpfSharpDXInterop_Tutorial7
解决方案的第二个项目 WpfSharpDXInterop_Tutorial7
是 SharpDX Rastertek Tutorials #7 的 WPF 版本。实际上,它与前面的示例非常相似,但立方体被纹理化并打上了光。
如果您想自己尝试移植,首先必须从 Github 这里 下载 SharpDX-Rastertek-Tutorials-master
。
以下是移植的详细步骤。
- 将项目
WpfSharpDXInterop_Master
复制到一个新的 WPF 解决方案中。 - 删除类
DGraphics
- 我们将用RasterTek
的代码替换它。 - 从原始
SharpDX_RasterTek
项目中,将 *Graphics* 文件夹中的文件复制到您的 WPF 项目的 *Graphics* 文件夹中。 - 删除
DDX11Class
。它被我们的DSharpDxInterop
类替换。
打开类 DGraphics
- 让它实现
IGraphics
接口。您需要实现Device
和DeviceContext
对象,并从Initialize
方法中初始化它们。 - D3D 不再存在了 => 删除所有与此类的创建和初始化相关的部分。
- 删除 Timer 和其他您不需要的东西。
DSystemConfiguration
不再存在了 => 创建一个新类来托管必要的常量(访问路径...)。windowsHandle
特定于WindForm
环境:从您项目中的所有类中删除它。-
替换
D3D.BeginScene(0f, 0f, 0f, 1f);
用
//Clear the render buffer RawColor4 clearColor = new(0.00f, 0.00f, 0.0f, 0.0f); _deviceContext?.ClearRenderTargetView(renderTargetView, clearColor)
-
替换
D3D.EndScene();
用
_deviceContext.Flush();
最后,生成并...修复编译错误。不要惊慌,自顶向下进行。
执行前,请确保所有外部文件(着色器、模型、纹理...)都已根据访问路径复制到正确的位置。
结论与意义
我认为我们可以从本文中学到三件事:
- 第一,如何在仅四行 C# 代码中将
D3DImage
转换为 D3D11Texture2D
。 - 第二,如何使用 MS WPF
DirectX Extensions
和我们的小型库WpfSharpDXLib
将SharpDX
托管代码集成到 WPF 应用程序中。 - 最后一个是它有多么容易。
关于将 SharpDX
代码从 WinForm 迁移到 WPF。重要的词是“计划”。在合并大量代码行之前,仔细研究不同部分如何互连。制定行动计划。
下一步我将把这个解决方案集成到我拥有复杂图形结构的游戏项目中。
历史
- 2023 年 7 月 27 日:初始版本