在 C# 的 WinForm 应用中,界面的绘制使用的是 GDI+。不过在一些特别的应用中,可能需要用硬件加速来提高绘制的效率。下面就来介绍两种在 WinForm 应用中嵌入 Direct2D 的方法。
这里所谓的“嵌入”,指的是只有窗口的某一部分应用 Direct2D 绘制(用一些控件承载),而不是整个窗口都使用 Direct2D 绘制。这是一种混合方案,需要用硬件加速的部分由自己来绘制,其它部分仍然可以使用现有的 WinForm 技术。
至于 Direct2D 的类库,我仍然使用 SharpDX 类库,使用 SharpDX.Windows.RenderControl 控件承载 Direct2D 渲染。
一、使用 HwndRenderTarget
HwndRenderTarget(ID2D1HwndRenderTarget interface),在 SharpDX 中对应的类是 WindowRenderTarget,是将窗口句柄(hwnd)作为渲染目标的类,利用它可以非常容易的在窗口中嵌入 Direct2D 渲染。
它的用法非常简单,只要先创建一个 Direct2D 工厂(SharpDX.Direct2D1.Factory),接下来直接创建 WindowRenderTarget 实例,然后就可以使用了。其核心代码如下所示:
// 创建 Direct2D 单线程工厂。 Factory factory = new Factory(FactoryType.SingleThreaded); // 渲染参数。 RenderTargetProperties renderProps = new RenderTargetProperties { PixelFormat = D2PixelFormat, Usage = RenderTargetUsage.None, Type = RenderTargetType.Default }; // 渲染目标属性。 HwndRenderTargetProperties hwndProps = new HwndRenderTargetProperties() { // 承载控件的句柄。 Hwnd = hwndRenderControl.Handle, // 控件的尺寸。 PixelSize = new Size2(hwndRenderControl.ClientSize.Width, hwndRenderControl.ClientSize.Height), PresentOptions = PresentOptions.None }; // 渲染目标。 hwndRenderTarget = new WindowRenderTarget(factory, renderProps, hwndProps) { AntialiasMode = AntialiasMode.PerPrimitive };
一般的用法,就是在控件的 Paint 事件中,调用 hwndRenderTarget 的相关方法进行绘制。需要特别注意的是,如果控件的大小发生了改变,必须调用 WindowRenderTarget.Resize 方法,重新调整渲染目标的尺寸才可以,否则会导致绘制结果被拉伸,引起失真。
这个方法的优点就是非常简单易用,而且基本上只要操作系统是 Windows 7 及更高版本,都可以正常绘制(在第 8 行设置 Type = RenderTargetType.Default,会根据情况自动选择硬件渲染或软件渲染),适用范围很广。
其缺点首先是不能在 Windows 8 的 Store app 中使用,其次是与 Direct2D 的一些高级功能不能很好的结合。Direct2D 的很多高级功能,如渲染特效,都是需要与 DeviceContext 结合使用的,而 HwndRenderTarget 不能直接使用 DeviceContext 的渲染结果。
二、使用 DeviceContext
DeviceContext 则是一个比较“万能”的类,它可以将结果绘制到任意的 Image 上,在离线渲染与多线程渲染中是非常有用的。
使用 DeviceContext 来绘制 Direct2D 的做法则要复杂很多,由于 DeviceContext 并不能直接将结果渲染到窗口句柄上,因此需要在 DeviceContext 和 hwnd 之间建立起联系,这里使用的是 DXGI 的 SwapChain。
大致的步骤,是先利用 DeviceContext,将结果绘制到一块缓冲区中(BackBuffer 后台缓冲区),然后由 SwapChain 将后台缓冲区的内容,呈现到 hwnd 上,完成一次绘制。
创建时,需要创建 Direct3D Device、DXGI Device 和 Diect2D Device,这样才能创建 DeviceContext。接着再创建 SwapChain 和相应的 Surface 作为缓冲区,才能正常使用。相应的代码如下所示,由于会有很多重名类,因此我用 using 语句定义了很多类型别名,代码看起来会乱一些:
// 创建 Dierect3D 设备。 D3D11Device d3DDevice = new D3D11Device(DriverType.Hardware, DeviceCreationFlags.BgraSupport); DXGIDevice dxgiDevice = d3DDevice.QueryInterface<D3D11Device1>().QueryInterface<DXGIDevice>(); // 创建 Direct2D 设备和工厂。 D2D1Device d2DDevice = new D2D1Device(dxgiDevice); this.deviceContext = new DeviceContext(d2DDevice, DeviceContextOptions.None); // 创建 DXGI SwapChain。 SwapChainDescription swapChainDesc = new SwapChainDescription() { BufferCount = 1, Usage = Usage.RenderTargetOutput, OutputHandle = dcRenderControl.Handle, IsWindowed = true, // 这里宽度和高度都是 0,表示自动获取。 ModeDescription = new ModeDescription(0, 0, new Rational(60, 1), Format), SampleDescription = new SampleDescription(1, 0), SwapEffect = SwapEffect.Discard }; this.swapChain = new SwapChain(dxgiDevice.GetParent<Adapter>().GetParent<DXGIFactory>(), d3DDevice, swapChainDesc); // 创建 BackBuffer。 this.backBuffer = Surface.FromSwapChain(this.swapChain, 0); // 从 BackBuffer 创建 DeviceContext 可用的目标。 this.targetBitmap = new Bitmap1(this.deviceContext, backBuffer); this.deviceContext.Target = targetBitmap;
绘制同样是在控件的 Paint 事件中绘制的,但要记得在最后调用 swapChain.Present 方法,令 SwapChain 将结果呈现在 hwnd 中。
在改变控件大小时,也要复杂很多。因为在调用 SwapChain 的 ResizeBuffers 方法之前,需要先将与 SwapChain 相关的资源全部释放掉,才能调整缓冲区的尺寸,最后还要重建相关的资源。
使用 DiviceContext 的优点很多,包括:
- 可以用于 Windows Store apps。
- DeviceContext 可以绘制到其它目标,只要简单的改变 Target 属性即可。
- 可以创建 Direct2D 特效。
- 可以使用多个 DeviceContext 来进行多线程绘制,可以参见这里。
在下面的 Demo 中,我也简单的演示了其它 DeviceContext 的绘制结果,可以直接被绘制到界面中。
该方法的缺点,就是创建略微复杂,而且需要电脑在一定程度上支持 Direct3D 才可以(具体要支持到什么程度,还不清楚~)。
关于 Direct2D 特效的应用,可以参见《C# 使用 Direct2D 实现斜角效果》。Direct2D 在 WinForm 中的实际应用,可以参见我的拼图游戏。
所有源代码和用到的类库都可以在这里下载。