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
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
mReportGrid.Margin = New Thickness(0)
mReportGrid.VerticalAlignment = Windows.VerticalAlignment.Stretch
mReportGrid.HorizontalAlignment = Windows.HorizontalAlignment.Stretch
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
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
Dim docContent As New ReportContent(dblPrtWidth, dblPrtHeight)
Dim i As Integer
docContent.AddElement("TITLE", _
ReportContent.enmTypeOfControl.textBlock, FindResource("ReportTitle"))
Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
objUserControl = New MyUserControl()
Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
i = docContent.AddElement(objUserControl , ReportContent.enmTypeOfControl.usercontrol)
Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
DirectCast(DirectCast(docContent.ReportStackPanel.Children(i), UserControl).Content, _
MyUserControl).imInside.Visibility = Windows.Visibility.Collapsed
Dispatcher.Invoke(Sub() Action(), Windows.Threading.DispatcherPriority.ContextIdle, Nothing)
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()
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