RenderTargetBitmap 在windows store app 中的应用
前言
RenderTargetBitmap 是windows 8.1中一个新的类,它可以将任意的UIElement以bitmap的形式呈现。这篇博客的第一部分将介绍如何在windows 8.1中使用RenderTargetBitmap。第二部分将介绍在windows store app与WPF中RenderTargetBitmap大小调整方式的差异。UIElement以bitmap的形式呈现时,RenderTargetBitmap如何调整原来的UIElement大小对于你的项目来说是一个难题,但幸运的是这篇文章将会提出一个解决方案,使您可以在windows store app中得到与WPF相同的结果。让我们开始这篇博客的介绍吧。
怎么在windows 8.1中使用RenderTargetBitmap
在windows store app,你可以分以下两种情况来使用RenderTargetBitmap
- 将它当成一个ImageSources使用(用一个image-Element来显示或者通过ImageBrush来显示)
- 使用它的pixels值(比如说将一个bitmap存储到硬盘或者与其他的app进行分享)
本节只是介绍一些基础的知识,在MSDN的相关文档里面有详尽的描述:
- RenderTargetBitmap-class (这个网站有关于RenderTargetBitmap的详细描述 )
- XAML render to bitmap sample (一个很好的例子,可以学习如何通过ImageSources和pixels来使用RenderTargetBitmap)
接下来我们来看一个简单的例子以及开始学习如何将一个UIElement以bitmap的方式呈现。
UIElement的呈现
在接下来的章节中,我们将通过下面的代码来定义一个Grid并将它作为RenderTargetBitmap的输入.在这个Grid中包含一个image和TextBlock控件。由于这个Grid没有定义任何的行或列,所以TextBlock的内容呈现在图片的上方。
<Grid x:Name="elementToRender" Width="500" Height="500">
<Image Source="Assets/image.jpg" Stretch="UniformToFill"/>
<TextBlock Text="This text is on top of the image" FontSize="40" TextWrapping="Wrap" Margin="50"/>
</Grid>
上段代码的结果如下:
需要注意的是在上面代码中,将Grid的名称定义为elementToRender,我们接下来可以在C#代码中通过该名称来访问Grid。现在我们来看看如何使用该Grid来定义一个RenderTargetBitmap。
用UIElement来定义一个RenderTargetBitmap
你可以通过调用RenderAsync这个方法来定义一个RenderTargetBitmap,该方法的输入参数是一个UIElement,下面的代码是通过将名为elementToRender的Grid来作为该方法的输入参数从而来定义一个RenderTargetBitmap对象:
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
通过上述两行代码你可以将UIElement以RenderTargetBitmap的方式呈现。现在你可以像前面章节提到的那样可以通过Imagesource的方法使用RenderTargetBitmap或者可以对其Pixel属性进行访问。
以Imagesource的方式来使用RenderTargetBitmap
由于RenderTargetBitmap类继承自ImageSource,所以您可以直接将其赋值到您定义的任何一个Image-Element的Source-Property上。假设你已经定义了这样一个Image-Element:
<Image x:Name="image" Width="200" Height="200".../>
现在你可以直接将该Image-Element的Source-Property赋值为一个RenderTargetBitmap对象,可以参考以下代码:
private async void ButtonRender_Click(object sender, RoutedEventArgs e)
{
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
image.Source = bitmap;
}
使用RenderTargetBitmap的Pixels值 :
如果你想访问RenderTargetBitmap的Pixels数据,你需要在用RenderAsync这个方法将UIElement定义为RenderTargetBitmap后,再调用RenderTargetBitmap的GetPixelsAsync方法来获得其Pixels数据。该方法返回的是一个IBuffer类型,里面存储的是二进制的位图数。这个IBuffer可以转换为一个Byte数组,数组里面的数据是以BGRA8格式存储的。
以下代码示例如何从一个RenderTargetBitmap对象中获得以byte数组类型存储的像素数。需要特别注意的是IBuffer实例调用的ToArray方法是一个扩展方法,你需要在您的项目中加入System.Runtime.InteropServices.WindowsRuntime这个命名空间。
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
// Get the pixels
IBuffer pixelBuffer = await bitmap.GetPixelsAsync();
byte[] pixels = pixelBuffer.ToArray();
通过获取的Pixels数据,你可以将您的 RenderTargetBitmap里的内容保存到硬盘,你也可以通过Pixels数据来创建一个WriteableBitmap对象,然后对该对象的内容做某些修改,或者您可以通过share-contract来将该bitmap对象分享给其他的app。下面的代码示例如何通过share-contract来分享bitmap对象。其中在DataRequested事件处理程序中先是获得Pixels数据,然后将其转化为InMemoryRandomAccessStream来向其他的app进行数据分享:
public sealed partial class MainPage : Page
{
...
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
DataTransferManager.GetForCurrentView().DataRequested+= MainPage_DataRequested;
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
DataTransferManager.GetForCurrentView().DataRequested -= MainPage_DataRequested;
}
private async void MainPage_DataRequested(DataTransferManager sender, DataRequestedEventArgs args)
{
var deferral = args.Request.GetDeferral();
var bitmap = new RenderTargetBitmap();
await bitmap.RenderAsync(elementToRender);
// 1. Get the pixels
IBuffer pixelBuffer = await bitmap.GetPixelsAsync();
byte[] pixels = pixelBuffer.ToArray();
// 2. Write the pixels to aInMemoryRandomAccessStream
var stream = new InMemoryRandomAccessStream();
var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, stream);
encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96, 96,
pixels);
await encoder.FlushAsync();
stream.Seek(0);
// 3. Share it
args.Request.Data.SetBitmap(RandomAccessStreamReference.CreateFromStream(stream));
args.Request.Data.Properties.Description = "RenderTargetBitmap-Sample";
args.Request.Data.Properties.Title = "Wow, here the shared Bitmap :-)";
deferral.Complete();
}
...
}
需要注意的一点是,调用GetPixelsAsync这个方法需要额外的开销,因为pixels数据是从显存里获取的。如果你仅仅是调用RenderAsync这个方法,并将RenderTargetBitmap赋值为ImageSource,那么pixels数据还是在显存中且并没有从显存中获得。
RenderTargetBitmap的局限:
如前所述,通过使用 RenderTargetBitmap,你可以将任意的UIElement以bitmap的形式呈现,但并不完全是这样,它还有一些限制。比如说RenderTargetBitmap 不能捕获MediaElement所显示的视频内容, 并且有些存在于可视化树中但不在屏幕内的UIElment你也无法捕获。你可以在MSDN的文档中看到详细的关于RenderTargetBitmap局限性的描述:https://msdn.microsoft.com/en-us/library/windows/apps/windows.ui.xaml.media.imaging.rendertargetbitmap.aspx
大小调整的问题以及解决方案:
假如你是WPF的开发者,当你在windows store app中使用RenderTargetBitmap 时,将会发现一些意想不到的问题。本部分会首先提出这些问题然后再提出相应的解决方案。
问题描述
当一个Elememt包含跨越其边界的子元素时,在windows store app中,当该Element转化为RenderTargetBitmap显示时,该RenderTargetBitmap对象的大小会比原来的Element要大。而在WPF中,该RenderTargetBitmap对象大小不会变,但是子元素会被剪裁。下面的示例说明了这个问题。
该例子显示的是一个Grid Element, 通过对子元素transformed属性的定义可以使子元素跨越Grid的边界:
<Grid x:Name="elementToRender" Height="100" Width="500" Background="Red">
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
输出结果如下,你可以看到黄色的矩形框超出了Grid的边界:
在windows store app中,当我们将Grid以RenderTargetBitmap的形式呈现时,该RenderTargetBitmap对象的大小是500×150,而原始的Grid大小是500×100,下图显示的是该RenderTargetBitmap的大小,其中灰色部分是超出原始Grid的范围,即使我们在转换的时候强制设置RenderTargetBitmap的大小为500×100,其结果还是不变,如下所示:
在WPF中,该RenderTargetBitmap的大小为500×100,与Grid大小一致,但是Grid里面的子元素被剪裁了,其结果如下:
对使用Clip-property这个解决办法的探索
为了在windows store app得到WPF一样的结果,我的第一想法是使用Clip-property来剪裁原来的Grid,代码如下:
<Grid x:Name="elementToRender" Height="100" Width="500" Background="Red">
<Grid.Clip>
<RectangleGeometry Rect="0 0 500 100"/>
</Grid.Clip>
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
这段代码得到的结果如下,黄色的子元素部分被剪裁,这与我想要显示的RenderTargetBitmap对象的样式是一致的:
但是你将上面剪裁过后的Grid以RenderTargetBitmap对象显示的时候,结果并不是我们所期盼的500 x 100的大小,它的大小仍然是500 x 150。其结果如下图所示,其中超出Grid的大小部分被标记为灰色:
这意味着RenderTargetBitmap所显示的大小是原总的UI元素所占空间的大小(这其中包括黄色的矩形框超出Grid边界的大小),所以不管我们有没有对原来的Grid有没有进行剪裁,RenderTargetBitmap的大小总是500 x 150而不是我们所期盼的500 x 100。
最终有效的解决方案
综上所讨论的,使用Clip-property并不是一个有效的解决方案。这个RenderTargetBitmap的大小仍然超过原来Grid的大小。但是使用Clip-property是最终有效解决方案的第一步。第二步时你需要定义一个新的Grid Element,该Grid在可视化树中,且该Grid的子元素包含第一步剪裁后的Grid。你只需要将该Grid以RenderTargetBitmap的形式呈现就会得到我们想要的结果。
在前一部分,我们已经得到了剪裁后的Grid:
<Grid x:Name="elementToRender" Height="100" Width="500" Background="Red">
<Grid.Clip>
<RectangleGeometry Rect="0 0 500 100"/>
</Grid.Clip>
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
第二步我们在可视化树种加入一个新的Grid元素,该Grid子元素包含上述剪裁后的Grid,代码如下:
<Grid x:Name="elementToRender" Height="100" Width="500">
<Grid x:Name="oldElementToRender" Height="100" Width="500" Background="Red">
<Grid.Clip>
<RectangleGeometry Rect="0 0 500 100"/>
</Grid.Clip>
<Rectangle Fill="Yellow" Width="100" Height="100">
<Rectangle.RenderTransform>
<TranslateTransform Y="50"/>
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</Grid>
如果你将新加入的Grid以RenderTargetBitmap的形式呈现,你会发现该RenderTargetBitmap大小是我们所期盼的500x100,而不再是500x150.这样我们就会得到与WPF相同的结果。该RenderTargetBitmap最后的结果如下:
最后希望该博客能对大家在使用RenderTargetBitmap时有更多的帮助,同时希望大家能更多的用到windows 8.1里面新的内容。