|
|
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