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

  1. 将它当成一个ImageSources使用(用一个image-Element来显示或者通过ImageBrush来显示)
  2. 使用它的pixels值(比如说将一个bitmap存储到硬盘或者与其他的app进行分享)

本节只是介绍一些基础的知识,在MSDN的相关文档里面有详尽的描述:

接下来我们来看一个简单的例子以及开始学习如何将一个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里面新的内容。