在Windows Presentation Foundation (WPF)中,经常需要将用户界面元素(如控件和布局)渲染为图像文件,或者直接打印到打印机上。Microsoft为WPF的表示模型构建了一个非常结构化的类层次结构,这为添加功能提供了坚实的基础。在WPF中,放置的每个元素都继承自Visual类,而基础类库(BCL)为Visual添加了内置功能,使其能够渲染为BitmapSource或直接打印到打印机。本文将讨论如何轻松地将Visual渲染为BitmapSource,并将其用作Image控件的源。
RenderTargetBitmap类主要用于将Visual渲染为位图对象。使用InkCanvas来演示这个过程。InkCanvas是一个特殊的画布元素,允许用户在屏幕上绘制图像。在继续代码之前,让先讨论一下示例应用程序。
在上图中,可以看到放置了一个InkCanvas,它允许在屏幕上动态地书写。按钮将从Canvas渲染位图图像,并将其添加到右侧的ListBox中。可以看到,当点击“Render as Bitmap”按钮后,它实际上将相同的视觉元素作为BitmapSource放置在右侧的ListBox中。
类似于之前所做的,更改了内容并再次拍摄,它确实做了同样的事情。最后,当点击“Render the Grid as Bitmap”时,它实际上渲染了整个Grid,包括ListBox和所有内容。还有一些其他按钮,如“Save Selection as JPEG / Print as Visual”,每个按钮都有自己的功能。
现在,已经看到了基本功能,让讨论代码。
<InkCanvas EditingMode="Ink" Grid.Row="1" x:Name="inkCanvas" Background="AliceBlue" />
<ListBox x:Name="lstImages" Grid.Row="1" Grid.Column="1" ItemsSource="{Binding ImageCollection}">
<ListBox.ItemTemplate>
<DataTemplate>
<Image Source="{Binding}" Stretch="UniformToFill" MaxWidth="150" MaxHeight="150" Margin="10" />
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" Width="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ScrollContentPresenter}}, Path=ActualWidth}" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Orientation="Horizontal">
<Button x:Name="btnRenderBitmap" Content="Render as BitMap" Click="btnRenderBitmap_Click" />
<Button x:Name="btnRenderwhole" Content="Render the Grid as Bitmap" Click="btnRenderwhole_Click" />
<Button x:Name="btnRenderJPEG" Content="Save Selecteditem as Jpeg" Click="btnRenderJPEG_Click" />
<Button x:Name="btnPrintVisual" Content="Print the Visual" Click="btnPrintVisual_Click" />
</StackPanel>
在这里,放置了一个InkCanvas元素和一个ListBox,以及一些按钮。XAML非常直接,当按钮被点击时,ListBox将更新自身。
private ObservableCollection<BitmapSource> imagecollection;
public ObservableCollection<BitmapSource> ImageCollection
get
{
this.imagecollection = this.imagecollection ?? new ObservableCollection<BitmapSource>();
return this.imagecollection;
}
public RenderTargetBitmap RenderVisaulToBitmap(Visual vsual, int width, int height)
{
RenderTargetBitmap rtb = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Default);
rtb.Render(vsual);
BitmapSource bsource = rtb;
this.ImageCollection.Add(bsource);
return rtb;
}
使用了一个模型来使用RenderTargetBitmap。基本上,模型将逻辑与表示分离。创建了一个BitmapSource对象的ObservableCollection。现在当Button1被点击时,使用RenderTargetBitmap来渲染Visual。RenderTargetBitmap接受width、height、dpiX和dpiY作为参数,并从Visual渲染BitmapSource对象。rtb.Render(visual)获取BitmapSource对象,已经将其添加到ImageCollection中。由于它直接绑定到ListBox,ListBox会立即更新图像。
从BitmapSource对象渲染图像实际上并不困难。让看一下下面的代码:
public MemoryStream GenerateImage(Visual vsual, int width, int height, ImageFormat format)
{
BitmapEncoder encoder = null;
switch (format)
{
case ImageFormat.JPG:
encoder = new JpegBitmapEncoder();
break;
case ImageFormat.PNG:
encoder = new PngBitmapEncoder();
break;
case ImageFormat.BMP:
encoder = new BmpBitmapEncoder();
break;
case ImageFormat.GIF:
encoder = new GifBitmapEncoder();
break;
case ImageFormat.TIF:
encoder = new TiffBitmapEncoder();
break;
}
if (encoder == null)
return null;
RenderTargetBitmap rtb = this.RenderVisaulToBitmap(vsual, width, height);
MemoryStream file = new MemoryStream();
encoder.Frames.Add(BitmapFrame.Create(rtb));
encoder.Save(file);
return file;
}
在这里,使用了一个枚举来决定图像的格式。在方法中,当传递JPEG时,它实际上会创建一个JpegBitmapEncoder对象,让将BitmapSource编码为压缩的JPEG格式。encoder.Frames允许添加一个BitmapFrame,稍后将被渲染为Image。可以使用JpegBitmapDecoder来解码JPEG图像。一旦调用encoder.Save,它将图像保存到Stream中。
在WPF中打印Visual是整个过程中最简单的,如果想直接将inkCanvas打印到打印机上,那么不需要创建PrintDocument。稍后将讨论如何打印FlowDocument。要打印Visual,需要使用PrintDialog。
PrintDialog dlg = new PrintDialog();
dlg.PrintVisual(this.inkCanvas, "The picture is drawn dynamically");