Hardcore Hardcopy

A quick dash through the procedures, techniques and methods that are involved in getting it all down on paper

The print system

Although it is possible to live a happy and fulfilling life without ever looking under the bonnet at the print subsystem it does make it slightly easier to write an application that has hardcopy capabilities if you have some understanding of what the operating system does in response to the “print” command.

The first thing an application needs to know in order to print is what print device to print to. To achieve this it queries the spool server and asks it to return a list of the devices attached, and some basic information about them (name, location, device driver etc.)

Then the user selects one and the application obtains a handle that refers to that printer, through which it queries the device settings (such as the paper size it uses, resolution etc.) and then it obtains a device context on which to draw the first page. Drawing the page is done with the standard GDI drawing commands such as ExtTextOut, Rectangle, Polygon etc.

Once the page is drawn the application either notifies the spool system that the job is complete or requests another page. At this stage the drawn page can be translated into the printer control language appropriate to the printer by the printer driver and the hardware can do its thing.

In between pages the application can also make changes to the settings of the printer so that, for example, one page can be printed landscape and the next portrait etc.

Whenever the application or the printer performs an action on the print job a notification is raised so that any application which monitors print jobs can be notified of their progress and so that any problems that occur printing the document can be notified to the user to rectify them.

Printing in .NET

Printing in .NET follows the overview of the print system quite closely. There are two main framework classes that you use to do all your work with printing which are in the System.Drawing.Printing namespace: PageSettings (which is used for such things as selecting the paper size and landscape or portrtait) and PrintDocument which is used to do the printing operation itself.

PrintDocument class

To demonstrate the use of the PrintDocument class I have put together a quick class which extends it in order to print a data grid onto a page with some of the niceties found in the Excel print system such as customising the colours and line styles in the grid and printing the grid across multiple pages across. (Full source code on TheCodeProject)

First I initialise all the print variables in the BeginPrint class event. This is called before any other code in the print process so it is a good place to reset your current row pointers and to set up the fonts that you are going to use throughout the print process.

    Private Sub _GridPrintDocument_BeginPrint(ByVal sender As Object, ByVal e As System.Drawing.Printing.PrintEventArgs) Handles _ 

        '\\ Initialise the current page and current grid line variables
        _CurrentPrintGridLine = 1
        _CurrentPageDown = 1
        _CurrentPageAcross = 1

        If _Textlayout Is Nothing Then
            _Textlayout = New System.Drawing.StringFormat
            _Textlayout.Trimming = StringTrimming.EllipsisCharacter
        End If

    End Sub

Then each page is printed in the PrintPage event. For the sake of code clarity (and developer sanity) the actual printing is split up into a number of private methods – PrintHeader which prints the header text, PrintFooter prints a page footer, PrintGridHeaderLine prints the table column headers and PrintGridLine prints a row of tabular data.

   Private Sub _GridPrintDocument_PrintPage(ByVal sender As Object, ByVal e As System.Drawing.Printing.PrintPageEventArgs) _ 
                               Handles _GridPrintDocument.PrintPage

        If _CurrentPageDown = 1 And _CurrentPageAcross = 1 Then
            ' _HeaderRectangle -  The top 10% of the page
            _HeaderRectangle = e.MarginBounds
            _HeaderRectangle.Height = CInt(e.MarginBounds.Height * _HeaderHeightPercent * 0.01)

            ' _FooterRectangle - the bottom 10% of the page
            _FooterRectangle = e.MarginBounds
            _FooterRectangle.Height = CInt(e.MarginBounds.Height * _FooterHeightPercent * 0.01)
            _FooterRectangle.Y += CInt(e.MarginBounds.Height * (1 - (0.01 * _FooterHeightPercent)))

            ' _PageContentRectangle - The middle 80% of the page
            _PageContentRectangle = e.MarginBounds
            _PageContentRectangle.Y += CInt(_HeaderRectangle.Height + e.MarginBounds.Height * (_InterSectionSpacingPercent * 0.01))
            _PageContentRectangle.Height = CInt(e.MarginBounds.Height * 0.8)

            _Rowheight = e.Graphics.MeasureString("a", _PrintFont).Height

            '\\ Create the _ColumnBounds array
            Dim nColumn As Integer
            Dim TotalWidth As Double

            If _DataGrid.DataSource Is Nothing Then
                '\\ Nothing in the grid to print
                Exit Sub
            End If

            Dim ColumnCount As Integer = GridColumnCount()

            For nColumn = 0 To ColumnCount - 1
                Dim rcLastCell As Rectangle = _DataGrid.GetCellBounds(0, nColumn)
                If rcLastCell.Width > 0 Then
                    TotalWidth += rcLastCell.Width
                End If

            Dim TotalWidthOfAllPages As Integer = (e.MarginBounds.Width * PagesAcross)
            For nColumn = 0 To ColumnCount - 1
                '\\ Calculate the column start point
                Dim NextColumn As New ColumnBound
                If nColumn = 0 Then
                    NextColumn.Left = e.MarginBounds.Left
                    NextColumn.Left = _ColumnBounds.RightExtents
                End If
                '\\ Set this column's width
                Dim rcCell As Rectangle = _DataGrid.GetCellBounds(0, nColumn)
                If rcCell.Width > 0 Then
                    rcCell.Width = rcCell.Width - 1
                    NextColumn.Width = (rcCell.Width / TotalWidth) * TotalWidthOfAllPages
                    If NextColumn.Width > e.MarginBounds.Width Then
                        NextColumn.Width = e.MarginBounds.Width
                    End If
                End If
                If _ColumnBounds.RightExtents + NextColumn.Width > (e.MarginBounds.Left + e.MarginBounds.Width) Then
                    NextColumn.Left = e.MarginBounds.Left
                End If
            If _ColumnBounds.TotalPages > Me.PagesAcross Then
                Me.PagesAcross = _ColumnBounds.TotalPages
            End If
        End If

        '\\ Print the document header
        Call PrintHeader(e)

        '\\ Print as many grid lines as can fit
        Dim nextLine As Int32
        Call PrintGridHeaderLine(e)
        Dim StartOfpage As Integer = _CurrentPrintGridLine
        For nextLine = _CurrentPrintGridLine To Min((_CurrentPrintGridLine + RowsPerPage(_PrintFont, e.Graphics)), CType(_DataGrid.DataSource, System.Data.DataTable).DefaultView.Count)
            Call PrintGridLine(e, nextLine)
        _CurrentPrintGridLine = nextLine

        '\\ Print the document footer
        Call PrintFooter(e)

        If _CurrentPageAcross = PagesAcross Then
            _CurrentPageAcross = 1
            _CurrentPageDown += 1
            _CurrentPageAcross += 1
            _CurrentPrintGridLine = StartOfpage
        End If

        '\\ If there are more lines to print, set the HasMorePages property to true
        If _CurrentPrintGridLine < GridRowCount() Then
            e.HasMorePages = True
        End If

    End Sub

Each of these private methods uses a simple method, DrawCellString, which prints text in a defined rectangle using the defined layout rules. It is from that simple method the whole page printing operation emerges.

Public Sub DrawCellString(ByVal s As String, _
                                    ByVal HorizontalAlignment As CellTextHorizontalAlignment, _
                                    ByVal VerticalAlignment As CellTextVerticalAlignment, _
                                    ByVal BoundingRect As Rectangle, _
                                    ByVal DrawRectangle As Boolean, _
                                    ByVal Target As Graphics, _
                                    ByVal PrintFont As Font, _
                                    ByVal FillColour As Brush)

        If DrawRectangle Then
            Target.FillRectangle(FillColour, BoundingRect)
            Target.DrawRectangle(_GridPen, BoundingRect)
        End If

        '\\ Set the text alignment
        If HorizontalAlignment = CellTextHorizontalAlignment.LeftAlign Then
            _Textlayout.Alignment = StringAlignment.Near
        ElseIf HorizontalAlignment = CellTextHorizontalAlignment.RightAlign Then
            _Textlayout.Alignment = StringAlignment.Far
            _Textlayout.Alignment = StringAlignment.Center
        End If

        '\\ Draw the text inside the defined rectangle
        Dim BoundingRectF As New RectangleF(BoundingRect.X + _CellGutter, BoundingRect.Y + _CellGutter, BoundingRect.Width - (2 * _CellGutter), BoundingRect.Height - (2 * _CellGutter))

        Target.DrawString(s, PrintFont, System.Drawing.Brushes.Black, BoundingRectF, _Textlayout)

    End Sub

At the end of each page we test to see if there are any more rows left to print. If there are we set the e.HasMorePages property to true which informs the spooler system that we want to print another page.

PrintDocument class overview

Customising PrintDocument

One of the problems with simply adding a PrintDocument class to your application to handle all the printing is that the code that goes in the PrintPage event can soon become very convoluted, especially if you are dealing with a complex page layout that features static elements (such as headers and footers and company logos) and dynamic elements such as tabular data grids and paragraphs of text.

To get around this complexity you can use a dedicated reporting component (such as crystal reports or running MS Word via automation or many other solutions) or you can write a printing utility library that extends the PrintDocument class to provide a much more powerful print experience – and it is this latter that I will show you an example of my own.

Customised printdocument class

The structured print document library is based on a set of classes that implement a concept that will be familiar to anyone who has ever used a word processor: text styles plus document templates plus data equals documents.

Text styles

A text style is a class that describes how a printed item should look – which font to use, which vertical and horizontal alignment to use and any colour and border settings.

Document Templates

A document template is the design time view of the document. It consists of one or more page templates which in turn are made up of one or more page areas. These are rectangular areas that can contain a text item (similar to a label in windows forms parlance), a picture or tabular data.

The design of the application allows for more document area writer classes to be added so that, for example, barcodes and graphs could be included.

Data flows

Also in the design time view of the document is the data flow template. This defines what data are to be printed using this document and what data actions are to be performed as the document is printed. For example you might have a document template that has an employee summary and a data source that is a collection of employee data objects. To print one page per employee you would need to set the data action to move the employee data set to the next record at the end of each page. You would also need a check that stops printing the employee summary pages when the employee data set reaches the end.

Data flow design is also involved in deciding whether a page template should be printed again for the next record or whether the current page template has no more pages to print.

Get the source code for this part of the article on the code project. If there is sufficient interest I will see if I can get it hosted on CodePlex so we can all develop it