Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Export WPF page to PDF file

0.00/5 (No votes)
5 Nov 2013 2  
This example shows you how to create a PDF report from a (set of) WPF page(s).

Introduction

Since there aren't any out of the box solutions to create a PDF report (or I haven't found any), we created a set of a classes and instruction to support this.

Background

After developing a WPF applications (which was to be used on PC and laptops with touchscreens), we were asked to create a possibility to create PDF reports in this application. After a lot of searching on the web and trying what we found, we came with a fairly easy solution.

Using the code

In our application we use a main screen with menu and command buttons. Each menu-item or command button can then open a a usercontrol in this main screen. This way we could keep track of navigation and implement the possibility of going back one page and going forward within one theme. Anyway, it is a kind of printscreen of these usercontrols we want in our PDF report.

We created three classes for a report.

1. ReportDocument

Public Class ReportDocument
    Private mReportDocument As New FixedDocument
    Public Property ReportDocument() As FixedDocument
        Get
            Return mReportDocument
        End Get
        Set(ByVal value As FixedDocument)
            mReportDocument = value
        End Set
    End Property
 
    Private mReportPage As New List(Of FixedPage)
    Public Property ReportPage() As List(Of FixedPage)
        Get
            Return mReportPage
        End Get
        Set(ByVal value As List(Of FixedPage))
            mReportPage = value
        End Set
    End Property
 
    Private mReportWidth As Double
    Public Property ReportWidth() As Double
        Get
            Return mReportWidth
        End Get
        Set(ByVal value As Double)
            mReportWidth = value
        End Set
    End Property
 
    Private mReportHeight As Double
    Public Property ReportHeight() As Double
        Get
            Return mReportHeight
        End Get
        Set(ByVal value As Double)
            mReportHeight = value
        End Set
    End Property
 
    Public Sub New(dblWidth As Double, dblHeight As Double)
        mReportWidth = dblWidth
        mReportHeight = dblHeight
    End Sub
 
    Public Function CreateReport() As FixedDocument
        'ADD PAGES TO CONTENT TO DOCUMENT
        mReportDocument.DocumentPaginator.PageSize = New Size(mReportWidth, mReportHeight)
        For Each itemElement In mReportPage
            Dim pagContent As New PageContent
            DirectCast(pagContent, IAddChild).AddChild(itemElement)
            mReportDocument.Pages.Add(pagContent)
        Next
        Return mReportDocument
    End Function
 
End Class

We need to have a FixedDocument with a Width and Height (defined at initialitsation) and a list of Pages (ReportPages). The function CreateReport will add every page to the FixedDocument as a PageContent.

2. ReportPage

Public Class ReportPage
 
    Private mReportGrid As New Grid
    Public Property ReportGrid() As Grid
        Get
            Return mReportGrid
        End Get
        Set(ByVal value As Grid)
            mReportGrid = value
        End Set
    End Property
 
    Private mReportWidth As Double
    Public Property ReportWidth() As Double
        Get
            Return mReportWidth
        End Get
        Set(ByVal value As Double)
            mReportWidth = value
        End Set
    End Property
 
    Private mReportHeight As Double
    Public Property ReportHeight() As Double
        Get
            Return mReportHeight
        End Get
        Set(ByVal value As Double)
            mReportHeight = value
        End Set
    End Property
 
    Public Sub New(dblWidth As Double, dblheight As Double, blnHeader As Boolean, blnFooter As Boolean)
        mReportHeight = dblheight
        mReportWidth = dblWidth
        'define grid
        mReportGrid.Margin = New Thickness(0)
        mReportGrid.VerticalAlignment = Windows.VerticalAlignment.Stretch
        mReportGrid.HorizontalAlignment = Windows.HorizontalAlignment.Stretch
        'CREATE ROW DEFINITION
        Dim grdRow1 As New RowDefinition
        Dim grdRow2 As New RowDefinition
        Dim grdRow3 As New RowDefinition
        Dim intLessHeight As Integer = 0
        If blnHeader = True Then
            grdRow1.Height = New GridLength(70)
            intLessHeight += 70
        End If
        If blnFooter = True Then
            grdRow3.Height = New GridLength(50)
            intLessHeight += 50
        End If
        grdRow2.Height = New GridLength(mReportHeight - intLessHeight)
        mReportGrid.RowDefinitions.Add(grdRow1)
        mReportGrid.RowDefinitions.Add(grdRow2)
        mReportGrid.RowDefinitions.Add(grdRow3)
    End Sub
 
    Public Sub AddHeader(objHeader As Object)
        Dim uctHeaderCode As New UserControl
        uctHeaderCode.Content = objHeader
 
        Dim stpHeader As New StackPanel
        stpHeader.Orientation = Orientation.Vertical
        stpHeader.Width = mReportWidth - 80
        stpHeader.Margin = New Thickness(0, 20, 0, 0)
        stpHeader.VerticalAlignment = Windows.VerticalAlignment.Top
        stpHeader.Children.Add(uctHeaderCode)
        Grid.SetRow(stpHeader, 0)
 
        mReportGrid.Children.Add(stpHeader)
    End Sub
 
    Public Sub AddFooter(strFooter As String)
        Dim txtFooter As New TextBlock
        txtFooter.Width = mReportWidth - 150
        txtFooter.Margin = New Thickness(0)
        txtFooter.FontSize = 8
        txtFooter.HorizontalAlignment = HorizontalAlignment.Left
        txtFooter.TextWrapping = TextWrapping.WrapWithOverflow
        txtFooter.Text = strFooter
 
        Dim txtDatePage As New TextBlock
        txtDatePage.Width = 100
        txtDatePage.Margin = New Thickness(60, 0, 0, 0)
        txtDatePage.FontSize = 8
        txtDatePage.HorizontalAlignment = HorizontalAlignment.Right
        txtDatePage.TextWrapping = TextWrapping.WrapWithOverflow
        txtDatePage.Text = Format(Date.Today, "dd/MM/yyyy").ToString
 
        Dim stpFooter As New StackPanel
        stpFooter.Orientation = Orientation.Horizontal
        stpFooter.Width = mReportWidth - 50
        stpFooter.Margin = New Thickness(0, 0, 0, 20)
        stpFooter.VerticalAlignment = Windows.VerticalAlignment.Bottom
        stpFooter.Children.Add(txtFooter)
        stpFooter.Children.Add(txtDatePage)
        Grid.SetRow(stpFooter, 2)
 
        mReportGrid.Children.Add(stpFooter)
    End Sub
 
    Public Sub AddContent(elElement As System.Windows.UIElement)
 
        Grid.SetRow(elElement, 1)
 
        mReportGrid.Children.Add(elElement)
    End Sub
 
    Private Function CreateLineUnderHeader() As Line
        Dim LinLine As New Line
        LinLine.StrokeThickness = 1
        LinLine.X1 = 20
        LinLine.Y1 = 60
        LinLine.X2 = mReportWidth - 20
        LinLine.Y2 = 60
        LinLine.Stroke = Brushes.Black
        Return LinLine
    End Function
 
    Private Function CreateLineAboveFooter() As Line
        Dim LinLine As New Line
        LinLine.StrokeThickness = 1
        LinLine.X1 = 20
        LinLine.Y1 = mReportHeight - 55
        LinLine.X2 = mReportWidth - 20
        LinLine.Y2 = mReportHeight - 55
        LinLine.Stroke = Brushes.Black
        Return LinLine
    End Function
 
    Public Function CreateReportPage(blnWithLineUnderHeader As Boolean, blnWithLineAboveFooter As Boolean)
        Dim tmpPage As New FixedPage
        tmpPage.Width = mReportWidth
        tmpPage.Height = mReportHeight
        tmpPage.Children.Add(mReportGrid)
        If blnWithLineUnderHeader Then tmpPage.Children.Add(CreateLineUnderHeader)
        If blnWithLineAboveFooter Then tmpPage.Children.Add(CreateLineAboveFooter)
        Return tmpPage
    End Function
 
End Class

A ReportPage contains a grid that has the same width and height of the document. This grid, in our case, contains 3 rows: a header, some content (our usercontrols) and a footer. Since our header and footer are the same, we added some methods to add them. Our header contains info about our 'customer', our footer contains some legal information.

We can add content to this Page by the method AddContent which takes a UiElement as parameter. We add a stackpanel of another class (ReportContent) which contains our usercontrols, labels ...

We then call the function CreateReportPage, that adds the grid to a fixedpage and returns this fixedpage. This fixedpage will be added to the list of pages for our ReportDocument (see higher).

3. ReportContent

Public Class ReportContent
 
    Enum enmTypeOfControl
        usercontrol
        textBlock
    End Enum
 
    Private mReportStackPanel As New StackPanel
    Public Property ReportStackPanel() As StackPanel
        Get
            Return mReportStackPanel
        End Get
        Set(ByVal value As StackPanel)
            mReportStackPanel = value
        End Set
    End Property
 
    Private mReportWidth As Double
    Public Property ReportWidth() As Double
        Get
            Return mReportWidth
        End Get
        Set(ByVal value As Double)
            mReportWidth = value
        End Set
    End Property
 
    Private mReportHeight As Double
    Public Property ReportHeight() As Double
        Get
            Return mReportHeight
        End Get
        Set(ByVal value As Double)
            mReportHeight = value
        End Set
    End Property
 
    Public Sub New(dblWidth As Double, dblheight As Double)
        mReportHeight = dblheight
        mReportWidth = dblWidth
 
        Dim stpDashboard As New StackPanel
        mReportStackPanel.Orientation = Orientation.Vertical
        mReportStackPanel.Width = mReportWidth
        'mReportStackPanel.Height = mReportHeight
    End Sub
 
    Public Function AddElement(objObject As Object, typType As enmTypeOfControl, _
           Optional styStyle As Style = Nothing) As Integer
        Select Case typType
            Case enmTypeOfControl.usercontrol
                Dim ucUsercontrol As New UserControl
                ucUsercontrol.Margin = New Thickness(5)
                ucUsercontrol.Width = mReportWidth - 40
                ucUsercontrol.Content = objObject
                mReportStackPanel.Children.Add(ucUsercontrol)
            Case enmTypeOfControl.textBlock
                Dim txtTextBlock As New TextBlock
                txtTextBlock.Style = styStyle
                txtTextBlock.Width = mReportWidth - 40
                txtTextBlock.Text = objObject.ToString
                mReportStackPanel.Children.Add(txtTextBlock)
        End Select
        Return mReportStackPanel.Children.Count - 1
 
    End Function
 
End Class

At the initialisation of this class, we create a stackpanel widt the same width and heigth as our actual report has to be.

We can then add elements with the function AddElement. As parameters we add an object (usercontrol, textBlock...), the type of control (we use it for formatting) and a style (optional). It returns a stackpanel and is then added to a ReportPage (see higher).

We created a Window to create the actual report

we created a window (that can be made invisible). We have to use a window because all content has to have the time to be rendered. In this window we can use threading to wait for the content to render.

Our window contains a docViewer that can be used to preview the document as well. We tried not to use this control, but the pages were not fully rendered ...

Initialize Report

First we initialize a report with the method InitializeReport, fired when the window is rendered (Me.ContentRendered)

Private dblPrtWidth As Double = 793
Private dblPrtHeight As Double = 1122
Private docRep As ReportDocument
Private docPaga as ReportPage 

Public Sub InitializeReport()
      docRep = New ReportDocument(dblPrtWidth, dblPrtHeight)
End Sub

Create the Page (with header, content and footer)

docPage = New ReportPage(dblPrtWidth, dblPrtHeight, True, True)
docPage.AddHeader(New MyHeaderUserControl)
docPage.AddFooter(DirectCast(TryFindResource("pdfFooter"), String))
docPage.AddContent(CreateContent.ReportStackPanel)
Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
docRep.ReportPage.Add(docPage.CreateReportPage(True, True))
Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)

This block creates a new page, adds a header (with brkSignaletique as one of our usercontrols), adds a footer (with some text as our legal stuff) and adds content (a function in the window, see next code block).

This page is then added to the Reports (docRep)

The dispatcher.invoke calls allows the code to be rendered, the sub Action is just an empty method.

Create the Content (to be used in the Page above)

Public Function CreateContent() As ReportContent
    'CREATE PAGECONTENT
    Dim docContent As New ReportContent(dblPrtWidth, dblPrtHeight)
    Dim i As Integer

    'ADD TITLE TO PAGECONTENT
    docContent.AddElement("TITLE", _
      ReportContent.enmTypeOfControl.textBlock, FindResource("ReportTitle"))
    Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)

    'A USERCONTROL
    objUserControl = New MyUserControl()
    Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
    'ADD TO PAGECONTENT
    i = docContent.AddElement(objUserControl , ReportContent.enmTypeOfControl.usercontrol)
    Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
    'Additional actions
    DirectCast(DirectCast(docContent.ReportStackPanel.Children(i), UserControl).Content, _
      MyUserControl).imInside.Visibility = Windows.Visibility.Collapsed
    Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)

    'ADD SPACE TO PAGECONTENT
    docContent.AddElement("", _
      ReportContent.enmTypeOfControl.textBlock, FindResource("ReportSpacer"))
    Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)

    Return docContent
End Function

This function adds 3 things in our content: A Title ( a TextBlock), our usercontrol and a spacer (empty textblock). Of course, you can add as many as you want, as long as it fits on 1 page ! When we add our usercontrol (MyUserControl) to the docContent, we get the position of our usercontrol within the stackpanel of our docContent. This way, we can make changes to our usercontrol, for instance, hide buttons that are not needed on the report.

Finalize the Report

Private Sub FinalizeReport()
    'CREATE FINAL DOCUMENT
    DocViewer.Document = docRep.CreateReport()
    Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
    mFile = "c:\report"       
    Dim xpsDoc As New XpsDocument(mFile & ".xps", IO.FileAccess.ReadWrite)
    Dim XpsDocWriter As Xps.XpsDocumentWriter = XpsDocument.CreateXpsDocumentWriter(xpsDoc)
    XpsDocWriter.Write(docRep.ReportDocument)
    xpsDoc.Close()
    PdfSharp.Xps.XpsConverter.Convert(mFile & ".xps", mFile & ".pdf", 0)
End Sub

We add the docRep to our docviewer. If you stop there, you can preview the document. Here we added some stuff to create and XPF and then convert this to PDF using PdfSharp XPS converter (to be downloaded and added to the reference).

This does the trick. Any question ? Shoot !

Good luck, Wim

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here