Namespaces

Variants
Actions

Please note that as of October 24, 2014, the Nokia Developer Wiki will no longer be accepting user contributions, including new entries, edits and comments, as we begin transitioning to our new home, in the Windows Phone Development Wiki. We plan to move over the majority of the existing entries over the next few weeks. Thanks for all your past and future contributions.

Windows 8: 混用CSharp和C++创建DirectX程序

From Wiki
Jump to: navigation, search
SignpostIcon XAML 40.png
WP Metro Icon DirectX.png
WP Metro Icon WP8.png
Article Metadata

代码示例
兼容于
文章
WS_YiLunLuo 在 21 Jun 2012 创建
最后由 hamishwillee 在 19 Jul 2013 编辑

注意:本文介绍的是Windows 8,不是Windows Phone。考虑到很多Windows Phone的开发人员都对Windows 8感兴趣,加上微软今天announce了Windows Phone 8会和Windows 8使用相同的内核,我们决定写一篇和Windows 8开发相关的wiki。

Contents

简介

在Windows 8中,如果你选择XAML+.NET,从很大程度上来讲和使用Silverlight开发一个Windows Phone程序是很像的。但是这两个平台也有很多不同点,其中之一就是Windows 8的Metro应用程序不支持XNA。如果你要将Windows Phone上的XNA游戏迁移到Windows 8,必须使用HTML 5 canvas或者C++和DirectX。其中HTML 5 canvas虽然用起来比较方便,但是威力远远不如DirectX来得强大。

本文将简要介绍如何使用DirectX进行程序开发。我们不会涉及到游戏,只会简要介绍DirectX,并且把话题局限在基本的2D图形。今后如果有时间,我们会介绍更多关于3D和游戏的内容。

Rainbow.png

以上截图中的彩虹全部是用代码生成的,并不是图片。

你可以从File:RadialGradientDemo.zip下载本文提供的源代码。

为什么混用DirectX,C++,C#

如果你要做的是一个很小的休闲游戏,使用C#和XAML或者HTML 5 canvas就足够了。不过,XAML不支持immediate rendering mode,每画一个图形,都必须往visual tree中添加一个元素。这意味着如果你的图形很复杂,游戏性能将会很差。

HTML 5 canvas支持immediate rendering mode,当然,这意味着你必须会HTML和JavaScript。对于一个只会C#的Windows Phone开发人员来说,学习JavaScript也是需要时间的,虽然或许会比C++来得简单。此外,XAML以及HTML 5 canvas不支持3D,不支持shader effect,不支持很多很多高级功能。所以如果你要开发一个大型游戏,还是必须使用DirectX。

虽然有一些第三方的类库对DirectX进行了封装,允许你在C# 程序中调用,但是如果你想发挥出DirectX真正的威力,还是需要学习C++才行。若是你一点不会C++,我们推荐你参考之前的一篇wiki:Windows Phone上的声音录制与编码(三):Windows C++简介,它对C#和C++做了一个比较,方便广大C#开发人员更快捷地对C++入门。

另一方面,C++这门语言确实比较麻烦,即使单纯就语法而言也挺复杂的,尤其是C++ 11中间提供的新功能,甚至让很多对C++ 98了如指掌的人都感到头晕。所以如果可能,我们还是希望把尽可能多的代码放到C#中。

好消息是:对于一个Metro程序而言,你可以混用JavaScript, C#, 以及C++。尤其是C#和C++的混用,相比起JavaScript和C++的混用,有更为强大的支持。例如,你无法在JavaScript中间使用DirectX绘图,即使DirectX相关的代码你是用C++写的(但是如果你用C++ DirectX不是在绘图,而是,例如,生成一张图片保存到硬盘上,那就没有问题)。不过,同样的事情却可以在C# 中实现。因此,我们可以使用C#和XAML完成大多数的功能,例如一些周边的UI。想象一个即使战略游戏,游戏中的各种控件(包括各种菜单项和建造部队用的面板)其实都可以用C#来实现,只有核心的地图、建筑、部队的绘制和交互会需要用到DirectX。

为了实现这种场景,你需要如下组件:

  • 一个使用C#开发的XAML程序
  • 一个使用C++开发的自定义WinRT组件
  • 在C++程序中,使用DirectX编码
  • 在C++程序中,通过SurfaceImageSource让XAML程序可以绘制DirectX图形

接下来,我们首先从C++自定义WinRT组件入手,让大家看看在没有DirectX的情况下,怎样在C#程序中访问一个简单的自定义WinRT组件。

制作并调用一个自定义WinRT组件

你可以使用.NET或者C++制作一个自定义WinRT组件。当然,使用.NET的话,你还是只能用.NET提供的功能。例如,你不能用DirectX, Media Foundation,等技术。既然今天我们介绍的是DirectX,就必须使用C++。

创建项目

用C++开发自定义WinRT组件其实很简单,Visual Studio提供了现成的模版。你可以选择Visual C++ => Windows Metro Style => Windows Rumtime Component来创建一个项目。这个模版会给你一个类,名叫Class1,感觉就好像用C#建立一个Class Library项目一样,只不过这边我们使用的是C++,并且项目属性已经自动帮我们设置好支持C++/CX语法(这是微软对标准C++的扩展,方便大家使用WinRT组件),将结果编译成一个WinRT组件。我们几乎什么都不用设置!

当然,这个类基本上是空的,它什么也做不了。尽管如此,还是先让我们来看看自动生成的这段代码有什么特别的地方:

#pragma once
namespace WindowsRuntimeComponent2
{
public ref class Class1 sealed
{
public:
Class1();
};
}

可以看到,这段代码虽然是C++代码,但是却和C#十分类似。事实上这是C++/CX语法。一般的C++类前面是没有public这样的修饰符的,但是在C++/CX中却有。同样,ref和sealed关键字也是C++/CX特有的。熟悉C#的人肯定知道public和sealed是什么,所以我们仅仅指出自定义WinRT组件中,所有非abstract的public的类规定必须被sealed,因此你无法使用继承(当然,你可以写一个非sealed的internal的类或者public abstract的类,并且让一个sealed的public的类继承它)。至于ref关键字,代表这是一个引用类,当你将这个类的一个实例赋值给另外一个变量时,仅仅是将它的地址复制了一份,而对象的内容没有被复制。如果你不加ref呢,这个类就是by value的,当你赋值时啊,整个对象会被复制一份。这一点和.NET中class是by reference,struct是by value是一致的。因为标准C++中class和struct唯一的区别在于struct默认所有元素都是public的,所以C++/CX不能通过class和struct来区分by reference和by value,只能使用ref class这样的新的关键字了。

除此之外的代码就和标准C++没什么区别了。若是你不会标准C++,请参考Windows Phone上的声音录制与编码(三):Windows C++简介

更多CX语法

考虑到接下来的需要,我们适当多介绍一些C++/CX的语法。

首先,你的组件中只有public ref sealed的类会被暴露给使用者。这三个关键字缺一不可。在组件实现的代码中,你可以使用任意的类,但是要暴露给使用者的类,就必须是public ref sealed的了。

在一个public ref sealed的类中,允许使用的public的变量,属性,方法的参数和返回值,也是有要求的(标准C++没有属性,但是CX有,等一下我们会另行说明)。若是你想要让JavaScript访问你的组件,你只能使用最最基本的类型,例如int,String,等等。当然,也可以是另外一个WinRT类,只要确保那个类中间只使用基本数据类型即可。具体可以使用的数据类型列表可以参考这里。可是,若是你只需要让XAML(不管是C#还是C++)访问你的组件,就可以使用所有XAML支持的类型。例如,等一下我们开发的组件中会使用到Windows::UI::Xaml::Media::ImageBrush,这就意味着我们的组件只能被XAML使用,不能被JavaScript使用。

没有暴露给客户端的类(非public ref sealed的那些类)不受这些限制,你可以使用任意数据类型。你甚至可以使用标准C++类,而不是ref class。

下面我们来说明怎样创建一个属性。C#开发人员都非常熟悉属性,但是标准C++却没有属性,而要求你必须写一个Get方法和一个Set方法。CX为了方便大家,提供了属性的语法。这个语法,呃,真的很简单。你只需要在变量前面添加property关键字就可以了:

property int Age;

这会创建一个自动实现的属性,类似于C#中的:

ing Age { get; set; }

若是你要实现getter和setter,可以使用更完整的语法:

property int Age
{
int get() { return this->m_age; }
void set(int value) { this->m_age = value; }
}

本文不会在C++代码中创建WinRT对象的实例,但是在很多地方你都会看到这样的语法:

String^ str = ref new String();

这明显不是标准C++的语法,只有在创建WinRT对象时才会用到。别小看这一句代码,事实上啊,在背后,编译器会为你做一大堆的事,例如初始化COM(调用RoInitialize),创建对象(调用RoActivateInstance),设置reference count,类型转换,等等等等。如果不用CX,你就必须手工写编所有这些代码,而且是每创建一个对象都要写一次!CX语法虽然不是标准C++,但是确实为我们省去了很多很多麻烦。使用CX,你甚至不一定要关心在后台到底发生了什么,只需要写那么一句话!但是请不要把它和C++/CLI搞错了,它们的语法很像,不过C++/CLI是用来开发.NET程序的,并且在Metro中不支持。而C++/CX开发出来的程序都是native程序,不会用到.NET。

可以看到,CX可说是集合了C++和C#两种语言的特点。CX也支持C#中的委托,事件之类的概念,具体今天我们就不再描述了,大家可以参考[1]。有些C++开发人员不喜欢微软自己订制一套C++方言,但是请理解CX的编译器为你做了多少事,帮你封装了多少COM中麻烦的特性。

在C#中调用自定义WinRT组件

现在,我们可以在C#代码中使用我们的WinRT组件了。为此先创建一个C#版的Metro项目。接下来的事情非常简单,你可以添加一个reference到之前的WinRT组件,就好像引用一个C# Class Library项目一样。然后就是标准的添加namespace,通过C#的new语法创建对象,调用上面的属性和方法了,这是标准的C#语法!

using WindowsRuntimeComponent1;
Class1 c = new Class1();
c.Age = 27;

可以看到,对于一个C#用户而言,根本不用care这个组件是用C++写的,他们可以把它当成一个C#类来使用。事实上绝大多数XAML框架自带的类都是用C++写的,这里面包括Button这样的大家司空见惯的类。很多人可能会想当然地认为XAML中的Button是用C#写的,但其实不是……

进入DirectX的世界

现在我们已经看到了怎样在C++中创建一个简单的自定义WinRT组件,并且在C#中使用它。下一步就是真的用这个组件来做点事了。不过在这之前,我们有必要简单介绍一下DirectX。

DirectX简介

DirectX主要包括以下几个部分:

  • Direct2D:用来绘制2D矢量图形,和Windows Imaging Components合作的话,也可以用来绘制位图
  • DirectWrite:用来制作文本,它仅仅提供文本的信息,例如字体,布局,等等。具体绘制是由Direct2D完成的
  • Direct3D:这应该是大家最熟悉,但其实也是最难的一块了,很多商业游戏都会用到它
  • DirectXMath:一个数学库

除此之外,还有一些和声音播放有关的类库,不过在XAML程序中大家通常会使用MediaElement来播放声音,而要编辑音频则通常会使用Media Foundation。

本文不会涉及到Direct3D和DirectXMath,因为仅仅Direct2D就已经有很多内容了,今天也是介绍不完的。今后有时间我们会给大家介绍Direct3D和DirectXMath。

创建Direct2D项目

Direct2D并不是一个简单的东西,但是Visual Studio给我们提供了现成的项目模版。你可以选择Visual C++ => Windows Metro Style => Direct2D App (XAML)来创建这样一个项目。别忘记在Windows 8中,XAML不仅仅支持.NET,也支持C++。运行这个项目,你会发现它仅仅输出了Hello XAML和Hello DirectX这样简单的几句话,其中Hello DirectX可以通过手指或者鼠标拖拽,此外还通过Application Bar提供了修改背景颜色的功能。但是,如果你看看代码,很可能会觉得头很晕。竟然有那么多个代码文件,而所做的事情只是输出Hello World!事实上呢,由于使用了C++/CX语法,代码已经算很简洁了。之前说过,如果不用CX,仅仅创建一个对象都需要写很多很多代码……没办法,DirectX确实挺复杂的。

但是大家也不要害怕,让我们来分析一下Visual Studio为我们生成的代码。我们不需要自己写噢,只需要分析别人为我们写好的代码就可以了。

项目的入口

熟悉Windows Phone的人肯定会知道App.xaml这个文件和它的code behind。在这边App也是类似的,它是程序的入口。在这里我们会找到大家耳熟能详的OnLaunched方法:

void App::OnLaunched(Windows::ApplicationModel::Activation::LaunchActivatedEventArgs^ pArgs)
{
if (pArgs->PreviousExecutionState == ApplicationExecutionState::Terminated)
{
//TODO: Load state from previously suspended application
}
// Place the frame in the current Window and ensure that it is active
Window::Current->Content = ref new DirectXPage();
Window::Current->Activate();
}

可以看到,这段代码将我们导向了DirectXPage,这也是一个XAML页面。Windows 8的应用程序生命周期和Windows Phone类似,但也有区别。由于时间和篇幅关系,今天我们就不介绍了。

DirectXPage的内容很单纯,就是一个TextBlock,用于输出Hello XAML,以及Application Bar中的按钮,用于修改背景颜色。到这里似乎一切都很简单,尽管code behind使用的是C++,可是一切都还是和Windows Phone那么地相似,没有一点点陌生的感觉。

唯一的问题在于,Hello DirectX这句话是怎么输出的?XAML中没有定义相关的TextBlock,也没看到后台代码中有修改Hello XAML这个TextBlock的值,它到底是怎么来的?事实上,就是这句Hello DirectX,占据了绝大多数的代码量。它是通过Direct Write准备好的文本,再通过Direct2D绘制出来的!

SwapChainBackgroundPanel

如果你再仔细观察XAML页面,会发现它的根元素是一个SwapChainBackgroundPanel,而不是Page。所谓的SwapChainBackgroundPanel,指的是一个容器,在这里可以放XAML元素,但是它的背景则是通过DirectX绘制,而不是通过标准的XAML brush给出的。请注意DirectX使用的是immediate rendering mode,它仅仅提供背景,而没有元素的概念。当然,背景中可以绘制很多很多东西,也可以让背景动起来。除了SwapChainBackgroundPanel,你还可以通过VirtualSurfaceImageSourceSurfaceImageSource来提供背景。等一下我们自己的组件中将会使用SurfaceImageSource,而Visual Studio生成的模版中则使用了了SwapChainBackgroundPanel。

如果你在Windows Phone中混用过Silverlight和XNA,你应该可以预测到了SwapChainBackgroundPanel的背景全部是通过代码绘制的。绝对正确!为了绘制这个背景,你需要处理CompositionTarget.Rendering事件。模版给我们生成的代码如下:

	m_eventToken = CompositionTarget::Rendering::add(ref new EventHandler<Object^>(this, &DirectXPage::OnRendering));

之前我们没有介绍C++/CX中的事件(标准C++没有事件的概念),这边大家会看到,添加/移除事件处理程序和C#中很像,你可以通过+=/-=的方式,或者通过add/remove的方式。在这边当Rendering事件触发时,OnRendering方法会被调用:

void DirectXPage::OnRendering(Platform::Object^ sender, Platform::Object^ args)
{
if (m_renderNeeded)
{
m_timer->Update();
m_renderer->Update(m_timer->Total, m_timer->Delta);
m_renderer->Render();
m_renderer->Present();
m_renderNeeded = false;
}
}

熟悉XNA的人会发现习以为常的Update和Render方法,不过这里还多了一个Present。只有当你使用SwapChainBackgroundPanel时才会出现Present。SwapChainBackgroundPanel,很自然,有一个SwapChain。而这个SwapChain呢,顾名思义,指的是一个可以交换的东西。事实上它的含义是,先在后台绘制出一副画,然后通过Present,将后台的这幅画和前台正在显示的画面交换(swap)一下,于是后台这幅画就被显示出来了。在你没有调用Present之前,所有的绘图工作都是在后台进行的,不会反映在屏幕上。

这段代码中的m_timer和XNA中的GameTimer是同一个概念。只不过,这个类不是DirectX自带的,而是一个名为BasicTimer的,项目模版帮你写的类……限于时间和篇幅,我们不分析这个类了。你可以自行调查。其核心是调用了Windows的QueryPerformanceFrequency函数,实现了一个定时器。它不会自动更新,这就是为什么在OnRendering中要显示调用它的Update方法的原因。

至于m_renderer,这是一个SimpleTextRenderer对象,个SimpleTextRenderer也是项目模版为我们自动生成的一个类。

SimpleTextRenderer

SimpleTextRenderer的代码比较多,事实上它还继承自DirectXBase(另一个模版为我们生成的类,而不是DirectX自带的)。但是熟悉XNA的人会发现,至少这个类的结构和XNA是很像的。它也有Update和Render方法。其中的Update方法是空的,我们也不再描述了。Render是重点,可是在介绍Render之前,大家可能意识到了这个类没有load resource。这是怎么回事?事实上,这是DirectX和XNA的一大不同点。DirectX的资源加载比XNA复杂得多。因为太复杂,所以通常我们会分三步走:CreateDeviceIndependentResources,CreateDeviceResources,CreateWindowSizeDependentResources。

CreateDeviceIndependentResources通常只需要调用一次。在这里创建即使设备发生了变化也不需要改变的资源。所谓的设备,你可以简单认为是显卡,屏幕,等等。这些资源包括各种factory(Direct2D,DirectWrite,以及WIC都使用了factory设计模式),还有text format,用于规定字体,文字布局,等信息。下面我们把DirectXBase和SimpleTextRender中的CreateDeviceIndependentResources的核心代码放在一起给出,省略了一些诸如内存清零之类的前奏。

// These are the resources required independent of the device.
void CreateDeviceIndependentResources()
{
DX::ThrowIfFailed(
D2D1CreateFactory(
D2D1_FACTORY_TYPE_SINGLE_THREADED,
__uuidof(ID2D1Factory1),
&options,
&m_d2dFactory
)
);
DX::ThrowIfFailed(
DWriteCreateFactory(
DWRITE_FACTORY_TYPE_SHARED,
__uuidof(IDWriteFactory),
&m_dwriteFactory
)
);
DX::ThrowIfFailed(
CoCreateInstance(
CLSID_WICImagingFactory,
nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(&m_wicFactory)
)
);
DX::ThrowIfFailed(
m_dwriteFactory->CreateTextFormat(
L"Segoe UI",
nullptr,
DWRITE_FONT_WEIGHT_NORMAL,
DWRITE_FONT_STYLE_NORMAL,
DWRITE_FONT_STRETCH_NORMAL,
42.0f,
L"en-US",
&m_textFormat
)
);
DX::ThrowIfFailed(
m_textFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING)
);
}

注意创建这些资源的方式。我们通过调用全局函数来创建factory:使用D2D1CreateFactory创建Direct2D factory,使用DWriteCreateFactory创建DirectWrite factory。至于WIC(Windows Imaging Components),没有提供一个特别的函数来创建factory,所以必须使用普通的COM的CoCreateInstance。很遗憾,CX语法只能简化WinRT对象的创建,传统COM对象还是必须自行创建……通常在创建好一个COM对象之后,你还必须手工管理reference count,以防止内存泄露,或者重复delete。不过,正如在Windows Phone上的声音录制与编码(三):Windows C++简介一文中指出的那样,我们推荐大家使用smart pointer来管理指针。在WinRT中,即使管理COM对象,也可以使用Microsoft::WRL::ComPtr(但不能使用STL的shared_ptr和unique_ptr,因为COM的reference counting和它们是不同的)。如果你观察DirectXBase.h,会发现它大量使用了ComPtr。

有了factory,就可以用这些factory创建其它资源了。例如DirectWrite的factory可以用来创建text format。注意text format不包括具体的文字,也不包含绘图用的代码。它仅仅提供了字体、字号、布局等信息。

最后注意DX::ThrowIfFailed这个函数的使用。这也是项目模版帮我们创建的一个帮助函数:

inline void ThrowIfFailed(HRESULT hr)
{
if (FAILED(hr))
{
// Set a breakpoint on this line to catch DirectX API errors
throw Platform::Exception::CreateException(hr);
}
}

它是一个inline函数,这意味着编译器会将这段代码直接嵌入到调用者中,运行时会直接执行这段代码而不是调一个函数。但更重要的是这个函数的实现。几乎所有DirectX函数都会返回一个HRESULT,而FAILED宏会检查HRESULT的值,如果这个返回值代表操作失败,这段代码就会抛出一个异常,这时候HRESULT的值代表错误代码,它是一个16进制的数,你可以通过查阅文档了解它代表的含义。出于历史原因,DirectX和COM都采取了HRESULT,而不是抛出异常的方式来处理错误,如果一个操作失败,可你不管返回值继续执行代码,就很可能会产生更多更奇怪的错误,搞到最后让你很难找到错误的根源。所以请一定要记住把所有返回HRESULT的DirectX函数的调用都包在ThrowIfFailed中,它会在第一时间帮你抛出异常,避免更多更奇怪的错误发生。

下一步是CreateDeviceResources。在这里要创建的资源都是和设备相关的。例如,当显卡发生变化时,这些资源必须被重新创建。由于篇幅关系,我们不再把完整的代码贴出,反正大家都可以参考项目模版生成的DirectXBase和SimpleTextRenderer中的CreateDeviceResources方法。这边我们仅仅列出基本步骤,注意这并不是完整的代码:

void CreateDeviceResources()
{
D3D11CreateDevice();
m_d2dFactory->CreateDevice();
m_d2dDevice->CreateDeviceContext();
m_d2dContext->CreateSolidColorBrush();
m_dwriteFactory->CreateTextLayout();
m_d2dContext->CreateBitmap(m_opacityBitmap);
m_d2dContext->SetTarget(m_opacityBitmap.Get());
m_d2dContext->BeginDraw();
m_d2dContext->Clear();
m_d2dContext->DrawTextLayout();
m_d2dContext->EndDraw();
m_d2dContext->SetTarget(m_d2dTargetBitmap.Get());
}

在DirectXBase的CreateDeviceResources方法中,代码通过调用D3D11CreateDevice创建了一个Direct3D device。Direct3D没有factory,全部资源都要通过调用全局函数来创建。你可能会觉得奇怪,为什么会用到Direct3D,答案是,在Metro中Direct2D必须绘制到一个位图或者一个DXGI (DirectX Graphic Infrastructure) surface上,而DXGI由于历史原因是和Direct3D紧密关联的。这个项目模版不会创建任何3D资源,它创建Direct3D device纯粹是为了创建DXGI。事实上啊,如果你使用Direct2D仅仅是为了绘制一张图片,而不需要显示在屏幕上,就根本不需要创建Direct3D device了。接下来还做了一系列类型转换,将Direct3D device转换成Direct3D device context和DXGI device,这些就不详细介绍了。

然后,使用Direct2D factory的CreateDevice方法创建一个Direct2D device,再用这个Direct2D device的CreateDeviceContext方法创建一个Direct2D device context,最后在SimpleTextRenderer中使用这个context的CreateSolidColorBrush方法创建一个单色刷。看到这里,你可能会抱怨为什么要那么多对象。事实上,理想情况下,Direct2D和Direct3D是完全可以分开使用的。如果你不需要Direct3D,就不要创建Direct3D device,只需要创建Direct2D device就可以啦(当然,在这边为了创建DXGI surface,还是需要Direct3D)。至于device context,更确切地说它是一个render target,事实上ID2D1DeviceContext正是继承自ID2D1RenderTarget的。所谓的render target,简单说来就是你的画要画到哪里。在桌面程序中我们通常创建一个HWND render target,意味着要画到一个窗口上。Metro程序没有HWND,所以就用一个所谓的device context来代表render target,最终它会和DXGI关联起来。通过render target,我们可以创建诸如各种刷子之类的绘图用的资源。

这段代码中还创建了一个DirectWrite的text layout。text layout和text format很像,不过它包括了具体要绘制的那句话的值(在这里就是Hello DirectX)。之后,它又创建了一个名为m_opacityBitmap的临时的bitmap,并且将文本绘制到这个bitmap之上。注意这边已经出现了一部分和绘图相关的代码,不过这里并不是将图形画到屏幕上,只是画到一个临时的位图中。事实上你可以去掉这一部分代码,对于程序的运行没有影响。关于绘图的代码我们等一下再解释。绘图完成后,再将Direct2D device context的target设置成target bitmap,这会在下一步中创建。

再下一步是CreateWindowSizeDependentResources。在这里创建的资源,很自然,每当屏幕大小发生改变时都必须重新创建。这边创建的资源包括这些(再次提醒,这不是完整的代码,而是大幅简化过的):

void CreateWindowSizeDependentResources()
{
dxgiFactory->CreateSwapChainForComposition();
panelNative->SetSwapChain(m_swapChain);
m_d3dDevice->CreateRenderTargetView(m_swapChain);
m_d3dContext->RSSetViewports(renderTargetView.size);
m_d2dContext->CreateBitmapFromDxgiSurface(m_swapChain);
m_d2dContext->SetTarget(m_d2dTargetBitmap);
}

首先,使用DXGI factory的[CreateSwapChainForComposition http://msdn.microsoft.com/en-us/library/windows/desktop/hh404558(v=vs.85).aspx]方法创建一个swap chain(这里的DXGI factory其实就是Direct3D device,不过进行了类型转换),并且然后将这个swap chain和XAML中的SwapChainBackgroundPanel关联起来。然后,通过Direct3D device创建一个render target view,这东西是和swap chain有关的(请注意事实上CreateRenderTargetView并不接受swap chain做参数,我们上面的代码做了大幅简化,只是为了说明render target view的创建需要用到swap chain)。再来是通过render target view获取大小相关的数据,并且设置viewport。所谓的viewport,就是用来显示图形的一块区域。最后,通过DXGI创建一个位图,并且将Direct2D context的target设置成它,这意味着所有的绘制都是画到这个位图上去的。由于DXGI是和swap chain相关的,所以综合起来,整个绘图过程就是:先绘制到一个DXGI surface上,形成一个位图,然后将DXGI和swap chain关联起来,并且通过swap,将后台的位图显示到屏幕上。

至此,总算所有的资源都创建完毕了。大家也许都会觉得DirectX创建资源非常麻烦。好在这边很多代码都是可以复用的,事实上就算你不用这个项目模版,你还是可以将DirectXBase这个类直接拿到你自己的项目中,正如等一下我们会将它拿到我们的自定义WinRT组件中那样。

剩下的最后一件事就是真正的绘图了。也许会出乎你的意料,这一步比起创建资源要来得简单得多!以下是Render方法中的核心代码(我们去掉了头和尾,以及一些控制逻辑):

void SimpleTextRenderer::Render()
{
m_d2dContext->SetTransform(D2D1::Matrix3x2F::Identity());
m_d2dContext->BeginDraw();
m_d2dContext->Clear(D2D1::ColorF(sc_bgColors[m_bgColorIndex]));
m_d2dContext->SetTransform(translation);
m_d2dContext->DrawTextLayout(
D2D1::Point2F(0.0f, 0.0f),
m_textLayout.Get(),
m_blackBrush.Get(),
D2D1_DRAW_TEXT_OPTIONS_NO_SNAP
);
HRESULT hr = m_d2dContext->EndDraw();
if (hr == D2DERR_RECREATE_TARGET)
{
m_d2dContext->SetTarget(nullptr);
m_d2dTargetBitmap = nullptr;
CreateWindowSizeDependentResources();
}
}

绘图的过程通常就是:先设置一个初始的变换矩阵(这边设置成identity),然后调BeginDraw,再调Clear设置一个背景色,接着就调用各种Draw***/Fill***相关的方法画图。在画图的过程中,你也可以不断改变变换矩阵,从而让接下来画的内容画到不同的位置,或者进行缩放和旋转。最后,调用EndDraw结束绘制。在绘图的过程中,没有一个方法会返回HRESULT。在你调用EndDraw时才会统一产生一个HRESULT。注意这里不要简单调用ThrowIfFailed,因为如果EndDraw返回的是D2DERR_RECREATE_TARGET,并不代表画画时发生了错误,只是说明你需要重新创建资源而已。

如果你熟悉HTML 5 canvas,会看到Direct2D绘图的代码和canvas其实是很像的。在这边可能会需要用到一些图形学的知识,限于时间和篇幅我们就不详细说明了。大家有兴趣可以去网上查找图形学相关资料。事实上在DirectX中需要用到的图形学知识不会很多,最常见的就是变换矩阵了。

至此,我们总算基本上解释完了这个Hello World程序。剩下的还有一些input相关的交互操作,大家可以自行学习。事实上虽然DirectX也提供了自己的input API,不过在XAML中通常我们会使用Pointer***事件,那要方便多了。

我们承认,DirectX是比较复杂,这个模版仅仅用到了DirectX中很小的一部分功能。但是好消息是,模版所提供的大部分代码都是可以复用的。如果你暂时无法理解全部代码,没有关系,花时间慢慢调查吧。就算没有完全理解透彻,你还是可以直接把这些代码拿到自己的项目中去使用。这也正是接下来我们要做的一件事。

在WinRT组件中使用DirectX

在WinRT组件中使用DirectX和新建一个DirextX项目非常相似,但是也稍微有点不同。

复用项目模版

首先,自定义WinRT组件的项目模版没有提供DirectXBase.h,DirectXBase.cpp,以及DirectXHelper.h这三个文件。但这不是障碍,我们可以直接把之前项目模版为我们创建的那三个文件添加到自定义WinRT组件项目中。

不过你要做一个小小的改动。在DirectX程序中,我们使用SwapChainBackgroundPanel,所以需要用到一个swap chain。在我们的WinRT组件中,我们打算使用另外一种方式:SurfaceImageSource。SurfaceImageSource让我们的组件能够完全封装DirectX相关的逻辑,并且返回一个XAML brush。这样一来,在C#方面,就可以完全不用管任何和DirectX相关的代码,只是单纯使用这个brush就可以了。

为此,我们不能使用swap chain,而必须直接使用DXGI。之前项目模版给我们生成的代码中,并没有把DXGI device暴露出来,所以让我们在DirectXBase的public变量中添加一项:

    Microsoft::WRL::ComPtr<IDXGIDevice>				       m_dxgiDevice;

随后,在CreateDeviceResources方法中,将临时变量dxgiDevice全部替换成this->m_dxgiDevice。其实也就两个地方:

// Obtain the underlying DXGI device of the Direct3D11.1 device.
DX::ThrowIfFailed(
m_d3dDevice.As(&this->m_dxgiDevice)
);
// Obtain the Direct2D device for 2-D rendering.
DX::ThrowIfFailed(
m_d2dFactory->CreateDevice(this->m_dxgiDevice.Get(), &m_d2dDevice)
);

这就是我们对项目模版所做的全部更改了。接下来,我们可以focus在自己的组件上。

自定义组件的实现

你可以在你的组件中绘制任何图画。我们的idea是使用radial gradient brush。Metro XAML application是不支持radial gradient brush的(但是HTML和DirectX都支持),为了在XAML程序里使用圆周渐变刷,你要么事先画好一张位图,要么就是用DirectX。

我们给自己的组件起名为RadialGradientBrush,让它继承自DirectXBase,从而可以使用模版为我们生成的所有那些功能。因为DirectXBase是一个abstract类,就算它没有被sealed掉,也可以在自定义WinRT组件中使用,而C#端则无法看到它,它对于使用我们组件的人来说是完全透明的。

在为RadialGradientBrush的构造函数中,传入几个参数,代表要绘制的图形的大小,以及渐变刷的stop。注意如果你想让你的组件被JavaScript调用,是不能用DirectX的,我们的组件只能被XAML使用。WinRT中没有.NET BCL中的诸如List<T>之类的类,但是有类似的基本数据结构。例如IVector<T>就是WinRT中取代List<T>的一个类。我们在这边使用IVector<GradientStop^>^,在.NET中调用这个方法时,它会自动被转换成标准的GradientStopCollection。

RadialGradientBrush::RadialGradientBrush::RadialGradientBrush(uint32 pixelWidth, uint32 pixelHeight, IVector<GradientStop^>^ stops)
{
if (pixelWidth <= 0 || pixelHeight <= 0 || stops->Size == 0 || stops->Size > 20)
{
throw ref new InvalidArgumentException();
}
this->m_pixelWidth = pixelWidth;
this->m_pixelHeight = pixelHeight;
this->m_dpi = static_cast<float>(DisplayProperties::LogicalDpi);
this->m_stops = stops->GetView();
 
// Associate ISurfaceImageSourceNative with SurfaceImageSource.
this->m_surfaceImageSource = ref new SurfaceImageSource(this->m_pixelWidth, this->m_pixelHeight);
IInspectable* sisInspectable = (IInspectable*)reinterpret_cast<IInspectable*>(this->m_surfaceImageSource);
DX::ThrowIfFailed(
sisInspectable->QueryInterface(__uuidof(ISurfaceImageSourceNative), (void**)&this->m_sisNative)
);
 
this->CreateDeviceIndependentResources();
this->CreateDeviceResources();
DX::ThrowIfFailed(
this->m_sisNative->SetDevice(this->m_dxgiDevice.Get())
);
this->Render();
}

构造函数的实现基本上就是做一些初始化而已,它会用到很多变量。以下是那些变量的定义:

private:
Windows::UI::Xaml::Media::ImageBrush^ m_resultBrush;
uint32 m_pixelWidth;
uint32 m_pixelHeight;
Windows::Foundation::Collections::IVectorView<Windows::UI::Xaml::Media::GradientStop^>^ m_stops;
Windows::UI::Xaml::Media::Imaging::SurfaceImageSource^ m_surfaceImageSource;
Microsoft::WRL::ComPtr<ISurfaceImageSourceNative> m_sisNative;
Microsoft::WRL::ComPtr<ID2D1Bitmap1> m_bitmap;
Microsoft::WRL::ComPtr<ID2D1RadialGradientBrush> m_d2dBrush;
D2D1_ELLIPSE m_ellipse;
 
float ConvertColorComponent(unsigned char component);
void InvokeEndDraw();

这里需要注意的有SurfaceImageSource,之前说过我们的组件使用SurfaceImageSource而不是SwapChainBackgroundPanel。SurfaceImageSource继承自XAML的ImageSource,因此可以被直接应用于一个ImageBrush之上。但是为了使用SurfaceImageSource,你还需要创建一个ISurfaceImageSourceNative,我们之前构造函数实现重的“// Associate ISurfaceImageSourceNative with SurfaceImageSource.”部分把它们两个关联了起来。

除此之外,在构造函数中我们还设置了this->m_dpi = static_cast<float>(DisplayProperties::LogicalDpi);这一步是在设置DPI。项目模版中DPI的设置涉及到CoreWindow,而我们这边则通过DisplayProperties::LogicalDpi直接获取DPI。

剩下的就是一些Direct2D相关的资源了,为了创建它们,我们要重写CreateDeviceResources方法,在这里首先调用基类的CreateDeviceResources创建之前我们分析过的那么多资源。

void RadialGradientBrush::RadialGradientBrush::CreateDeviceResources()
{
DirectXBase::CreateDeviceResources();
uint32 halfWidth = this->m_pixelWidth / 2;
uint32 halfHeight = this->m_pixelHeight / 2;
 
// Create gradient.
ComPtr<ID2D1GradientStopCollection> pGradientStops;
D2D1_RADIAL_GRADIENT_BRUSH_PROPERTIES brushProperties = D2D1::RadialGradientBrushProperties(
D2D1::Point2F(halfWidth, halfHeight),
D2D1::Point2F(0, 0),
halfWidth - 30,
halfHeight - 30);
uint32 size = this->m_stops->Size;
D2D1_GRADIENT_STOP gradientStops[20];
int i = 0;
std::for_each(begin(this->m_stops), end(this->m_stops), [this, &i, &gradientStops](GradientStop^ stop)
{
Windows::UI::Color a = stop->Color;
D2D1_COLOR_F color = D2D1::ColorF(this->ConvertColorComponent(stop->Color.R), this->ConvertColorComponent(stop->Color.G), this->ConvertColorComponent(stop->Color.B), this->ConvertColorComponent(stop->Color.A));
gradientStops[i].color = color;
gradientStops[i].position = stop->Offset;
i++;
});
DX::ThrowIfFailed(
this->m_d2dContext->CreateGradientStopCollection(gradientStops, this->m_stops->Size, &pGradientStops)
);
DX::ThrowIfFailed(
this->m_d2dContext->CreateRadialGradientBrush(brushProperties, pGradientStops.Get(), &m_d2dBrush)
);
 
// Create ellipse.
this->m_ellipse.point = D2D1::Point2F(halfWidth, halfHeight);
this->m_ellipse.radiusX = halfWidth - 30;
this->m_ellipse.radiusY = halfHeight - 30;
}

看上去似乎代码很多,其实无非就是创建了一个圆周渐变刷和一个椭圆而已。注意我们将XAML的GradientStopCollection转换成了Direct2D的D2D1_GRADIENT_STOP。没有现成的API提供了这个转换,所以只能由我们自己一个stop一个stop地转。这边的[this, &i, &gradientStops]这一段是标准C++中的lambda expression语法,和C#中的lambda expression是同一个概念。

让我们返回构造函数的最后几行,在创建完所有资源之后,我们将ISurfaceImageSourceNative的device设置成DXGI device。这样一来,当我们往DXGI device上绘图时,就会画到ISurfaceImageSourceNative上了。

    DX::ThrowIfFailed(
this->m_sisNative->SetDevice(this->m_dxgiDevice.Get())
);
this->Render();

最后是Render:

void RadialGradientBrush::RadialGradientBrush::Render()
{
// Draw to SurfaceImageSource.
ComPtr<IDXGISurface> dxgiSurface;
ComPtr<ID2D1Bitmap1> dxgiBitmap;
RECT rect = { 0, 0, this->m_pixelWidth, this->m_pixelHeight };
POINT point = { 0, 0 };
DX::ThrowIfFailed(
this->m_sisNative->BeginDraw(rect, &dxgiSurface, &point)
);
DX::ThrowIfFailed(
this->m_d2dContext->CreateBitmapFromDxgiSurface(dxgiSurface.Get(), nullptr, &dxgiBitmap)
);
this->m_d2dContext->SetTarget(dxgiBitmap.Get());
this->m_d2dContext->BeginDraw();
this->m_d2dContext->Clear(D2D1::ColorF(D2D1::ColorF::CornflowerBlue));
this->m_d2dContext->FillEllipse(m_ellipse, m_d2dBrush.Get());
this->InvokeEndDraw();
DX::ThrowIfFailed(
this->m_sisNative->EndDraw()
);
this->m_resultBrush = ref new ImageBrush();
this->m_resultBrush->ImageSource = this->m_surfaceImageSource;
}

注意到和标准的Direct2D代码稍有不同,我们在这边首先调用了ISurfaceImageSourceNative的BeginDraw方法,获取对应的DXGI surface(其实就是之前关联起来的那个DXGI device)。然后使用Direct2D device context在这个DXGI surface上创建一个位图,并且将所有图画都画到这个位图上。画完之后不要忘记调用ISurfaceImageSourceNative::EndDraw。因为之前的关联,往DXGI surface上画画,就等于在往ISurfaceImageSourceNative上绘图,也等于在往SurfaceImageSource上渲染。

最后,我们创建一个ImageBrush,将SurfaceImageSource作为它的ImageSource传进来。这个ImageBrush作为一个属性给出:

property Windows::UI::Xaml::Media::ImageBrush^ Brush
{
Windows::UI::Xaml::Media::ImageBrush^ get() { return this->m_resultBrush; }
}

使用自定义组件

最后,我们在C#中使用这个组件。作为一个简单的实验,我们创建一个矩形:

<Rectangle x:Name="rect" Width="800" Height="600"  VerticalAlignment="Center"/>

然后在C#中创建一个GradientStopCollection,将它传到自定义组件中,并且将组件返回的ImageBrush赋值给Rectangle的Fill。为了让结果更像是一个彩虹,我们给Rectangle加一个clip。请注意所有这些都是标准的XAML(或者你可以说是Silverlight)代码。DirectX只是给我们提供了一个ImageBrush而已。

private void MainPage_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (this.rect.ActualWidth != 0d && this.rect.ActualHeight != 0d)
{
GradientStopCollection stops = new GradientStopCollection();
stops.Add(new GradientStop() { Color = Colors.Transparent, Offset = 0.866d });
stops.Add(new GradientStop() { Color = Colors.Purple, Offset = 0.9 });
stops.Add(new GradientStop() { Color = Colors.Blue, Offset = 0.9166 });
stops.Add(new GradientStop() { Color = Colors.Aqua, Offset = 0.9332 });
stops.Add(new GradientStop() { Color = Colors.LightGreen, Offset = 0.9498 });
stops.Add(new GradientStop() { Color = Colors.Yellow, Offset = 0.9664 });
stops.Add(new GradientStop() { Color = Colors.Orange, Offset = 0.983 });
stops.Add(new GradientStop() { Color = Colors.Red, Offset = 1d });
this.gradient = new RadialGradientBrush.RadialGradientBrush((uint)this.rect.ActualWidth, (uint)this.rect.ActualHeight, stops);
this.rect.Fill = gradient.Brush;
this.rect.Clip = new RectangleGeometry() { Rect = new Rect(0d, 0, this.rect.ActualWidth, this.rect.ActualHeight / 5) };
}

总结

很多开发人员可能都会觉得使用DirectX不值得,要写太多太多的代码,并且抱怨微软不在Metro中支持XNA。可是请注意,今天我们做的演示只是冰山一脚,这么点功能HTML 5 canvas也能实现。DirectX真正的威力在于shader effect和3D。当然,使用强大的功能是需要你付出更多的时间和精力写代码来换取的。今后如果有时间,我们会给大家介绍shader effect和3D相关的内容。

This page was last modified on 19 July 2013, at 04:13.
557 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×