操作实例:阅读器应用(DirectX和XAML)

随着平板电脑变得越来越普及,融合了阅读体验的应用很快就会变得流行起来。 下面我们介绍如何使用 DirectX、C++ 和 XAML 创建丰富的文档阅读器应用。我们涵盖了此类型应用的结构、技术和***实践,包括如何使用 Virtual Surface Image Source 在 DirectX 绘图代码和 XAML 界面管理之间进行交互操作,使用 DirectWrite 加载字体,使用 WIC 加载图像,以及将 Direct2D 图像效果应用到图像。

我们将了解使用 XAML 互操作的 DirectX 杂志应用示例的大量代码并探讨构成应用的不同组件。

杂志示例结构

示例中的文件分为三组:XML、XAML 和 DirectX。XML 组中的文件负责表示 XML 文件(容纳有关文章的所有信息)中的每个标记。XAML 组中的文件负责设置 XAML 元素并与Virtual Surface Image Source协作。***,DirectX 组中的文件负责像 DirectWrite 自定义字体加载、Direct2D 图像效果和 WIC 图像解码这样的事情。

名称 类型
BindablePropertyXAML
ContentImageSourceXAML
DesignXML
DocumentXML
ElementXML
FontFileStreamDirectX
FontLoaderDirectX
ImageXML
ImageFileXML
ImageFrameXML
LayerXML
ListXML
PageXML
PageModelXAML
PageRollXML
RectangleXML
ResourceXML
StoryXML
TextXML
TextFrameXML
TreeIteratorXML

虚拟图面图像源

Virtual Surface Image Source (VSIS)是一个 XAML 管理的呈现图面,当你要编写一个涉及平移和缩放的应用时,它将非常有用。此图面与 DirectX 中的任何其他位图源相同,除了 XAML 管理 DirectX 和图像源之间的交互操作这一事实。

当你希望在应用中使用 VSIS 时,你必须:

  • 确定内容的大小。
  • 创建图像大小的 VSIS
  • 将 DXGI 设备设置为 VSIS 上的设备。
  • 注册图像源回调,以便当你需要呈现内容时收到消息。

下面的代码显示如何执行这些步骤。

 
 
 
  1. // Measure the content and store its size. 
  2.     Measure(&m_contentSize); 
  3.  
  4.     // Create an image source at the initial pixel size. 
  5.     m_imageSource = ref new VirtualSurfaceImageSource(m_contentSize.cx, m_contentSize.cy); 
  6.  
  7.     ComPtr unknown(reinterpret_cast(m_imageSource)); 
  8.     unknown.As(&m_imageSourceNative); 
  9.  
  10.     auto renderer = m_document->GetRenderer(); 
  11.  
  12.     // Set DXGI device to the image source 
  13.     ComPtr dxgiDevice; 
  14.     renderer->GetDXGIDevice(&dxgiDevice); 
  15.     m_imageSourceNative->SetDevice(dxgiDevice.Get()); 
  16.  
  17.     // Register image source's update callback so update can be made to it. 
  18.     m_imageSourceNative->RegisterForUpdatesNeeded(this); 

选择,你有了一个 VSIS 并注册你的类接收来自虚拟图面的回调信息。接下来,你必须实现当你需要更新内容时 VSIS 调用的回调方法。

当用户通过滚动内容来操作 VSIS 时,VSIS 将调用你注册的类的 UpdatesNeeded 方法。因此,你必须实现 UpdatesNeeded 回调方法。

下面的代码向你显示了如何实现 UpdatesNeeded 回调方法和 Draw 帮助程序方法。当 VSISContentImageSource 类上调用此回调方法时,此方法将检索 VSIS 正在使用 VSISGetUpdateRectCount 方法呈现的更新的矩形。然后,你对此更新的区域调用绘制方法。

 
 
 
  1. / This method is called when the framework needs to update region managed by 
  2. // the virtual surface image source. 
  3. HRESULT STDMETHODCALLTYPE ContentImageSource::UpdatesNeeded() 
  4.     HRESULT hr = S_OK; 
  5.  
  6.     try 
  7.     { 
  8.         ULONG drawingBoundsCount = 0; 
  9.  
  10.         DX::ThrowIfFailed( 
  11.             m_imageSourceNative->GetUpdateRectCount(&drawingBoundsCount) 
  12.             ); 
  13.  
  14.         std::unique_ptr drawingBounds(new RECT[drawingBoundsCount]); 
  15.  
  16.         DX::ThrowIfFailed( 
  17.             m_imageSourceNative->GetUpdateRects(drawingBounds.get(), drawingBoundsCount) 
  18.             ); 
  19.  
  20.         // This code doesn't try to coalesce multiple drawing bounds into one. Although that 
  21.         // extra process  reduces the number of draw calls, it requires the virtual surface 
  22.         // image source to manage non-uniform tile size, which requires it to make extra copy 
  23.         // operations to the compositor. By using the drawing bounds it directly returns, which are 
  24.         //  non-overlapping  tiles of the same size, the compositor can use these tiles directly, 
  25.         // which can greatly reduce the amount of memory needed by the virtual surface image source. 
  26.         // This results in more draw calls, but Direct2D can accommodate them 
  27.         // without significant impact on presentation frame rate. 
  28.         for (ULONG i = 0; i < drawingBoundsCount; ++i) 
  29.         { 
  30.             if (Draw(drawingBounds[i])) 
  31.             { 
  32.                 // Drawing isn't complete. This can happen when the content is still being 
  33.                 // asynchronously loaded. Inform the image source to invalidate the drawing 
  34.                 // bounds so that it calls back to redraw. 
  35.                 DX::ThrowIfFailed( 
  36.                     m_imageSourceNative->Invalidate(drawingBounds[i]) 
  37.                     ); 
  38.             } 
  39.         } 
  40.     } 
  41.     catch (Platform::Exception^ exception) 
  42.     { 
  43.         hr = exception->HResult; 
  44.     } 
  45.  
  46.     return hr; 
  47.  
  48. bool ContentImageSource::Draw(RECT const& drawingBounds) 
  49.     ComPtr dxgiSurface; 
  50.     POINT surfaceOffset = {0}; 
  51.  
  52.     DX::ThrowIfFailed( 
  53.         m_imageSourceNative->BeginDraw( 
  54.             drawingBounds, 
  55.             &dxgiSurface, 
  56.             &surfaceOffset 
  57.             ) 
  58.         ); 
  59.  
  60.     auto renderer = m_document->GetRenderer(); 
  61.  
  62.     ComPtr d2dDeviceContext; 
  63.     renderer->GetD2DDeviceContext(&d2dDeviceContext); 
  64.  
  65.     ComPtr bitmap; 
  66.     DX::ThrowIfFailed( 
  67.         d2dDeviceContext->CreateBitmapFromDxgiSurface( 
  68.             dxgiSurface.Get(), 
  69.             nullptr, 
  70.             &bitmap 
  71.             ) 
  72.         ); 
  73.  
  74.     // Begin the drawing batch 
  75.     d2dDeviceContext->BeginDraw(); 
  76.  
  77.     // Scale content design coordinate to the display coordinate, 
  78.     // then translate the drawing to the designated place on the surface. 
  79.     D2D1::Matrix3x2F transform = 
  80.         D2D1::Matrix3x2F::Scale( 
  81.             m_document->DesignToDisplayWidth(1.0f), 
  82.             m_document->DesignToDisplayHeight(1.0f) 
  83.             ) * 
  84.         D2D1::Matrix3x2F::Translation( 
  85.             static_cast(surfaceOffset.x - drawingBounds.left), 
  86.             static_cast(surfaceOffset.y - drawingBounds.top) 
  87.             ); 
  88.  
  89.     // Prepare to draw content. This is the appropriate time for content element 
  90.     // to draw to an intermediate if there is any. It is important for performance 
  91.     // reason that you don't call SetTarget too often. Preparing the intermediates 
  92.     // upfront reduces the number of times the render target switches back and forth. 
  93.     bool needRedraw = m_content->PrepareToDraw( 
  94.         m_document, 
  95.         transform 
  96.         ); 
  97.  
  98.     if (!needRedraw) 
  99.     { 
  100.         // Set the render target to surface given by the framework 
  101.         d2dDeviceContext->SetTarget(bitmap.Get()); 
  102.  
  103.         d2dDeviceContext->SetTransform(D2D1::IdentityMatrix()); 
  104.  
  105.         // Constrain the drawing to the designated portion of the surface 
  106.         d2dDeviceContext->PushAxisAlignedClip( 
  107.             D2D1::RectF( 
  108.                 static_cast(surfaceOffset.x), 
  109.                 static_cast(surfaceOffset.y), 
  110.                 static_cast(surfaceOffset.x + (drawingBounds.right - drawingBounds.left)), 
  111.                 static_cast(surfaceOffset.y + (drawingBounds.bottom - drawingBounds.top)) 
  112.                 ), 
  113.             D2D1_ANTIALIAS_MODE_ALIASED 
  114.             ); 
  115.  
  116.         // The Clear call must follow the PushAxisAlignedClip call. 
  117.         // Placing the Clear call before the clip is set violates the contract of the 
  118.         // virtual surface image source in that the app draws outside the 
  119.         // designated portion of the surface the image source hands over to it. This 
  120.         // violation won't actually cause the content to spill outside the designated 
  121.         // area because the image source will safeguard it. But this extra protection 
  122.         // has a runtime cost associated with it, and in some drivers this cost can be 
  123.         // very expensive. So the best performance strategy here is to never create a 
  124.         // situation where this protection is required. Not drawing outside the appropriate 
  125.         // clip does that the right way. 
  126.         d2dDeviceContext->Clear(D2D1::ColorF(0, 0)); 
  127.  
  128.         // Draw the content 
  129.         needRedraw = m_content->Draw( 
  130.             m_document, 
  131.             transform 
  132.             ); 
  133.  
  134.         d2dDeviceContext->PopAxisAlignedClip(); 
  135.  
  136.         d2dDeviceContext->SetTarget(nullptr); 
  137.     } 
  138.  
  139.     // End the drawing 
  140.     DX::ThrowIfFailed( 
  141.         d2dDeviceContext->EndDraw() 
  142.         ); 
  143.  
  144.     // Submit the completed drawing to the framework 
  145.     DX::ThrowIfFailed( 
  146.         m_imageSourceNative->EndDraw() 
  147.         ); 
  148.  
  149.     return needRedraw; 

#p#

实现回调后,你的应用现在将从 VSIS 接收有关你需要绘制的区域的信息。你的 draw 函数只需要获取此矩形,并绘制到 VSIS 的右侧区域中。通过实现这两个回调,并向 VSIS 注册类,应用便可以在用户操作内容时呈现到虚拟图面并更新必要的区域,以显示更多图面。

使用 XML 加载内容和绑定数据

我们的应用显示在杂志中的信息都存储在 Sample.story 文件中。此文件是一个 XML 文档,它包含有关文章内容、标题、字体、背景图像和其他属性的信息。 应用需要编入摘要的每一部分重要信息都有一个标记。通过使用标记,你可以轻松地展开文章,从该标记的信息中创建对象,以及将该数据绑定到你的应用的 XAML 中。

因为 .story 文件中的信息采用自定义的 XML 架构,所以 TreeIterator.h 中的类帮助分析此 XML 数据并将其置于应用的专用类中。

下面是应用使用的 .story 文件中包含的信息示例。

 
 
 
  1.  
  2.  
  3.      
  4.          
  5.          
  6.         A BUG'S LIFE 
  7.         GRASSHOPPER 
  8.         a slender plant-eating flying and jumping insect that produces a buzzing sound by rubbing its back legs against its forewings 
  9.         LOREM ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. 
  10. In hac habitasse platea dictumst. Curabitur at lacus ac velit ornare lobortis. Curabitur a felis in nunc fringilla tristique. Morbi mattis ullamcorper velit. Phasellus gravida semper nisi. Nullam vel sem. Pellentesque libero tortor, tincidunt et, tincidunt eget, semper nec, quam. Sed hendrerit. Morbi ac felis. Nunc egestas, augue at pellentesque laoreet, felis eros vehicula leo, at malesuada velit leo quis pede. Donec interdum, metus et hendrerit aliquet, dolor diam sagittis ligula, eget egestas libero turpis vel mi. Nunc nulla. Fusce risus nisl, viverra et, tempor et, pretium in, sapien. Donec venenatis vulputate lorem. 

如 XML 代码段所示,信息的标记直接对应于应用中的某些 XML 类。当迭代器分析 XML 文件时,将为每个标记创建一个相同名称的类,在其中存储标记中的信息。

在我们将 XML 文件中的所有信息都存储到应用后,你将该数据绑定到 UI 上的元素。此数据绑定存在于两个位置,页面的 XAML 标记中及 XAML 标记页面随附的 cpp 文件中。在此示例中,它存在于 MainPage.xamlMainPage.cpp 中。

在 XAML 标记中创建 FlipViewDataTemplateScrollViewer 类的实例。然后,将该信息绑定到那些 XAML 标记项。下面是这些元素的 XAML 标记代码。

 
 
 
  1.  
  2.          
  3.              
  4.                  
  5.                      
  6.                          
  7.                      
  8.                     
  9.                         Source="{Binding Content}"  
  10.                         Width="{Binding ContentWidth}"  
  11.                         Height="{Binding ContentHeight}"  
  12.                         HorizontalAlignment="Left" 
  13.                         /> 
  14.                  
  15.              
  16.          
  17.      

此处的代码显示,背景和图像全都具有对应于 XML 源中数据的数据绑定。现在,你需要使用你具有的信息填充这些绑定。你将代码放置在相同名称的 cpp 文件中。下面是 MainPage.cpp 中帮助进行数据绑定的代码。

 
 
 
  1. void MainPage::DocumentLoaded(_In_ Document^ document) 
  2.     // Parse the document into an element tree. 
  3.     document->Parse(); 
  4.  
  5.     auto contentRoot = document->GetContentRoot(); 
  6.  
  7.     if (contentRoot != nullptr) 
  8.     { 
  9.         // Create a collection of content element to bind to the view 
  10.         auto items = ref new Platform::Collections::Vector(); 
  11.  
  12.         auto pageContent = contentRoot->GetFirstChild(); 
  13.  
  14.         while (pageContent != nullptr) 
  15.         { 
  16.             items->Append(ref new PageModel(pageContent, document)); 
  17.  
  18.             pageContent = pageContent->GetNextSibling(); 
  19.         } 
  20.  
  21.         FlipView->ItemsSource = items; 
  22.  
  23.         // Load the saved document state if any 
  24.         LoadState(ApplicationData::Current->LocalSettings->Values); 
  25.  
  26.         m_document = document; 
  27.     } 

此处的代码分析 XML 文档,然后创建内容元素的矢量,以便将其绑定到 UI 中的 XAML 元素。

自定义字体加载

要在此 DirectX 应用中使用自定义字体,你必须在 DirectWrite 中实现异步字体加载程序和集合。在 DirectX 应用中,如果你希望提供一个要在文本元素上使用的自定义字体,你需要使用 DirectWrite 获取未安装在用户系统上的新字体。在示例中,可在 FontFileStreamFontLoader 类中找到加载和提供这些新字体的代码。

FontFileStream 类是 IDWriteFontFileStream 接口的实现,它接受实际字体文件并将其加载到应用可以编入摘要的表单中。这涉及到读取文件片段、释放文件片段、获取文件大小及获取***编辑时间的处理方法。

下面是用于读取和释放字体文件片段的代码。

 
 
 
  1. HRESULT STDMETHODCALLTYPE FontFileStream::ReadFileFragment( 
  2.     _Outptr_result_bytebuffer_(fragmentSize) void const** fragmentStart, 
  3.     UINT64 fileOffset, 
  4.     UINT64 fragmentSize, 
  5.     _Out_ void** fragmentContext 
  6.     ) 
  7.     // The loader is responsible for doing a bounds check. 
  8.     if (    fileOffset <= m_data->Length 
  9.         &&  fragmentSize + fileOffset <= m_data->Length 
  10.         ) 
  11.     { 
  12.         *fragmentStart = m_data->Data + static_cast(fileOffset); 
  13.         *fragmentContext = nullptr; 
  14.         return S_OK; 
  15.     } 
  16.     else 
  17.     { 
  18.         *fragmentStart = nullptr; 
  19.         *fragmentContext = nullptr; 
  20.         return E_FAIL; 
  21.     } 
  22.  
  23. void STDMETHODCALLTYPE FontFileStream::ReleaseFileFragment( 
  24.     _In_ void* fragmentContext 
  25.     ) 

#p#

下面是用于检索文件大小和***编辑时间的代码。

 
 
 
  1. HRESULT STDMETHODCALLTYPE FontFileStream::GetFileSize( 
  2.     _Out_ UINT64* fileSize 
  3.     ) 
  4.     *fileSize = m_data->Length; 
  5.     return S_OK; 
  6.  
  7. HRESULT STDMETHODCALLTYPE FontFileStream::GetLastWriteTime( 
  8.     _Out_ UINT64* lastWriteTime 
  9.     ) 
  10.     // The concept of last write time does not apply to this loader. 
  11.     *lastWriteTime = 0; 
  12.     return E_NOTIMPL; 

在所有片段均已到位并且你可以访问字体文件中的信息后,你必须编写允许你从文件流中构造自定义字体集合和字体对象的代码。可以在 FontLoader.cpp 文件中找到此代码。

首先,你需要一个函数用于加载和枚举你要使用的字体。以下代码用于 LoadAsync 函数,该函数查找杂志示例中的字体目录,并枚举该目录中的 .ttf 字体文件列表。

 
 
 
  1. task FontLoader::LoadAsync() 
  2.     // Locate the "fonts" subfolder within the document folder 
  3.     return task([this]() 
  4.     { 
  5.         task(m_location->GetFolderAsync("fonts")).then([=](StorageFolder^ folder) 
  6.         { 
  7.             // Enumerate a list of .TTF files in the storage location 
  8.             auto filters = ref new Platform::Collections::Vector(); 
  9.             filters->Append(".ttf"); 
  10.  
  11.             auto queryOptions = ref new QueryOptions(CommonFileQuery::DefaultQuery, filters); 
  12.             auto queryResult = folder->CreateFileQueryWithOptions(queryOptions); 
  13.  
  14.             return queryResult->GetFilesAsync(); 
  15.  
  16.         }).then([=](IVectorView^ files) 
  17.         { 
  18.             m_fontFileCount = files->Size; 
  19.  
  20.             std::vector> tasks; 
  21.  
  22.             for (uint32 i = 0; i < m_fontFileCount; ++i) 
  23.             { 
  24.                 auto file = dynamic_cast(files->GetAt(i)); 
  25.  
  26.                 tasks.push_back(task(FileIO::ReadBufferAsync(file))); 
  27.             } 
  28.  
  29.             return when_all(tasks.begin(), tasks.end()); 
  30.  
  31.         }).then([=](std::vector buffers) 
  32.         { 
  33.             for each (IBuffer^ buffer in buffers) 
  34.             { 
  35.                 auto fileData = ref new Platform::Array(buffer->Length); 
  36.                 DataReader::FromBuffer(buffer)->ReadBytes(fileData); 
  37.  
  38.                 ComPtr fontFileStream(new FontFileStream(fileData)); 
  39.                 m_fontFileStreams.push_back(fontFileStream); 
  40.             } 
  41.  
  42.         }).wait(); 
  43.     }); 

现在,编写另一个方法,它采用在 DirectWrite 创建自定义字体集合时传入的字体集合项。此方法将采用此值并返回字体文件枚举器。

 
 
 
  1. HRESULT STDMETHODCALLTYPE FontLoader::CreateEnumeratorFromKey( 
  2.     _In_ IDWriteFactory* factory, 
  3.     _In_reads_bytes_(fontCollectionKeySize) void const* fontCollectionKey, 
  4.     uint32 fontCollectionKeySize, 
  5.     _Outptr_ IDWriteFontFileEnumerator** fontFileEnumerator 
  6.     ) 
  7.     *fontFileEnumerator = ComPtr(this).Detach(); 
  8.     return S_OK; 

此方法接受相同的集合项并返回字体文件流。

 
 
 
  1. HRESULT STDMETHODCALLTYPE FontLoader::CreateStreamFromKey( 
  2.     _In_reads_bytes_(fontFileReferenceKeySize) void const* fontFileReferenceKey, 
  3.     uint32 fontFileReferenceKeySize, 
  4.     _Outptr_ IDWriteFontFileStream** fontFileStream 
  5.     ) 
  6.     if (fontFileReferenceKeySize != sizeof(size_t)) 
  7.     { 
  8.         return E_INVALIDARG; 
  9.     } 
  10.  
  11.     size_t fontFileStreamIndex = *(static_cast(fontFileReferenceKey)); 
  12.  
  13.     *fontFileStream = ComPtr(m_fontFileStreams.at(fontFileStreamIndex).Get()).Detach(); 
  14.  
  15.     return S_OK; 

接下来的 2 个方法是帮助程序方法。 ***个函数移到文件流中的下一个字体文件,另一个函数用作当前字体文件的简单 getter。下面是这 2 种方法的代码。

 
 
 
  1. HRESULT STDMETHODCALLTYPE FontLoader::MoveNext(OUT BOOL* hasCurrentFile) 
  2.     *hasCurrentFile = FALSE; 
  3.  
  4.     if (m_fontFileStreamIndex < m_fontFileCount) 
  5.     { 
  6.         DX::ThrowIfFailed( 
  7.             m_dwriteFactory->CreateCustomFontFileReference( 
  8.                 &m_fontFileStreamIndex, 
  9.                 sizeof(size_t), 
  10.                 this, 
  11.                 &m_currentFontFile 
  12.                 ) 
  13.             ); 
  14.  
  15.         *hasCurrentFile = TRUE; 
  16.         ++m_fontFileStreamIndex; 
  17.     } 
  18.  
  19.     return S_OK; 
  20.  
  21. HRESULT STDMETHODCALLTYPE FontLoader::GetCurrentFontFile(OUT IDWriteFontFile** currentFontFile) 
  22.     *currentFontFile = ComPtr(m_currentFontFile.Get()).Detach(); 
  23.     return S_OK; 

我们的代码完成后,你现在已拥有一个异步处理应用的正常字体文件加载程序。这 2 个类一起用于枚举系统上的文件,将它们加载到字体文件流中,并使用你可以使用的这些字体创建一个自定义字体集合。

使用 VSIS 的***实践

当你希望执行 DirectX 和 XAML 互操作时,Windows 8 支持Surface Image SourceVirtual Surface Image Source,但是具体使用哪一个取决于你希望执行什么操作。

有 3 个主要方案,每一个都有一个 XAML 互操作选项。

  • 绘制元素(如纹理)作为应用中的位图。
  • 绘制大于屏幕的位图,因此需要进行平移。
  • 改进触摸操作的性能。

如果你需要管理静态图像或偶然更新 SIS 的内容,则Surface Image Source是一个好的选择。当 SIS 有用时的一个良好示例是,如果你希望将位图绘制为 XAML UI 元素。此元素可能需要进行更新以显示不同的信息,但是大部分是静态的。SIS 能很好地发挥作用,因为你的应用对 SIS 所做的更新与 XAML UI 线程同步,因此在它需要像应用中的 UI 一样高性能的方案中运行得***。

但是如果你需要一个需要直接操作(滚动或平移)的较大位图或 DirectX 内容区域,则使用 VSIS。如果你要显示的信息不能容纳到屏幕或元素上,则 VSIS 会很有用。一个使用 VSIS 的良好示例是具有滚动文本的阅读应用,或者需要平移和缩放以探索地图的地图应用。

如果这些方案中没有一个匹配你的应用使用情况,则 VSISSIS 可能不适合你的应用。尤其,如果性能对你的应用来说很重要,则 XAML 中的 SwapChainBackgroundPanel 元素可能***。有关详细信息,请参阅 Windows.UI::Xaml::Controls::SwapChainBackgroundPanel

原文链接:http://msdn.microsoft.com/zh-cn/library/windows/apps/jj552955.aspx

本文题目:操作实例:阅读器应用(DirectX和XAML)
文章URL:http://www.shufengxianlan.com/qtweb/news21/371921.html

网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等

广告

声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联