So far, all the printing in this chapter has been done to the default printer, as defined by Windows. The user can change the printer for a document via the printer button on the PageSetupDialog. It's more common, however, to allow the user to choose the printer after choosing the Print item from the File menu. For this you use the PrintDialog component, from the System.Windows.Forms namespace, as shown in Figure 8.10. Figure 8.10. The PrintDialog Component
Here's how you use the PrintDialog component: // MainForm.Designer.cs partial class MainForm { ... PrintDialog printDialog; ... void InitializeComponent() { ... this.printDialog = new PrintDialog(); ... } } // MainForm.cs partial class MainForm : Form { ... void printButton_Click(object sender, EventArgs e) { // Let the user choose the printer this.printDialog.Document = this.printDocument; if( this.printDialog.ShowDialog() == DialogResult.OK ) { this.printDocument.DocumentName = fileName; this.printDocument.Print(); } } } Like PageSetupDialog, the PrintDialog component allows you to set a number of options before it is shown:
namespace System.Windows.Forms { sealed class PrintDialog : CommonDialog { // Methods public PrintDialog(); public override void Reset(); // Properties public bool AllowCurrentPage { get; set; } // New public bool AllowPrintToFile { get; set; } public bool AllowSelection { get; set; } public bool AllowSomePages { get; set; } public PrintDocument Document { get; set; } public PrinterSettings PrinterSettings { get; set; } public bool PrintToFile { get; set; } public bool ShowHelp { get; set; } public bool ShowNetwork { get; set; } public bool UseEXDialog { get; set; } // New // Events public event EventHandler HelpRequest; } } You must set the Document property before showing a PrintDialog object. The UseEXDialog property can be set to true if you prefer to display the extended, better-looking Print dialog shown in Figure 8.11[3]
Figure 8.11. The Extended PrintDialog Component
The other PrintDialog properties are similar in function to the PageSetupDialog properties. A couple of properties are special, however, because they determine what to print. Let's take a look. Print RangeThe AllowSelection property of PrintDialog lets the user print only the current selection, and AllowSomePages allows the user to decide on a subset of pages to be printed.[4] Both settings require you to print specially, based on the PrintRange property of the PrinterSettings class (discussed in a moment), which is of type PrintRange:
namespace System.Drawing.Printing { enum PrintRange { // Fields AllPages = 0, // Print all pages (default) Selection = 1, // Print only the current selection SomePages = 2, // Print pages from FromPage to ToPage CurrentPage = 4194304 // Print the current page (New) } } Before you can set a print range that's different from AllPages, you must set AllowSelection or AllowSomePages (or both) to true (they both default to false). AllowSomePages also requires that the PrinterSettings' FromPage and ToPage be set greater than the default of zero: int totalPages = 13; int page; void printButton_Click(object sender, EventArgs e) { // Let the user choose the printer this.printDocument.PrinterSettings.FromPage = 1; this.printDocument.PrinterSettings.ToPage = totalPages; this.printDocument.PrinterSettings.MinimumPage = 1; this.printDocument.PrinterSettings.MaximumPage = totalPages; this.printDialog.AllowSomePages = true; this.printDialog.Document = this.printDocument; if( this.printDialog.ShowDialog() == DialogResult.OK ) { this.printDocument.DocumentName = fileName; this.printDocument.Print(); } } When you set AllowSomePages to true, it's a good idea to also set MinimumPage and MaximumPage; while not required, this prevents users from accidentally asking for a page out of the allowed range. If AllowSelection or AllowSomePages is set to true, the PrintPage event must check the PrintRange and FromPage and ToPage properties to see what to print: int totalPages = 13; int page; int maxPage; void printButton_Click(object sender, EventArgs e) { ... if( this.printDialog.ShowDialog() == DialogResult.OK ) { if( this.printDialog.PrinterSettings.PrintRange == PrintRange.SomePages ) { // Set first page to print to FromPage page = this.printDocument.PrinterSettings.FromPage; // Set last page to print to ToPage maxPage = this.printDocument.PrinterSettings.ToPage; } else { // Print all pages page = 1; maxPage = totalPages; } // Print from first page to last page this.printDocument.DocumentName = fileName; this.printDocument.Print(); } } void printDocument_PrintPage(object sender, PrintPageEventArgs e) { // Draw to the e.Graphics object that wraps the print target Graphics g = e.Graphics; // Print current page ... // Check whether there are more pages to print ++page; e.HasMorePages = ( page <= maxPage ); } In addition to the PrintRange, FromPage, and ToPage properties, the PrinterSettings class has many more settings for use in determining exactly how the user would like to print:
namespace System.Drawing.Printing { class PrinterSettings : ICloneable { // Properties public bool CanDuplex { get; } public bool Collate { get; set; } public short Copies { get; set; } public PageSettings DefaultPageSettings { get; } internal string DriverName { get; } public Duplex Duplex { get; set; } public int FromPage { get; set; } public static StringCollection InstalledPrinters { get; } public bool IsDefaultPrinter { get; } public bool IsPlotter { get; } public bool IsValid { get; } public int LandscapeAngle { get; } public int MaximumCopies { get; } public int MaximumPage { get; set; } public int MinimumPage { get; set; } public PaperSizeCollection PaperSizes { get; } public PaperSourceCollection PaperSources { get; } public string PrinterName { get; set; } public PrinterResolutionCollection PrinterResolutions { get; } public string PrintFileName { get; set; } // New public PrintRange PrintRange { get; set; } public bool PrintToFile { get; set; } public bool SupportsColor { get; } public int ToPage { get; set; } // Methods public PrinterSettings(); public object Clone(); public Graphics CreateMeasurementGraphics(...); // New public IntPtr GetHdevmode(...); public IntPtr GetHdevnames(); public bool IsDirectPrintingSupported(...); // New public void SetHdevmode(IntPtr hdevmode); public void SetHdevnames(IntPtr hdevnames); } } Of particular interest is the CreateMeasurementGraphics method, which returns a Graphics object based on the printer and its settings. You can use this Graphics object for making measurement calculations and for enumerating the font families (using the FontFamily.GetFamilies method), all without having to actually start a print operation. PrinterSettings represents the last major piece of the printing infrastructure, which includes print controllers, print documents, and page settings. Figure 8.12 illustrates the basic relationship between them. Figure 8.12. Relationship Between the Major Elements of the Printing Infrastructure
Targeting the PrinterRecall that because the drawing happens on a Graphics object, all the drawing techniques from Chapter 5, Chapter 6, and Chapter 7 work just as well with printers as they do with screens. However, unlike the screen, where page units default to Pixel, the page units for the printer default to Display. Furthermore, whereas Display means Pixel on the screen, Display maps the printer resolution to a logical 100 dpi for the printer. Because printers often have different resolutions both vertically and horizontally and are almost never 100 dpi anyway, this may seem unintuitive. However, the default system font setting is 96 dpi on the screen, so mapping the printer to a logical 100 dpi means that the default mappings for both screen and printer yield a quick and dirty near-WYSIWYG without your having to change a thing. If you want something even closer, you're free to use page units such as inches or millimeters, as discussed in Chapter 7. If you do change the units, remember to convert PageBounds and MarginBounds to the new units as well. You can use the Graphics method TransformPoints: static RectangleF TranslateBounds(Graphics g, Rectangle bounds) { // Translate from units of 1/100 inch to page units float dpiX = g.DpiX; float dpiY = g.DpiY; PointF[] pts = new PointF[2]; pts[0] = new PointF(bounds.X * dpiX / 100, bounds.Y * dpiY / 100); pts[1] = new PointF( bounds.Width * dpiX / 100, bounds.Height * dpiX / 100); g.TransformPoints(CoordinateSpace.Page, CoordinateSpace.Device, pts); return new RectangleF(pts[0].X, pts[0].Y, pts[1].X, pts[1].Y); } The TranslateBounds helper method uses the current Graphics object to translate a PageBounds or MarginBounds rectangle from units of 100 dpi to whatever the page unit is set to. This helper is meant to be used from the PrintPage handler: void printDocument_PrintPage(object sender, PrintPageEventArgs e) { // Draw to the e.Graphics object that wraps the print target Graphics g = e.Graphics; g.PageUnit = GraphicsUnit.Inch; ... using( Pen thinPen = new Pen(Color.Black, 0) ) { RectangleF pageBounds = GetRealPageBounds(e, preview); pageBounds = TranslateBounds(g, Rectangle.Truncate(pageBounds)); g.DrawRectangle( thinPen, pageBounds.X, pageBounds.Y, pageBounds.Width, pageBounds.Height); ... } ... } Notice that PageUnit is set on the Graphics object to the appropriate GraphicsUnit enumeration value right away so that any drawing that takes place in the PrintPage handler is in the specified unit. Notice also the creation of a new Pen object with a thickness of zero. By default, all the pens exposed from the Pens class have a width of 1, which is 1 unit thick, or, in this example, 1 inch thick. A Pen of width zero, on the other hand, is always 1 device unit thick, something that is more useful for framing a rectangle. Finally, notice that the PrintPage handler sets the PageUnit during each page being printed. Each time the PrintPage handler is called, it gets a fresh Graphics object, so don't forget to set its options every time. |