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 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.
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 _ _GridPrintDocument.BeginPrint '\\ 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 Next Dim TotalWidthOfAllPages As Integer = (e.MarginBounds.Width * PagesAcross) _ColumnBounds.Clear() 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 Else 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 _ColumnBounds.NextPage() NextColumn.Left = e.MarginBounds.Left End If _ColumnBounds.Add(NextColumn) Next 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) Next _CurrentPrintGridLine = nextLine '\\ Print the document footer Call PrintFooter(e) If _CurrentPageAcross = PagesAcross Then _CurrentPageAcross = 1 _CurrentPageDown += 1 Else _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 Else _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.
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.
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.
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.
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.
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