Friday, August 20, 2010

How to Render Bitmap or to Print a Visual in WPF

Working with WPF never become so easy if there was no inbuilt capability to take snaps or print a visual object. Microsoft made a very constructive class hierarchy to add functionality in each of the classes and hence building the solid foundation of WPF presentation model. Every element that we place in WPF is  inherited from a Visual. BCL added inbuilt functionality for a visual to render itself as Bitmapsource or to print the same Visual directly to the printer. In this post I will discuss how easily you could render a Visual as BitmapSource and later use it as source of an Image control.

Using Sample Application

RenderTargetBitmap is a class that is basically used to render a Visual to a bitmap object. Lets demonstrate this using an InkCanvas. You might already know, there is a special canvas element called InkCanvas which lets the user draw image on the screen. So before we proceed with the code lets discuss the sample application a bit.

In the above snap you can see that I have placed one InkCanvas which allows you to write dynamically on the screen. The buttons will Render the bitmap image from the Canvas and add the item on the ListBox on the Right. You can see after I click on Render as Bitmap button, it actually places the same visual as BitmapSource on the Right hand side ListBox.






Similar to what we worked on, I changed the content a bit and took another shot, and it exactly does the same.
Finally when I click on Render the Grid as Bitmap, it actually renders the whole Grid including the listbox and all as Grid. There are few buttons more, like Save Selection as JPEG / Print as Visual, each of them have its own functionality.

Download the Sample - 60KB


Using the Code



So, as you see the basic functionality, let me discuss the code.



<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>

So here I have placed one InkCanvas element and a ListBox with Few buttons. The XAML is quite straight forward, and when buttons are clicked, the listbox will update itself.

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;
}
I have used a model to use RenderTargetBitmap. Basically, The model will actually separate the logic from the presentation. I have created an ObservableCollection of ButmapSource objects. Now when Button1 is clicked, I use RenderTargetBitmap to render the Visual.

RenderTargetBitmap takes width, height, dpiX and dpiY as argument and renders the BitmapSource object from the Visual. rtb.Render(visual) gets the BitmapSource object, which I have added to the ImageCollection. As it is directly bound to the ListBox, the ListBox updates the Image instantly.

To Encode the BitmapSource to actual Image :

Rendering an image from a BitmapSource object is actually not a big deal. Let look at the code below :

public MemoryStream GenerateImage(Visual vsual, int widhth, 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, widhth, height);
    MemoryStream file = new MemoryStream();
    encoder.Frames.Add(BitmapFrame.Create(rtb));
    encoder.Save(file);

    return file;
}

Here I have used an enumerable which decides the format of the image. In the method, when I pass Jpeg, it will actually create an object of JpegBitmapEncoder which lets me to encode the BitmapSource into compressed JPEG format. encoder.Frames allows you to add a BitmapFrame which would later be rendered as Image. You can use JpegBitmapDecoder to decode jpeg image. Once I call encoder.Save, it saves the image into Stream.

private void btnRenderJPEG_Click(object sender, RoutedEventArgs e)
{
    MemoryStream memstream = this.DataModel.GenerateImage(inkCanvas, (int)inkCanvas.ActualWidth, (int)inkCanvas.ActualHeight, ImageFormat.JPG);

    if (memstream != null)
    {
        SaveFileDialog fdlg = new SaveFileDialog
        {
             DefaultExt="jpg",
             Title="Choose  filename and location",
             Filter="*Jpeg files|.jpg|Bmp Files|*.bmp|PNG Files|*.png|Tiff Files|*.tif|Gif Files|*.gif"
        };

        bool? result = fdlg.ShowDialog();

        if (result.HasValue && result.Value)
        {
            using (FileStream fstream = File.OpenWrite(fdlg.FileName))
            {
                memstream.WriteTo(fstream);
                fstream.Flush();
                fstream.Close();
            }
        }
    }
}

From the front end, I call the method, and store the stream into a file location.

Printing a Visual

Printing a Visual WPF is somewhat the easiest of the whole lot. Let you want to print the inkCanvas directly to the printer. In that case, you dont need to create a PrintDocument.  I will discuss how to print FlowDocument later. To print a visual, you need to use PrintDialog.

PrintDialog dlg = new PrintDialog();
dlg.PrintVisual(this.inkCanvas, "The picture is drawn dynamically");

Thus PrintDialog has a method that allows you to pass description and a visual, and eventually the command will print the visual from printer.

Try the sample application :
Download the Sample - 60KB


I hope this made you clear how to work with Visual. If you want to comment, feel free to do so.
Thanks for reading.

2 comments:

  1. Unfortunately the way you are using RTB will potentially "fall down" if the item you are taking the "screenshot" of has margins or isn't located at 0,0. If you add a margin (10,10,10,10) to the ink canvas, take an image then save it as a jpg you'll see that there's a black border left and top, and the bottom and right have been chopped off.

    You can work around this using a VisualBrush - there's an example of this, and an extension method to "screenshot" any UIElement on my blog at http://www.grumpydev.com/2009/01/03/taking-wpf-screenshots/

    ReplyDelete
  2. Hey,
    Yes, in my workplace where I wrote this I have actually used DrawingVisual to draw the BitmapImage. Yes the approach will not work when there is Margin and scrollbar present. I thought of giving an example of DrawingVisual to draw the image as well.
    Thank you so much for pointing this out. I am glad to see your input.
    Cheers.

    ReplyDelete

Please make sure that the question you ask is somehow related to the post you choose. Otherwise you post your general question in Forum section.

Author's new book

Abhishek authored one of the best selling book of .NET. It covers ASP.NET, WPF, Windows 8, Threading, Memory Management, Internals, Visual Studio, HTML5, JQuery and many more...
Grab it now !!!