Click here to Skip to main content
14,926,216 members
Articles / Desktop Programming / WPF
Article
Posted 28 Aug 2016

Stats

11.1K views
415 downloads
2 bookmarked

WPF/MVVM Print or Create an Image of your Application Screens

Rate me:
Please Sign up or sign in to vote.
5.00/5 (3 votes)
28 Aug 2016CPOL15 min read
This article will show an easy way to add the ability to print, or save as an image, each screen (Window) or FrameworkElement (UserControl) in your application.

Sample Printed Output of Application Screen

Image 1

Introduction

I was working on a project and found myself trying to replicate the data on the screen into a report for the user. After giving it some thought, I decided to look into printing the screen information. I wanted to produce a professional looking report, not just a simple screen print or snip-it image that the user had to manipulate.

The following code shows an easy way to add printing, along with a title or description on top of the page. It also allows the program to capture your program screen as an image that can be used in documentation, emails, troubleshooting, help files or even to use it as a tooltip!

Sample Screen Image with Screen Image Tooltip

Image 2

Code Construction

Image 3

The code is comprised of 2 main classes (and 2 corresponding interface classes). One to create an image file of your screen and another to create the printout of the screen.

Each of the following topics:

  1. Save screen (FrameworkElement) to an image file
  2. Print screen (FrameworkElement) as a report

is explained below.

This code could be compiled to a class library and added to your project or it could be added to your MVVM framework so you can add this functionality to all your MVVM projects. After explaining the code in detail, I will use both in a small MVVM application to show how to implement it into your programs.

1. Save Screen to an Image File

Application Screen with Menu Options to Print or Create Page Image

Image 4

The code to create an image file of the "screen"/FrameworkElement starts with an Enum of the various image types that are available to save the image as. The 5 available image types are listed below.

ImageType Enums
VB.NET
Private Enum GWSImageType
  BMPImage
  GIFImage
  JPGImage
  PNGImage
  TIFImage
End Enum

The Enums are used behind the scenes to differentiate each method call. BMPImage will create a ". bmp" bitmap file. JPGImage will create a ".jpg" JPEG graphics file, etc...

Each method call uses the same signature (a framework element to create the image from, an optional double value used to scale the size of the image).

A default scale value of "1" means to create the image as the actual size with no scaling. A value of "2" would create an image twice as large and a value of ".5" would create the image half the size.

I created an interface for the public methods in order to use a service provider to locate the available methods, more on that later (MVVM stuff).

The function call to create the BitmapSource file was also made public so that this function could be used to obtain a BitmapSource of the screen/FrameworkElement. You could then use this BitmapSource in an image control to display it. An example of using this function is shown later on when it is used to create an image of the screen and then uses that image in a tooltip. Sample of this screen appears above.

SaveImage Interface
VB.NET
Public Interface IGWSSaveImage
   Sub CreateImageFileBMP(ByVal objControl As FrameworkElement, Optional ByVal Scale As Double = 1)
   Sub CreateImageFileGIF(ByVal objControl As FrameworkElement, Optional ByVal Scale As Double = 1)
   ...

   Function CreateBitmapSource(ByVal source As FrameworkElement, _
                               Optional ByVal scale As Double = 1) As BitmapSource
End Interface

The code for each image type is the same except for the encoder that is used to save the bitmap image and of course the filename extension. Each method calls the same procedures with "XXX" replaced with the image type.

CreateImageFileXXX
VB.NET
Public Sub CreateImageFileXXX(ByVal source As FrameworkElement, Optional ByVal scale As Double = 1)
   Dim _bm As BitmapSource = CreateBitmapSource(source, scale)
   Dim _bmArray As Byte() = CreateImageByteArray(_bm, GWSImageType.XXXImage)
   SaveImageFile(_bmArray, GWSImageType.XXXImage)
End Sub

The three steps in the method above perform the following functions:

  1. Turn FrameworkElement into a BitmapSource
  2. Convert the BitmapSource into an encoded byte array
  3. Save the byte array to a file

1. Turn FrameworkElement into a BitmapSource

The basic code to turn a framework element into a bitmap source is right from Microsoft documentation. I added the scaling to obtain a smaller or larger image.

UIElement.Rendersize.Width is the same as FrameworkElement.ActualWidth (and also height), so if you wanted to use a UIElement as your source object, the code would still work. ActualWidth and ActualHeight are only found on FrameworkElements and not UIElements.

The code uses a VisualBrush to "paint" the source into a DrawingContext Rectangle.

CreateBitmapSource Function
VB.NET
Public Function CreateBitmapSource(ByVal source As FrameworkElement, _
Optional ByVal scale As Double = 1) As BitmapSource Implements IGWSSaveImage.CreateBitmapSource
  Dim actualWidth As Double = _
  source.RenderSize.Width   'same as source.actualwidth - rendersize also works with UIElements
  Dim actualHeight As Double = _
  source.RenderSize.Heigt 'same as source.actualHeight- rendersize also works with UIElements
  
  Dim renderWidth As Double = actualWidth * scale
  Dim renderHeight As Double = actualHeight * scale

  Dim renderTarget As New RenderTargetBitmap(CInt(renderWidth), _
  CInt(renderHeight), 96, 96, PixelFormats.[Default])
  Dim sourceBrush As New VisualBrush(source)
  Dim drawingVisual As New System.Windows.Media.DrawingVisual()
  Dim drawingContext As DrawingContext = drawingVisual.RenderOpen()

  Using drawingContext
     drawingContext.PushTransform(New ScaleTransform(scale, scale))
     drawingContext.DrawRectangle(sourceBrush, Nothing, _
     New Rect(New Point(0, 0), New Point(actualWidth, actualHeight)))
  End Using
  renderTarget.Render(drawingVisual)

  Return renderTarget

End Function

2. Convert the BitmapSource into an Encoded Byte Array

Once we have our BitmapSource object, we can then convert it into a byte array using the proper encoder based on the file type we wish to receive.

A select case statement chooses the proper encoder based on the ImageType parameter of the function call. Once we have the proper encoder, we can create the byte array file.

CreateImageByteArray Function
VB.NET
Private Shared Function CreateImageByteArray_
(ByRef source As BitmapSource, ByVal imageType As GWSImageType) As Byte()
  Dim _imageArray As Byte() = Nothing
  Dim ImageEncoder As BitmapEncoder = Nothing

  Select Case imageType
    Case GWSImageType.BMPImage
      ImageEncoder = New BmpBitmapEncoder()

...


  ImageEncoder.Frames.Add(BitmapFrame.Create(source))
  Using outputStream As New MemoryStream()
     ImageEncoder.Save(outputStream)
     _imageArray = outputStream.ToArray()
  End Using

  Return _imageArray

End Function

3. Save the byte array to a file

Now that we have our byte array encoded for the file type we desire, the next step is to save the file to disk. The code below uses a SaveFileDialog to get a path & filename to save the file. The SaveFileDialog would usually be part of your framework helper methods, but I included it here in this class for completeness.

Save Image File Stream
VB.NET
Private Shared Sub SaveImageFile(ByVal byteData As Byte(), ByRef imageType As GWSImageType)
   Dim fileName As String
   fileName = GetSaveImageDialog(imageType) 
   If String.IsNullOrEmpty(fileName) OrElse fileName.Trim() = String.Empty Then
   Else
      Try
         Dim oFileStream As System.IO.FileStream
         oFileStream = New System.IO.FileStream(fileName, System.IO.FileMode.Create)
         oFileStream.Write(byteData, 0, byteData.Length)
         oFileStream.Close()
         MessageBox.Show("Window Image Saved. " & fileName, "Save Data", _
                          MessageBoxButton.OK, MessageBoxImage.Information)
      Catch ex As Exception
          MessageBox.Show("Error Saving Image to disk.", "Disk Error", _
                           MessageBoxButton.OK, MessageBoxImage.Stop)
      End Try
   End If
End Sub      

We now have an image of our screen saved to a file of the image type that we selected. Further below, we will demonstrate using this code in a MVVM type application.

2. Print Screen as a Report

Application Screen with Menu Options to Print Page

Image 5

The code to create a report of our screen is similar to the code to create the screen image. The print code takes an image of the screen as a visual brush (like we did above) and puts it in a Viewbox, that is then manipulated and printed. The code allows you to select a title for the report as well as setting print options such as paper size, orientation, font size and the ability to stretch the report size.

Once again, I created an Interface for access to this class in order to use a service provider to locate the available methods, again more on that later (MVVM stuff).

In addition, the PrintControl class contains public properties that can be set to format your printed report. Here is the interface code and the properties that can be set.

PrintControl Interface
VB.NET
Public Interface IGWSPrintControl
        Sub PrintUIElement(ByVal objPrint As UIElement)

        Property PrintDescription As String
        Property PrintTitle As String
        Property PrintMargin As Double
        Property PrintFontSize As Double
        Property PrintFontWeight As FontWeight
        Property PrintForeground As Media.Color
        Property PrintStretch As Windows.Media.Stretch
    End Interface

The properties are self explanatory as to their purpose. The PrintControl class starts with setting default values for all of the public properties. The PrintDescription is the description of the report that appears in the print queue when you print it.

The PrintTitle is the title that prints as part of the report. It is initially set to an empty string which means no title will appear. The font size, font weight and media color only apply to the print title while the print margin and media stretch apply to the whole report.

Default Public Property Values
VB.NET
Private _printDescription As String = "GWS Framework Print"
Private _printTitle As String = String.Empty
Private _printMargin As Double = 100
Private _printFontSize As Double = 36
Private _printFontWeight As FontWeight = FontWeights.Bold
Private _printForeground As Media.Color = Colors.Maroon
Private _printStretch As Windows.Media.Stretch = Stretch.Uniform

In the main method call of the Print Control class, I will pass in a UIElement instead of a Framework Element to show that either can be used.

The main method first declares a PrintDialog which will pop up the print dialog window and allow you to make a printer selection, paper size, orientation, etc. Once selected, the PrintDialog will contain the properties we need to set the properties for our report.

Here is an outline of the steps to print the report:

  1. Create a PrintDialog object.
  2. Create a main grid and set its size to the PrintableArea.
  3. Add a Viewbox to the main grid and set its alignment.
  4. Add a new grid to the Viewbox with 2 rows.
  5. Add your screen as a VisualBrush to the bottom row.
  6. Add your title text to the top row and set its properties.
  7. Print the main grid.

First, you can see that a PrintDialog object is declared and PrtDialog.ShowDialog will pop up the dialog for the user. If the user selects the print button, ShowDialog will return true and the PrintDialog object will be populated with their selections. We can obtain the PrintableAreaWidth and PrintableAreaHeight from the PrintDialog and assign it to the size of our main grid (PageGrid).

Next, we create a Viewbox and add it to our grid. The key to the Viewbox is the stretch setting which will allow you to change the size of the screen report. Further below in the MVVM sample program, I will add a menu item where the user can change the stretch parameter before printing.

CreateUIElement Setup and Parameters
VB.NET
Private Sub CreateUIElement(ByVal objPrint As UIElement)
   Dim PrtDialog As New PrintDialog

   If PrtDialog.ShowDialog = True Then
      Dim PageGrid As New Grid
      PageGrid.Width = PrtDialog.PrintableAreaWidth
      PageGrid.Height = PrtDialog.PrintableAreaHeight

      Dim PageViewBox As New Viewbox
      PageGrid.Children.Add(PageViewBox)
         With PageViewBox
           .HorizontalAlignment = Windows.HorizontalAlignment.Center
           .VerticalAlignment = Windows.VerticalAlignment.Center
           .Margin = New Thickness(Me.PrintMargin)
           .Stretch = Me.PrintStretch
         End With
 ...

We create a rectangle and set it to the size of our screen object. We are going to put this rectangle into a Viewbox so it will be adjusted if needed. A grid is created with 2 rows. The bottom row is set a height of "*" so it will take all the room it needs to display the VisualBrush image.

Create Grid and Assign VisualBrush
VB.NET
...
Dim PrintRectangle As New Rectangle
PrintRectangle.Height = objPrint.RenderSize.Width
PrintRectangle.Width = objPrint.RenderSize.Width

Dim PrintGrid As New Grid
Dim PrintRow1 As New RowDefinition
Dim PrintRow2 As New RowDefinition
PrintRow1.Height = GridLength.Auto
PrintRow2.Height = New GridLength(1.0, GridUnitType.Star)
PrintGrid.RowDefinitions.Add(PrintRow1)
PrintGrid.RowDefinitions.Add(PrintRow2)

Dim PrintVisualBrush As New VisualBrush(objPrint)
PrintRectangle.Fill = PrintVisualBrush
 ...

We create a TextBlock to hold our report title and set its properties from the public properties for this class. You can easily change the values of each property before you print the report. This will also be demonstrated in the sample MVVM app to follow.

Create Title Text
VB.NET
...
Dim PrintTextBlock As New TextBlock
With PrintTextBlock
   .Text = Me.PrintTitle
   .FontSize = Me.PrintFontSize
   .FontWeight = Me.PrintFontWeight
   .Foreground = New SolidColorBrush(Me.PrintForeground)
   .VerticalAlignment = VerticalAlignment.Top
   .HorizontalAlignment = HorizontalAlignment.Center
End With

Grid.SetRow(PrintTextBlock, 0)
Grid.SetRow(PrintRectangle, 1)
PrintGrid.Children.Add(PrintRectangle)

If Me.PrintTitle <> String.Empty Then
   PrintGrid.Children.Add(PrintTextBlock)
End If

PageViewBox.Child = PrintGrid
 ...

Measure and Arrange must be called on our grid or the report will print out a blank page. Once that has occurred, we just need to send our grid to the PrintDialog.PrintVisual method. This is where we use the print description for the report.

Measure and Arrange and Print
VB.NET
...
PageGrid.Measure(New Size(Double.PositiveInfinity, Double.PositiveInfinity))
PageGrid.Arrange(New Rect(0, 0, PrtDialog.PrintableAreaWidth, PrtDialog.PrintableAreaHeight))

Try
   PrtDialog.PrintVisual(PageGrid, Me.PrintDescription)
Catch ex As Exception
   Throw ex
End Try
...

There you have it. You can now print or create an image of your screens.

To make this article complete, I will now incorporate this code into a sample MVVM project to illustrate one way to implement this code.

MVVM Sample Project

Sample WPF/MVVM Screen

Image 6

Overview

In order to see the code in action, I created a small MVVM style project to show how to implement both utilities into your programs. This is not an explanation of WPF or MVVM. I will just explain the code that relates to implementing the screen printing/image capture. The full source code is included in the attached download files if you would like to look at all the functionality.

As you can see in the image above, the print menu will let the user select the stretch setting before printing. You could also add a menu option to allow the user to enter a report title. I used the ViewModel Switching pattern to navigate screens in the program. I explain the ViewModel Switching in detail in my previous article here on Code Project.

Laying the Groundwork

This is the same simple MVVM framework as shown in my previous article. It is repeated it here for completeness and to acknowledge the people who wrote the original code.

First - The Plumbing

If you are creating any kind of WPF application (an enterprise wide solution to a small standalone app), it makes sense to use the Model-View-ViewModel pattern. This is not an article to explain MVVM, there are plenty of them on Code Project that you can reference. When using MVVM, it pays to use a framework for the program infrastructure. There are also numerous frameworks available to choose from.

I have created a simple MVVM framework module to provide the basic "plumbing" for the application. The framework contains the following items:

  • RelayCommand.vb
  • Messenger.vb
  • ServiceContainer.vb
  • IMessageBoxService.vb
  • MessageBoxService.vb
  • ServiceInjector.vb
  • IScreen.vb
  • BaseViewModel.vb
  • IShowDialogService.vb
  • ShowDialogService.vb

The relay-command, messenger, IScreen and service provider are from articles from Josh Smith and the BaseViewModel class complete with INotifyPropertyChanged logic is from Karl Shifflett's articles, all here on Code Project. All the code for this framework is included in the project source code download file.

Bringing It All Together

In a few easy steps, Printing and ImageSave is added to all screens in my program. Since I use ViewModel switching in all my WPF/MVVM programs, my main window contains a content control to display the current ViewModel. First, all I need to do is add a name to the ContentControl (x:name = "MyView") so I can pass it to the Print and SaveImage methods.

MainWindow XAML ContentControl
XML
 <ContentControl Content="{Binding Path=CurrentPageViewModel}"
                        Grid.Row="1"
                        x:Name="MyView"
                        HorizontalAlignment="Stretch"
                        VerticalAlignment="Stretch" />

Then, I create two menu items on the MainWindow. I link each menu item to a command and pass the ContentControl as the command parameter.

MainWindow Xaml Menu Items
XML
<MenuItem Header="_Print Page"
 	Command="{Binding Path=PrintCommand}"
        CommandParameter="{Binding ElementName=MyView}">
 ...
<MenuItem Header="_Create Page Image"
	Command="{Binding Path=SaveImageCommand}"
	CommandParameter="{Binding ElementName=MyView}">

Then in the application file, I use the ServiceContainer from my MVVM Framework and set up a reference to each method. This is why I created an interface for both classes as explained in the details above.

Application.Xaml.VB File
VB.NET
Public Sub New()
	...
        ServiceContainer.Instance.AddService(Of IGWSPrintControl)(New GWSPrintControl)
        ServiceContainer.Instance.AddService(Of IGWSSaveImage)(New GWSSaveImage)
	...
End Sub

Then each menu item will call the ICommand on the ViewModel. I put this code in the DisplayViewModel that is inherited by each ViewModel. Each ICommand uses the standard RelayCommand from Josh Smith's articles on Code Project.

Each command uses the MVVM Framework ServiceContainer to find the proper method. Each method is passed the ContentControl as a parameter and is printed or saved using the default properties.

DisplayViewModel ICommands
VB.NET
Public Overridable ReadOnly Property PrintCommand() As ICommand
        Get
            Return New RelayCommand(Of UIElement)(AddressOf Me.Print)
        End Get
End Property
Protected Overridable Sub Print(ByVal obj As UIElement)
        Dim GWSPrintControl = MyBase.GetService(Of IGWSPrintControl)()
        GWSPrintControl.PrintUIElement(obj)
End Sub

Public Overridable ReadOnly Property SaveImageCommand() As ICommand
        Get
            Return New RelayCommand(Of FrameworkElement)(AddressOf Me.ImageExecute)
 End Get
End Property
Protected Overridable Sub ImageExecute(ByVal obj As FrameworkElement)
        Dim GWSSaveImage = MyBase.GetService(Of IGWSSaveImage)()
	GWSSaveImage.CreateImageFileJPG(obj)
End Sub

That's it! The program now has the functionality to Print or SaveImage of each screen in the program.

Image 7Now for a Few Bells and WhistlesImage 8

Let's change the title, margin and stretch on the BoxGridViewModel Screen report.

For each ViewModel, you can override the ICommand and set the properties for that ViewModel. This will let you change the description of each screen and change the font size, color and margins.

Since each ICommand on the DisplayViewModel is marked as Overridable, we can simply Override the ICommand in the CurrentPageViewModel. Then, we create the PrintControl object and set any properties we wish for this ViewModel.

BoxGridViewModel with Overrides
VB.NET
Public Overrides ReadOnly Property PrintCommand() As ICommand
        Get
            Return New RelayCommand(Of UIElement)(AddressOf Me.Print)
        End Get
End Property

Protected Overrides Sub Print(ByVal obj As UIElement)
        Dim GWSPrintControl = MyBase.GetService(Of IGWSPrintControl)()

        With GWSPrintControl
            .PrintDescription = "Print Demo Description"
            .PrintTitle = My.Application.Info.Description
            .PrintStretch = Stretch.Fill
            .PrintMargin = 50
        End With

        GWSPrintControl.PrintUIElement(obj)
End Sub

Let's change the SaveImage file type and hide the 4 buttons on the edit screen.

Some information on the screen you may not wish to show on the screen image or printout. You can change the screen anyway you like before creating the image and then restore it.

First, we declare the messenger from the MVVM Framework in our application file and set up 2 constant strings to be used as our messages.

Application.Xaml.vb Declarations
VB.NET
Shared ReadOnly _messenger As New GwsMvvmFramework.Messenger
...

Friend Const MSG_CLEANUP_COMMAND As String = "Clean up"
Friend Const MSG_CLEANUP_RESET_COMMAND As String = "Clean up Reset"
...

Friend Shared ReadOnly Property Messenger() As Messenger
     Get
         Return _messenger
     End Get
End Property

Now, when we do our override, we can send a message beforehand and tell the "listener" to perform some action. (hide the buttons). Then we can call the CreateImageFilePNG method to produce a .png outputfile. Then, we send another message to tell the "listener" to perform another action (unhide the buttons).

EditRoundViewModel Override
VB.NET
 Protected Overrides Sub ImageExecute(ByVal obj As FrameworkElement)
        Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_COMMAND)

        Dim GWSSaveImage = MyBase.GetService(Of IGWSSaveImage)()
        GWSSaveImage.CreateImageFilePNG(obj)

        Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_RESET_COMMAND)
    End Sub

In the EditRoundView code behind file, we register to receive both messages and assign an action to perform for each one. The first message will hide the buttons and the second message will restore them. The buttons must have a name property in the XAML so they can be accessed from the codebehind. All of this code behind code is View related and called by the messenger.

EditRoundView.Xaml.VB Code Behind File
VB.NET
Partial Public Class EditRoundView
    Public Sub New()
        InitializeComponent()
        Application.Messenger.Register_
        (Application.MSG_CLEANUP_COMMAND, New Action(AddressOf Me.CleanUpScreenImage))
        Application.Messenger.Register_
        (Application.MSG_CLEANUP_RESET_COMMAND, New Action(AddressOf Me.CleanUpResetScreenImage))
    End Sub

    Private Sub CleanUpScreenImage()
        AcceptButton.Visibility = Windows.Visibility.Hidden
        CancelButton.Visibility = Windows.Visibility.Hidden
        ApplyButton.Visibility = Windows.Visibility.Hidden
        GetRnd.Visibility = Windows.Visibility.Hidden
    End Sub

    Private Sub CleanUpResetScreenImage()
        AcceptButton.Visibility = Windows.Visibility.Visible
        CancelButton.Visibility = Windows.Visibility.Visible
        ApplyButton.Visibility = Windows.Visibility.Visible
        GetRnd.Visibility = Windows.Visibility.Visible
    End Sub
End Class

Let's create a bitmap image of the screen and use it in a tooltip.

This might not seem as the most useful thing, but it is demonstrating how to obtain a bitmap of the screen and use it in your program.

On our EditRoundViewModel, we need to add property get/set to hold the bitmap source.

EditRoundViewModel Property
VB.NET
 Public Property BitmapScreen As BitmapSource
        Get
            Return _BitmapScreen
        End Get
        Set(value As BitmapSource)
            MyBase.SetPropertyValue("BitmapScreen", _BitmapScreen, value)
        End Set
End Property

Recall from the top of the article, we declared the method to CreateBitmapSource as public so we could access it later. So here we are later. Assign the bitmap source created to our new BitmapScreen property and we now have an image of the screen sitting in our ViewModel. You can hide any controls you like before creating the image. Notice I set the scaling in the CreateBitmapSource to 0.35 to create a small image.

EditRoundViewModel ImageExecute Override
VB.NET
 Protected Overrides Sub ImageExecute(ByVal obj As FrameworkElement)
        Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_COMMAND)

        Dim GWSSaveImage = MyBase.GetService(Of IGWSSaveImage)()
        GWSSaveImage.CreateImageFilePNG(obj)

        BitmapScreen = GWSSaveImage.CreateBitmapSource(obj, 0.35)

        Application.Messenger.NotifyColleagues(Application.MSG_CLEANUP_RESET_COMMAND)
End Sub

Now, we just create the tooltip in the XAML file and set the source to the BitmapScreen property in the ViewModel holding the screen image. Then, assign the tooltip to a style and use it wherever you like.

EditRoundViewModel XAML Tooltip
VB.NET
 <Grid.Resources>
      <ToolTip x:Key="tt">
             <StackPanel Orientation="Vertical">
                <TextBlock Text="Tool Tip with Image"></TextBlock>
                <Image  Source="{Binding BitmapScreen}"/>
             </StackPanel>
      </ToolTip>
...

      <Style x:Key="TextBoxStyle1" TargetType="TextBox">
          <Setter Property="TextBox.Margin" Value="5" />
          <Setter Property="TextBox.TextAlignment" Value="Right" />
          <Setter Property="ToolTip" Value="{StaticResource tt}"/>
       </Style>
...

Let's allow the user to select the print stretch setting from a menu item.

This will show how to let the user select property values to adjust the screen image. Although this will explain how to let the user select stretch values, the same logic could be followed to allow the user to select other properties. (Font size, color, etc.)

On the DisplayViewModel, we need to setup an ICommand to link to the Stretch Menu item we will create in a moment. We also need a PrintStretch property to hold our user selected Stretch Value. We need to make sure that PrintControl object points to Print Stretch property for its value.

DisplayViewModel Stretch ICommand and Property
VB.NET
...
Public ReadOnly Property PrintStretchCommand() As ICommand
        Get
            Return New RelayCommand(Of Stretch)(AddressOf Me.PrintStretch)
        End Get
End Property
Private Sub PrintStretch(ByVal obj As Stretch)
        Print_Stretch = obj
End Sub

Public Property Print_Stretch() As Stretch
        Get
            Return _Print_Stretch
        End Get
        Set(value As Stretch)
            _Print_Stretch = value
        End Set
End Property
...

Protected Overridable Sub Print(ByVal obj As UIElement)
        Dim GWSPrintControl = MyBase.GetService(Of IGWSPrintControl)()

        With GWSPrintControl
            .PrintDescription = "Print Demo Description"
            .PrintTitle = "Control Printout"
    	    .PrintStretch = Me.Print_Stretch
            .PrintMargin = 50
        End With

        GWSPrintControl.PrintUIElement(obj)
    End Sub
...

Stretch is an Microsoft supplied Enum. There are 4 available values (Fill, None, Uniform, UniformtoFill). Since it is an Enum, we can use it to automatically create our menu items in XAML. We need to make sure we set a reference Mscorlib and PresentationCore assemblies in our XAML. Then, we can set up an ObjectDataProvider to give us the stretch values.

MainWindowView XAML ObjectDataProvider
XML
<Window x:Class="MainWindowView" Name="MyWin"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:PrintSaveDemo"
        xmlns:sys="clr-namespace:System;assembly=mscorlib"
        xmlns:prt="clr-namespace:System.Windows.Media;assembly=PresentationCore"
...
 <Window.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/MainResources.xaml" />
            </ResourceDictionary.MergedDictionaries>
           
            <ObjectDataProvider x:Key="dataFromEnum1"
                            MethodName="GetValues"
                            ObjectType="{x:Type sys:Enum}">
                <ObjectDataProvider.MethodParameters>
                    <x:Type TypeName="prt:Stretch" />
                </ObjectDataProvider.MethodParameters>
            </ObjectDataProvider>
...

Now, we can create the menu item for the Stretch values and set the ItemSource to ObjectDataProvider (x:Key=dataFromEnum1). To select the value from the menu item, I am using a mutually exclusive set of Radio Buttons. A StyleSelector is used to select a style to show a RadioButton that is checked or unchecked.

MainWindowView XAML Stretch Menu and Styles
XML
<MenuItem.Resources>
     <RadioButton x:Key="RadioMenu"
                  GroupName="RadioEnum"
                  x:Shared="False"
                  HorizontalAlignment="Center"
                  IsHitTestVisible="False"/>

     <RadioButton x:Key="RadioMenuChecked"
                  GroupName="RadioEnum"
                  x:Shared="False"
                  HorizontalAlignment="Center"
                  IsHitTestVisible="False"
                  IsChecked="True"/>

       <Style TargetType="MenuItem" x:Key="RadioUnCheckedStyle">
              <Setter Property="Command" 
              Value="{Binding Path=DataContext.PrintStretchCommand,
                      RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
              <Setter Property="CommandParameter" Value="{Binding}" />
              <EventSetter Event="Click" 
              Handler="RadioEnum_Click" />
              <Setter Property="Icon" 
              Value="{StaticResource RadioMenu}"/>
              <Setter Property="StaysOpenOnClick" Value="True"/>
        </Style>

        <Style TargetType="MenuItem" x:Key="RadioCheckedStyle"
               BasedOn="{StaticResource RadioUnCheckedStyle}">
               <Setter Property="Icon" 
               Value="{StaticResource RadioMenuChecked}"/>
       </Style>
</MenuItem.Resources>

<MenuItem Header="Print S_tretch"
          Name="StretchEnum"
          DataContext="{Binding}"
          ItemsSource="
         {Binding Source={StaticResource dataFromEnum1}}" >
...
   <MenuItem.ItemContainerStyleSelector>
        <local:RadioStyleSelector UnCheckedStyle="{StaticResource RadioUnCheckedStyle}"
               CheckedStyle="{StaticResource RadioCheckedStyle}" />
   </MenuItem.ItemContainerStyleSelector>
</MenuItem>

In order to make sure you can click on the MenuItem and not just the RadioButton, the following code needs to be added to the Codebehind file. Again, this is all View related code.

MainWindow.Xaml.Vb Click Event
VB.NET
Partial Public Class MainWindowView
    Public Sub New()
        InitializeComponent()
    End Sub
...

#Region "Methods"
    Private Sub RadioEnum_Click(sender As Object, e As System.Windows.RoutedEventArgs)
       'This solution is from a stackoverflow answer:   http://stackoverflow.com/a/11497189/375727

        Dim mitem As MenuItem = TryCast(sender, MenuItem)
        If mitem IsNot Nothing Then
            Dim rbutton As RadioButton = TryCast(mitem.Icon, RadioButton)
            If rbutton IsNot Nothing Then
                rbutton.IsChecked = True
            End If
        End If
    End Sub
#End Region
End Class

There you have it. A fully functional WPF/MVVM program with the ability to print or capture each screen.

*Cartoon Images from Xamalot.com free clipart.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

George Sefcik
Software Developer (Senior) GWSoftware
United States United States
Independent Software Developer
There are no such things as problems...only opportunities to work harder!

Comments and Discussions

 
-- There are no messages in this forum --