Mar 12, 2007

Using .NET Classes To Simplify Printing




If I could figure out where each of my gray hairs came from,

I bet that programming Windows printing would a major cause.

I think most programmers would agree that writing code to create top-quality printed output is a real chore. For example, in Visual Basic classic, you were limited to using the PrintForm method and the Printer object - workable, but hardly easy! Other pre-.NET programming tools had similar limitations, if not always as serious as that one.

Fortunately, .NET's class library offers truly significant improvements in the way printing from an application is handled. From the ground up, printing is treated as just another kind of graphical operation, no different from displaying graphics on the screen (in .NET the term "graphics" encompasses text as well as lines, shapes, and so on).

This is not a new concept - other programming tools have treated the printed page as just another drawing surface - but the way that .NET implements it breaks new ground in power and ease of use.

This means that to understand printing in .NET you must understand its graphics capabilities. I can give only the briefest of overviews here before getting to the details of printing. All printing-related classes are in the System.Drawing.Printing namespace.

The Graphics class is the heart of all .NET's graphics capabilities. This class encapsulates a GDI+ drawing surface and is involved in essentially all graphics operations that a program performs. It provides methods for drawing text and a wide variety of shapes. Typically, a program will use the Graphics object that is passed to a form's Paint event procedure to draw to the screen. Here's a simple example:

Private Sub Form1_Paint(ByVal sender As Object, ByVal e As _
System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint

Dim g As Graphics = e.Graphics

' Define a rectangle.
Dim r As New Rectangle(25, 25, 200, 150)
' Draw it with a black pen.
g.DrawRectangle(Pens.Black, r)
' Add some text.
Dim f As New Font("Times", 24)
g.DrawString("Printing", f, Brushes.Red, 70, 80)

End Sub

Figure 1An example of .NET graphics output.

Figure 1 shows the resulting screen output. Note that most programs do not actually put the drawing code in the Paint event procedure, but rather in one or more independent procedures that are called from this event procedure.

There's lots more to the Graphics class, but that's not the subject of this article. We want to know how .NET integrates printing with the standard on-screen graphics operations. The answer lies in the PrintDocument class, which encapsulates almost all of .NET's printing functionality. Most important, the PrintDocument class also encapsulates a Graphics object that represents the printer. In broad overview, here's how this works:

  1. Write a procedure that uses the Graphics class to create the desired output.
  2. Create an instance of the PrintDocument class.
  3. Connect the procedure created in Step 1 to the PrintPage event of the PrintDocument object. You do this with Visual Basic's AddressOf operator.
  4. Call the PrintDocument object's Print method.

Let's see how this works for printing the output shown in figure 1. The first step is to write the procedure that uses Graphics class methods to create the output. This procedure can have any name you like, but it must have the correct signature (number and type of arguments) as shown here:

Private Sub pd_PrintPage(ByVal sender As Object, _
ByVal e As PrintPageEventArgs)

Dim g As Graphics = e.Graphics

' Define a rectangle.
Dim r As New Rectangle(25, 25, 200, 150)
' Draw it with a black pen.
g.DrawRectangle(Pens.Black, r)
' Add some text.
Dim f As New Font("Times", 24)
g.DrawString("Printing", f, Brushes.Red, 70, 80)

End Sub

There's two things to pay attention to in this procedure. First, it is passed a Graphics object as part of the PrintPageEventArgs object. This is the Graphics object that is connected to the printer and is, therefore, the Graphics object that you'll use to create output. Second, the actual drawing code is exactly the same as we used earlier to display the rectangle and text on-screen.

The remaining three steps are accomplished together in the following code. In this case, it is in a Button control's event procedure:

Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click

' Create a PrintDocument object
Dim prDoc As New PrintDocument
' Link the printing procedure with the PrintPage event.
AddHandler prDoc.PrintPage, AddressOf Me.pd_PrintPage
'Start printing.
prDoc.Print()

End Sub

The result is the box and text printed on the system's default printer. In this example, the code for screen display and the for printer output are in different procedures, but the beauty of .NET is that they do not have to be. More commonly, you will write a single procedure for output - this procedure is passed a reference to the Graphics object to use. Here's the procedure for our example:

Private Sub DrawOutput(ByVal g As Graphics)

' Define a rectangle.
Dim r As New Rectangle(25, 25, 200, 150)
' Draw it with a black pen.
g.DrawRectangle(Pens.Black, r)
' Add some text.
Dim f As New Font("Times", 24)
g.DrawString("Printing", f, Brushes.Red, 70, 80)

End Sub

Then, your code calls this procedure from the Paint event procedure for screen output or from the PrintPage procedure for printed output, passing the correct Graphics reference in each case. This enables the same drawing code to perform double duty, for screen display and printing:

Private Sub pd_PrintPage(ByVal sender As Object, _
ByVal e As PrintPageEventArgs)

DrawOutput(e.Graphics)

End Sub

Private Sub Form1_Paint(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint

DrawOutput(e.Graphics)

End Sub

Dealing With Settings

So far, we have looked at .NET printing at its simplest. There's a lot more to it, as you might well suspect. It's unavoidable that printing involves certain complications that are not present for screen display. These include:

  • Printer settings associated with a print job. These settings are related to the printer hardware, controlling things such as the printer to use, the paper source, and the number of copies.
  • Page settings. These control page-related characteristics, such as paper size and margins.

Figure 2Figure 2: The printer settings dialog box.

.NET makes it relatively painless to deal with these settings. Printer settings are handled by the PrintDialog class. This class presents the standard print settings dialog box which is used in many Windows applications, as shown in Figure 2. The user can select a printer, set the printer properties (using the printer driver software), and choose other printer settings (See Figure 2).

Here's how it works:

  1. Create an instance of the PrintDialog class.
  2. Set the PrintDialog object's Document property to the PrintDocument object that is being used for the print job.
  3. Display the printer settings dialog box by calling the ShowDialog method.
  4. Verify that the user closed the printer settings dialog box by clicking OK (rather than Cancel). Then, start the print job by calling the PrintDocument.Print method.

Because the PrintDialog object was associated with the print job in Step 2, the settings that the user chooses in this dialog box are automatically applied to the print job. Here's a modification of an earlier listing that displays the printer settings dialog box before starting the print job.

Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click

' Create a PrintDocument object
Dim prDoc As New PrintDocument
' Link the printing procedure with the PrintPage event.
AddHandler prDoc.PrintPage, AddressOf Me.pd_PrintPage< ' Create the printer settings dialog box and associate ' it with the PrintDocument object. Dim prDlg As New PrintDialog prDlg.Document = prDoc ' Display dialog. If user closes dialog with OK, ' start printing. if prDlg.ShowDialog = DialogResult.OK Then prDoc.Print() End Sub
Figure 3Figure 3: The page settings dialog box.

Page settings are handled in a similar manner. You use the PageSetupDialog class in a manner that parallels the technique for PrintDialog class that I just described, associating it with the PrintDocument object and then displaying it to the user. This dialog box is shown in Figure 3. The user can select the paper source and size as well as orientation and margins.

You do not have to use the PrintDialog and PageSetupDialog classes in your program. In fact, for maximum printing control you cannot use these classes, because the dialog boxes do not provide access to some settings, particularly regarding page setup. If you need all the printing control possible, you'd use the PageSettings class, which encapsulates all available page settings but has no visual interface. You would design your own page settings dialog box to let the user make selections, then put this data in a PageSettings object, and then associate that object with the print job's PrintDocument object.

There's a lot more to .NET printing that cannot be covered here. Once you start working with the printing-related classes I think you'll agree that with .NET, Microsoft has made printing... well, perhaps not exactly easy, but certainly easier!

Peter Aitken has been writing about Windows applications and programming for more than 10 years. His books include Visual Basic.Net Programming with Peter Aitken(ISBN 1576109615). Peter operates a private consulting firm that provides programming, Web design, and technical writing services to business and government.