Printing

Preview and Print from Your Windows Forms App with the .NET Printing Namespace

Alex Calvo

Code download available at:PrintinginNET.exe(134 KB)

This article assumes you're familiar with C# and Windows Forms

Level of Difficulty123

SUMMARY

Printing is an integral part of every complete Windows-based application. Providing robust printing capabilities in these applications has often proved to be a tedious chore. Now, printing from Windows Forms with the .NET Framework means you must adopt a document-centric approach, resulting in cleaner and more manageable code. While the System.Windows.Forms namespace provides seamless integration with all the standard print dialogs (such as Print Preview, Page Setup, and Print), the System.Drawing.Printing namespace offers numerous classes for extensibility and customization. These classes, and how they provide access to printing capabilities, are covered here. Other useful techniques, such as printing in the background to allow the user to continue other tasks, is also explained.

Contents

Using the PrintDocument Class
Implementing a Derived PrintDocument Class
Printing with GDI+
The PrintController Class
Using the Print Dialogs
Background Printing
Conclusion

M icrosoft® .NET has changed just about everything from a development point of view. Some of these changes, such as Web Forms and ADO.NET, have required major shifts in how you get things done, while others have been more evolutionary in nature, merely improving upon existing technologies (such as System.Xml). For the traditional developer who uses Visual Basic® and Visual C++®, printing from Windows® Forms represents a major change. But, as is the case with so much of the .NET Framework, this change is definitely for the better.

Gone are the days of the Visual Basic Print object and its Printers collection. In the .NET Framework, there is no monolithic Print object and you won't find yourself setting CurrentX and CurrentY properties or issuing commands like EndDoc and NewPage either. If you're coming to .NET from Visual C++, you are probably aware that printing can be a tedious task. It requires, for example, that you carefully keep track of the printing process using the Win32® API, to ensure that pages are printed correctly. It's not so much that you don't have to do these kinds of things anymore. It's just that, with .NET, you end up with printing logic that is cleaner and much more maintainable. The .NET Framework classes allow your printing code to be fully encapsulated. Because you can derive your code from a set of base classes, you'll get all kinds of additional features for free. Hooking up to a Print Preview dialog is trivial in .NET, and that's just one example.

Most of the code examples for this article come from the sample printing application available for download at the link at the top of this article. This sample Windows Forms application, shown in Figure 1, demonstrates many of the new features and capabilities you'll have access to when printing in .NET. It allows you to choose any text document and send it to either the Print Preview dialog or to a specific printer. For demonstration purposes, I'll give you an option for selecting whether a watermark should be displayed on each page.

Figure 1 Windows Forms App

Figure 1** Windows Forms App **

When printing using the Print Preview dialog, you can enable or disable anti-aliasing, a built-in feature that renders text and graphics on the screen with a smoother appearance. Keep in mind, however, that this incurs the cost of reduced output speed. In addition, the Print Preview dialog will automatically take advantage of any font smoothing provided by Windows (ClearType), thus reducing the need to use anti-aliasing. When sending output to the printer, the sample printing application lets you choose a variety of other options as well. Then you can decide whether or not to display a status dialog, show an animated printer in the status bar, or print in a background thread.

Let's take a tour of Windows Forms printing by first examining the quick and dirty way to send output to the printer. I will then look more closely at the right way to print from Windows Forms—using a derived PrintDocument class.

Using the PrintDocument Class

Printing from Windows Forms is a document-centric, event-driven process. The bulk of your effort will go into either using a generic PrintDocument object or implementing a derived PrintDocument class. Inheriting from the base PrintDocument class is the better way to go—for reasons which I'll go into shortly. Nevertheless, at times it may be quicker and simpler to use an instance of the base PrintDocument class.

Printing with a base PrintDocument class requires you to wire the class's PrintPage event to a handler method (static or instance) whose signature matches the PrintPageEventHandler delegate. This event will fire when your code calls the Print method on an instance of a PrintDocument object. To actually draw your page, you use the Graphics property of the PrintPageEventArgs object. An instance of a PrintPageEventArgs class is passed as an argument to the PrintPage event handler. The Graphics property of the PrintPageEventArgs object exposes a GDI+ object that encapsulates the drawing surface onto which you paint your page. (I'll cover some of the basic GDI+ commands later in the article.) To print more than one page, you notify the underlying print controller that you have more to print. You can do this using the HasMorePages property of the PrintPageEventArgs object. Setting the HasMorePages property to true will ensure that your PrintPage event handler gets called again.

In addition, you can set up event handlers for other common print events such as BeginPrint, EndPrint, and QueryPageSettings. BeginPrint is a good place to initialize any objects (such as Fonts) that your PrintPage routine may rely on. The QueryPageSettings event fires immediately before each PrintPage event. It allows you to print each page using different page settings, which you can do by modifying the QueryPageSettingsEventArgs.PageSettings property. In order to modify page settings for the entire document, you can use the DefaultPageSettings property of the PrintDocument class.

Here's an example that illustrates how to initiate a print job using the base PrintDocument class:

PrintDocument printDoc = new PrintDocument(); printDoc.PrintPage += new PrintPageEventHandler(this.printDoc_PrintPage); printDoc.Print(); // The PrintPage event is raised for each page to be printed. private void printDoc_PrintPage(object sender, PrintPageEventArgs e) { // TODO: Print your page using the e.Graphics GDI+ object // Notify the PrintController whether there are any more pages e.HasMorePages = false; }

As you can see, there are numerous drawbacks to this approach. The biggest drawback is that you must maintain state-aware objects between subsequent calls to the PrintPage event handler. For example, if you're printing a text document you will need to hold onto an open StreamReader object. You could initialize the StreamReader during the BeginPrint event and then close it during the EndPrint event. No matter how you slice it, however, the StreamReader variable will need to be scoped outside of the PrintPage event handler along with your other variables. When this happens, your printing code is exposed and vulnerable and can muddy up the rest of your code.

Implementing a Derived PrintDocument Class

When printing from Windows Forms, a better approach is to implement a class that inherits from the generic PrintDocument class. Doing so, you'll quickly reap the rewards of encapsulation. Instead of implementing event handlers for the BeginPrint, EndPrint, and PrintPage events, you override the OnBeginPrint, OnEndPrint, and OnPrintPage methods of the underlying PrintDocument base class. Any state-aware objects used by the OnPrintPage method can now be kept in private class fields. This completely does away with the potential code problems I just mentioned. In addition, you are now free to add custom properties, methods, events, and constructors to your derived PrintDocument class.

The sample printing application uses a derived PrintDocument class of type TextPrintDocument, as shown in Figure 2. The TextPrintDocument class exposes an overloaded constructor that takes a file name as an argument. Alternatively, the file name can be set and read using a custom FileToPrint property. This property throws an exception when set to a nonexistent file. This class also exposes a public Boolean field, named Watermark, which enables or disables a page background graphic. (The page background graphic is stored in the assembly as an embedded resource called Watermark.gif.) Finally, the derived TextPrintDocument class exposes a Font property that specifies which font is the correct one to use when rendering the page.

Figure 2 Derived PrintDocument Class

using System; using System.Drawing; using System.Drawing.Printing; using System.IO; using System.Windows.Forms; namespace PrintingDemo { public class TextPrintDocument : PrintDocument { private Font printFont; private TextReader printStream; private string fileToPrint; private Bitmap imgWatermark; public bool Watermark = false; public TextPrintDocument() { imgWatermark = new Bitmap(GetType(), "Watermark.gif"); } public TextPrintDocument(string fileName) : this() { this.FileToPrint = fileName; } public string FileToPrint { get { return fileToPrint; } set { if (File.Exists(value)) { fileToPrint = value; this.DocumentName = value; } else throw(new Exception("File not found.")); } } public Font Font { get { return printFont; } set { printFont = value; } } protected override void OnBeginPrint(PrintEventArgs e) { base.OnBeginPrint(e); printFont = new Font("Verdana", 10); printStream = new StreamReader(fileToPrint); } protected override void OnEndPrint(PrintEventArgs e) { base.OnEndPrint(e); printFont.Dispose(); printStream.Close(); } protected override void OnPrintPage(PrintPageEventArgs e) { base.OnPrintPage(e); // Slow down printing for demo. System.Threading.Thread.Sleep(200); Graphics gdiPage = e.Graphics; float leftMargin = e.MarginBounds.Left; float topMargin = e.MarginBounds.Top; float lineHeight = printFont.GetHeight(gdiPage); float linesPerPage = e.MarginBounds.Height / lineHeight; int lineCount = 0; string lineText = null; // Watermark? if (this.Watermark) { int top = Math.Max(0, (e.PageBounds.Height - imgWatermark.Height) / 2); int left = Math.Max(0, (e.PageBounds.Width - imgWatermark.Width) / 2); gdiPage.DrawImage(imgWatermark, left, top, imgWatermark.Width, imgWatermark.Height); } // Print each line of the file. while (lineCount < linesPerPage && ((lineText = printStream.ReadLine()) != null)) { gdiPage.DrawString(lineText, printFont, Brushes.Black, leftMargin, (topMargin + (lineCount++ * lineHeight))); } // If more lines exist, print another page. if(lineText != null) e.HasMorePages = true; else e.HasMorePages = false; } } }

The guts of the TextPrintDocument class can be found in the OnPrintPage method. This is where you paint your page using the GDI+ drawing surface provided by the PrintPageEventArgs Graphics property. In addition, the PrintPageEventArgs object contains the following properties: Cancel, HasMorePages, MarginBounds, PageBounds, and PageSettings. Cancel allows you to cancel the print job. The MarginBounds property returns a Rectangle object representing the portion of the page within the margins. You can use this rectangle to determine where to start and stop your printing on each page. PageBounds, on the other hand, represents the total area of the page, including the margins.

Printing with GDI+

The particulars of GDI+ would require an article in and of itself, so for the purposes of this article, I will only cover GDI+ from the perspective of how the TextPrintDocument class renders each page. I will then discuss some of the GDI+ calls you're most likely to make during the typical printing process.

First, the OnPrintPage method determines the height of the current font using the GetHeight method of the Font class. The GetHeight method can determine the dots per inch (dpi) of the current page by taking a Graphics object as an argument. Once the height of the current font is determined, the number of lines per page is calculated using the current MarginBounds.Height. Next, the text file is read, one line at a time, and printed using the DrawString method. If the end of the page is reached before the end of file, the HasMorePages property is set to true.

As you can see, basic printing can be accomplished simply by using DrawString. GDI+, however, arms you with more than 15 draw methods, each with numerous overloads. You can print using both vector graphics (such as DrawBezier, DrawEllipse) and raster graphics (such as DrawImage, DrawIcon). Notice how the OnPrintPage method uses DrawImage to display the watermark. You can also take advantage of the many advanced features of GDI+, such as clipping and transformations. Try doing that with the Visual Basic Print object!

The PrintController Class

Earlier, I mentioned how the sample printing application (shown in Figure 1) allows you to display an optional status dialog and/or animated status bar icon (the kind that spits out pages while printing). Both of these features are implemented using derived print controller classes. PrintController is an abstract class that is implemented by three different concrete classes within the .NET Framework: StandardPrintController, PrintControllerWithStatusDialog, and PreviewPrintController.

The print controller is responsible for how a print document is printed. The PrintDocument class exposes its underlying print controller as a property. Calling a print document's Print method triggers the underlying print controller's OnStartPrint, OnEndPrint, OnStartPage, and OnEndPage methods. Figure 3 shows the sequence of events that occur between the print document and the print controller. OnStartPage is the only method that returns a value. The return value is of type Graphics and, as you may have already guessed, is the GDI+ drawing surface that is passed to the print document via the PrintPageEventArgs argument.

Figure 3 Print Flow

Figure 3** Print Flow **

The default print controller is of type PrintControllerWithStatusDialog. So, if you want to turn off the print status dialog, you'll need to use the StandardPrintController. The PreviewPrintController is used by the PrintPreviewDialog and PrintPreviewControl classes. PrintControllerWithStatusDialog can be found in the System.Windows.Forms namespace, while StandardPrintController and PreviewPrintController are located under the System.Drawing.Printing namespace. The PrintControllerWithStatusDialog provides an overloaded constructor that takes another print controller as an argument. This allows you to combine the PrintControllerWithStatusDialog with any additional features that you might add to your own print controller. When running the sample printing application, try checking both the PrintControllerWithStatusDialog and PrintControllerWithStatusBar checkboxes to see this in action. Here is a snippet of how the code works:

CustomPrintDocument printDoc = new CustomPrintDocument(); CustomPrintController printCtl = new CustomPrintController(); printDoc.PrintController = new PrintControllerWithStatusDialog( printCtl, "Dialog Caption"); printDoc.Print();

The sample printing application uses a custom print controller of type PrintControllerWithStatusBar (see Figure 4). PrintControllerWithStatusBar exposes a StatusBarPanel property that determines which status bar panel should display the animated printer icon. I used a timer of type System.Timers.Timer in order to do the actual animation. The System.Timers.Timer class works very well in a multithreaded application, as is the case when doing background printing.

Figure 4 PrintControllerWithStatusBar

using System; using System.Drawing; using System.Drawing.Printing; using System.Timers; using System.Windows.Forms; namespace PrintingDemo { public class PrintControllerWithStatusBar : StandardPrintController { private int iCurrentPage; private int iCurrentIconFrame; private string strPriorStatus; private Icon icoPriorIcon; private Icon[] icoPrinters; private StatusBarPanel statusBarPanel; private System.Timers.Timer tmrIconAnimation; public bool ShowPrinterIcon = false; public PrintControllerWithStatusBar() { Icon icoPrinter1 = new Icon(GetType(), "Printer1.ico"); Icon icoPrinter2 = new Icon(GetType(), "Printer2.ico"); Icon icoPrinter3 = new Icon(GetType(), "Printer3.ico"); icoPrinters = new Icon[3]; icoPrinters[0] = icoPrinter1; icoPrinters[1] = icoPrinter2; icoPrinters[2] = icoPrinter3; tmrIconAnimation = new System.Timers.Timer(); tmrIconAnimation.Enabled = false; tmrIconAnimation.Interval = 200; tmrIconAnimation.Elapsed += new ElapsedEventHandler(tmrIconAnimation_Elapsed); } public PrintControllerWithStatusBar(StatusBarPanel sbp) : this() { statusBarPanel = sbp; } private void tmrIconAnimation_Elapsed(object sender, ElapsedEventArgs e) { if (statusBarPanel != null) { // Animate printer icon... statusBarPanel.Icon = icoPrinters[iCurrentIconFrame++]; if (iCurrentIconFrame > 2) iCurrentIconFrame = 0; } } public StatusBarPanel StatusBarPanel { get { return statusBarPanel; } set { statusBarPanel = value; } } public override void OnStartPrint(PrintDocument printDoc, PrintEventArgs e) { iCurrentPage = 1; iCurrentIconFrame = 0; if (statusBarPanel != null) { // Save prior settings... strPriorStatus = statusBarPanel.Text; icoPriorIcon = statusBarPanel.Icon; statusBarPanel.Text = "Printing..."; } if (this.ShowPrinterIcon) { tmrIconAnimation.Start(); } base.OnStartPrint(printDoc, e); } public override Graphics OnStartPage(PrintDocument printDoc, PrintPageEventArgs e) { if (statusBarPanel != null) { statusBarPanel.Text = "Printing page " + iCurrentPage++; } return base.OnStartPage(printDoc, e); } public override void OnEndPage(PrintDocument printDoc, PrintPageEventArgs e) { base.OnEndPage(printDoc, e); } public override void OnEndPrint(PrintDocument printDoc, PrintEventArgs e) { tmrIconAnimation.Stop(); // DoEvents is required here, when not printing in a // background thread. It allows any pending messages to be // processed prior to restoring the StatusBar back to its // original settings. Application.DoEvents(); if (statusBarPanel != null) { // Restore original panel settings... statusBarPanel.Icon = icoPriorIcon; statusBarPanel.Text = strPriorStatus; } base.OnEndPrint(printDoc, e); } } }

Using the Print Dialogs

The beauty of printing in .NET is the way that the print document so elegantly fits together with the print dialogs. There are three different print dialogs in the System.Windows.Forms namespace: PrintDialog, PrintPreviewDialog, and PageSetupDialog. In addition, there is a PrintPreviewControl class that doesn't include the surrounding dialog, providing you with greater UI design flexibility. For simplicity, the sample printing application uses the PrintPreviewDialog class (see Figure 5).

Figure 5 Print Preview

Figure 5** Print Preview **

The PrintDialog class can be used to display the standard Print dialog in Windows and gives the user the ability to select a printer, specify which pages to print, and determine the number of copies. Using it is as simple as this:

CustomPrintDocument printDoc = new CustomPrintDocument(); PrintDialog dlgPrint = new PrintDialog(); dlgPrint.Document = printDoc; if (dlgPrint.ShowDialog() == DialogResult.OK) { printDoc.Print(); }

Using the PrintPreviewDialog class is even easier. I found the Print Preview dialog to be extremely useful during the development process. It can save you a lot of paper when debugging your print documents. Here's how it works:

CustomPrintDocument printDoc = new CustomPrintDocument(); PrintPreviewDialog dlgPrintPreview = new PrintPreviewDialog(); // Set any optional properties of dlgPrintPreview here... dlgPrintPreview.Document = printDoc; dlgPrintPreview.ShowDialog();

After you create an instance of the PrintPreviewDialog class, you set its Document property to an instance of any class that derives from PrintDocument. The ShowDialog method of the PrintPreviewClass will automatically take care of rendering a print preview of your document (see Figure 5). As you know, it doesn't get much simpler than that.

The PageSetupDialog class works in a similar fashion. However, in addition to supporting a Document property, you can opt to set the PageSettings or PrinterSettings property to an instance of a PageSettings or PrinterSettings class. The PageSettings class defines settings that apply to the actual printed page, such as Margins and PaperSize, while the PrinterSettings class specifies information about how a document is printed, such as FromPage, ToPage, and PrinterName. The PrinterSettings class also allows you to obtain a list of available printers using the InstalledPrinters static method, which returns a PrinterSettings.StringCollection object. Upon returning from the Page Setup dialog, the Document, PageSettings, or PrinterSettings object will be modified accordingly.

Background Printing

Threading is a tricky thing, and by no means do I intend to deny the tendency for issues to arise when writing a multithreaded application. The truth is, though, that the use of a background thread really improves the print experience for users. It allows your users to continue to use the rest of your application while the print job is processing in the background. To experiment with background printing, it's best to use a large document and to make sure that your printer is paused. Why waste the paper? You can also slow down printing using the static Sleep method of the Thread class. The sample printing application lets you try this out when sending output to the printer. It doesn't make sense to use background printing with the Print Preview dialog. In a nutshell, here is how it works:

private void cmdBackgroundPrint_Click(object sender, System.EventArgs e) { Thread t = new Thread(new ThreadStart(PrintInBackground)); t.IsBackground = true; t.Start(); } private void PrintInBackground() { printDoc.Print(); }

You can also use a delegate to accomplish the same thing. Initially, when I wrote the sample printing application, I used the Thread class. However, be-cause I wanted to be informed when the printing was complete, I decided to use a delegate instead. Using a delegate's BeginInvoke method allows you to specify a callback function so that your code can be notified when an asynchronous operation has completed. I needed to do this in order to re-enable the Print button in a thread-safe manner after printing was completed (see Figure 6).

Figure 6 Sample Printing Application Code

using System; using System.Data; using System.Drawing; using System.Drawing.Printing; using System.ComponentModel; using System.Threading; using System.Windows.Forms; namespace PrintingDemo { public class PrintDialog : System.Windows.Forms.Form { private System.Windows.Forms.Button cmdPrint; private System.Windows.Forms.Label lblFile; private System.Windows.Forms.TextBox txtFileName; private System.Windows.Forms.Button cmdBrowse; private System.Windows.Forms.OpenFileDialog dlgOpenFile; private System.Windows.Forms.PrintPreviewDialog dlgPrintPreview; private System.ComponentModel.Container components = null; private System.Windows.Forms.PrintDialog dlgPrint; private System.Windows.Forms.CheckBox chkBackgroundThread; private System.Windows.Forms.RadioButton optPrinter; private System.Windows.Forms.RadioButton optPrintPreview; private System.Windows.Forms.Button cmdPageSettings; private System.Windows.Forms.PageSetupDialog dlgPageSettings; private System.Windows.Forms.CheckBox chkWatermark; private System.Windows.Forms.GroupBox fraSettings; private System.Windows.Forms.CheckBox chkAntiAlias; private System.Windows.Forms.StatusBar sbStatus; private System.Windows.Forms.StatusBarPanel simpleTextPanel; private System.Windows.Forms.CheckBox chkPrintControllerWithStatusBar; private System.Windows.Forms.CheckBox chkPrintControllerWithStatusDialog; private TextPrintDocument printDoc = new TextPrintDocument(); delegate void PrintInBackgroundDelegate(); public PrintDialog() { InitializeComponent(); dlgPrintPreview.Icon = this.Icon; } protected override void Dispose( bool disposing ) { if( disposing ) { if (components != null) { components.Dispose(); } } base.Dispose( disposing ); } #region Windows Form Designer generated code [STAThread] static void Main() { Application.Run(new PrintDialog()); } private void cmdBrowse_Click(object sender, System.EventArgs e) { if (dlgOpenFile.ShowDialog(this) == DialogResult.OK) txtFileName.Text = dlgOpenFile.FileName.ToString(); } private void optPrintPreview_CheckedChanged(object sender, System.EventArgs e) { EnableDisableCheckBoxes(); } private void optPrinter_CheckedChanged(object sender, System.EventArgs e) { EnableDisableCheckBoxes(); } private void EnableDisableCheckBoxes() { chkAntiAlias.Enabled = optPrintPreview.Checked; chkPrintControllerWithStatusDialog.Enabled = optPrinter.Checked; chkPrintControllerWithStatusBar.Enabled = optPrinter.Checked; chkBackgroundThread.Enabled = optPrinter.Checked; } private void cmdPageSettings_Click(object sender, System.EventArgs e) { dlgPageSettings.Document = this.printDoc; dlgPageSettings.ShowDialog(this); } private void cmdPrint_Click(object sender, System.EventArgs e) { try { printDoc.FileToPrint = txtFileName.Text; printDoc.Watermark = chkWatermark.Checked; if (optPrintPreview.Checked) { dlgPrintPreview.UseAntiAlias = chkAntiAlias.Checked; dlgPrintPreview.Document = printDoc; dlgPrintPreview.ShowDialog(this); } else if (optPrinter.Checked) { dlgPrint.Document = printDoc; if (dlgPrint.ShowDialog(this) == DialogResult.OK) { this.Refresh(); PrintController printController; if (chkPrintControllerWithStatusBar.Checked) { printController = new PrintControllerWithStatusBar(); ((PrintControllerWithStatusBar) printController) .StatusBarPanel = sbStatus.Panels[0]; ((PrintControllerWithStatusBar) printController) .ShowPrinterIcon = true; } else printController = new StandardPrintController(); if (chkPrintControllerWithStatusDialog.Checked) printDoc.PrintController = new PrintControllerWithStatusDialog( printController, "Please wait..."); else printDoc.PrintController = printController; if (chkBackgroundThread.Checked) { cmdPrint.Enabled = false; cmdPageSettings.Enabled = false; fraSettings.Enabled = false; PrintInBackgroundDelegate d = new PrintInBackgroundDelegate(PrintInBackground); d.BeginInvoke(new AsyncCallback(PrintInBackgroundComplete), null); } else { this.Cursor = Cursors.WaitCursor; printDoc.Print(); this.Cursor = Cursors.Default; } } } } catch (Exception ex) { MessageBox.Show(ex.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void PrintInBackground() { try { printDoc.Print(); } catch (Exception e) { MessageBox.Show(e.Message, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void PrintInBackgroundComplete(IAsyncResult r) { cmdPrint.Enabled = true; cmdPageSettings.Enabled = true; fraSettings.Enabled = true; } } }

Figure 6 contains most of the code used to drive the sample printing application. In particular, it contains a delegate called PrintInBackgroundDelegate. The delegate's BeginInvoke method is called inside the Print button's Click event handler if the Background thread checkbox is checked. Otherwise, the print document's Print method is called directly from the main UI thread.

Any pitfalls you may encounter will most likely involve state-aware objects your print document class may rely upon. For instance, using the prior example, what happens if the user clicks cmdBackgroundPrint more than once? The results would be unpredictable at best. Because the PrintDocument.PrintPage event will often rely on state-aware class fields, things can get really messy in a case like this. An easy way to deal with these types of issues is by simply disabling some UI controls to prevent the user from reissuing the same command. In the meantime, they can continue to use the rest of the app. This is the approach the sample printing app uses. Before you tackle background printing, I recommend that you read Ian Griffiths' article on page 68 in this issue.

Conclusion

While this article covered native Windows Forms printing in .NET, there are other printer output methods available for .NET, such as Crystal Reports. In certain situations, a reporting tool may be more appropriate than native printing. However, if you don't want the overhead of an off-the-shelf product and your application requires custom or proprietary printing capabilities—like those found in many desktop Windows-based apps—native printing is definitely the way to go.

For related articles see:
Safe, Simple Multithreading in Windows Forms

Alex Calvois an MCSD and MCAD for Microsoft .NET. He is the co-founder of Developer Box LLC (https://www.developerbox.com), a Conn. consulting company specializing in .NET, Visual Basic, COM, and SQL Server. You can reach Alex at acalvo@hotmail.com.