Introduction
This printing utility is a combination of two efforts. It has the ability to print the content of user controls and it is, moreover, an attempt to create an "all-in-one" printing utility. The latter is an integration into a single control of what is otherwise spread across several standard dialogs for printer properties, page setup options and print previewing.
Background
Grateful use is made of the following article here on CodeProject, authored by Nader Elshehabi: A component that prints any control... I needed to print the content of a treeview, amongst other things, and the above solution proved to do a fine job. I did not extend very much on it, but instead wrapped it with a printing utility that integrates the facilities of the System.Drawing.Printing
namespace into a PropertyGrid.
Another incentive for this control is the variety of printing dialogs out there. If you look at the printing tools supplied with the purchase of an average printer, their appearance, non-standardisation and staggering complexity are, exceptions aside, overwhelming, intimidating and fundamentally scary. They definitely do not help people to understand their printers.
This is not an approach with a juicy UI-appeal, but that was not intended. I'm somewhat devoted to the power of PropertyGrid leveraged by the System.ComponentModel
namespace. I believe that effective designer grids can even appeal to end users, provided that added value is somewhere in the equation.
Using the code
The solution contains two projects:
Codebasement.PrintControl
: the printing utility user control and supporting classes
Codebasement.PrintControl.Test
: a simple test form
The printing utility project contains several classes, which will be commented on below.
PrintDocumentControl
is the actual user control as illustrated in the partial image above. It contains tool strips for printing and preview manipulation, a property grid for the various settings and PrintPreviewControl
for displaying previews. This latter control, by the way, has to be used programmatically. It will not do its job if used as a design-time drag and drop control. PrintDocumentControl
can be used as-is and has a simple constructor that only needs to be provided with the target user control to print.
The control checks whether the Print Spooler service is running and, if so, also checks whether there is a printer installed. It wires the various elements of the control together and contains the methods that do the actual work, notably ShowPreview()
and PrintNow()
. I could have splattered this functionality across other classes, but decided to take the simple route and take all that up in the control itself. Since the control is intended to be used singularly in a dialog, there is a PrintDocumentForm
in the project that already contains the control.
The ShowPreview()
method is described below. It utilizes the classes discussed further on, assigning properties to PrintDocumentComponent
-- i.e. the _printDocument
member -- to be previewed. It creates a PreviewPrintController
, which is not to be confused with the PrintPreviewControl
user control, and assigns this controller to the document. Finally, it hands the document to the preview user control.
The PreviewToolStrip
, incidentally, has several functions that you would expect:
- (Re)Show preview
- Zoom in/out
- Fit to page
- Set zoom factor
- Step through pages
- Multi-page view
private void ShowPreview()
{
Cursor = Cursors.WaitCursor;
NewPrintPreviewControl();
_printDocument.DocumentName = _printDocumentSettings.HeaderText;
_printDocument.DefaultPageSettings = _printDocumentSettings.PageSettings;
_printDocument.PrinterSettings = _printDocumentSettings.PrinterSettings;
_printDocument.OriginAtMargins = _printDocumentSettings.OriginAtMargins;
PreviewPrintController pc = new PreviewPrintController();
pc.UseAntiAlias = true;
_printDocument.PrintController = pc;
_printPreviewControl.Document = _printDocument;
_printPreviewControl.StartPage = 0;
_printPreviewControl.UseAntiAlias = true;
_pageCounter = 1;
SetPreviewToolStrip();
Cursor = Cursors.Default;
}
The PrintNow()
method is similar. It also assigns PrinterSettings
and PageSettings
assembled in PrintDocumentSettings
to PrintDocumentComponent
. Then, using PrintControllerWithStatusDialog
as a print controller as before, the actual Print()
method is ultimately called upon.
private void PrintNow()
{
DialogResult result;
if (_printDocumentSettings.PrintToFile)
{
result =
MessageBox.Show(
"The document " + _printDocument.DocumentName +
" will be saved to file " +
_printDocumentSettings.FileName +
". Continue?",
"Print to file...",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2,
0);
if (result == DialogResult.Yes)
{
PrintToFile();
return;
}
}
result =
MessageBox.Show(
"The document " + _printDocument.DocumentName +
" from page " + _printDocumentSettings.FromPage +
" to page " + _printDocumentSettings.ToPage +
" will be sent to printer " +
_printDocumentSettings.SelectedPrinter +
". Continue?",
"Print to printer...",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question,
MessageBoxDefaultButton.Button2,
0);
if (result == DialogResult.Yes)
{
_printDocument.PrinterSettings =
_printDocumentSettings.PrinterSettings;
_printDocumentSettings.PageSettings.PrinterSettings =
_printDocument.PrinterSettings;
_printDocument.DefaultPageSettings =
_printDocumentSettings.PageSettings;
StandardPrintController spc = new StandardPrintController();
_printDocument.PrintController =
new PrintControllerWithStatusDialog(spc,
"Printing "+_printDocument.DocumentName + " to " +
_printDocumentSettings.SelectedPrinter);
_printDocument.Print();
}
}
PrintDocumentSettings
is a class that supplies a number of properties taken from the System.Drawing.Printing
namespace. The idea behind it is to simplify access to this namespace by bundling the most immediately relevant contents, while on the other hand making this content "PropertyGrid-aware." This is best illustrated by an example: the SelectedPinter
property in the snippet shown below gets/sets a member field and adjusts a PrinterSettings
member instance in which the printer settings are collected for use when the ShowPreview()
or PrintNow()
methods are called.
The attributes used are mostly standard, but they do provide added value for the PropertyGrid used. I will not address the application of type converters here in-depth. As an example, the PrinterSelectionConverter
used below is an included custom type converter class that provides combo fill for the property when used in the grid to enable selecting a printer. So the return type is simply a string
, but the type converter provides a little magic under the cover.
[Category(catPrinter)]
[TypeConverter(typeof(PrinterSelectionConverter))]
[RefreshProperties(RefreshProperties.All)]
[Description("Choice for the selected printer.")]
[DisplayName("Selected printer")]
public string SelectedPrinter
{
get
{
return _selectedPrinter;
}
set
{
_selectedPrinter = value;
_printerSettings.PrinterName = value;
}
}
Another example is the PaperKind
property. The setter parses the input value to a string value of the related enum. The PaperSize
of the _pageSettings member is then assigned to a PaperSize instance with this PaperKind, and otherwise to a default by the custom SetPaperSize
method. In the getter, an inline validation of the PaperKind is carried out to ensure that the value corresponds to a valid value of the paper size collection supported by the selected printer. This collection is again provided by a type converter. In this case, it's PaperKindSelectionConverter
. Simple code, essentially, but it results in powerful functionality by combining programmatic and declarative elements.
[Category(catPaper)]
[DefaultValue("A4")]
[Description("Paper size kind for selected printer.")]
[DisplayName("Paper size kind")]
[TypeConverter(typeof(PaperKindSelectionConverter))]
public string PaperKind
{
get
{
return ValidatePaperSize(_paperKind).ToString();
}
set
{
if (value != null)
{
_paperKind =
(PaperKind) Enum.Parse(typeof (PaperKind),
value.ToString());
_pageSettings.PaperSize = SetPaperSize(_paperKind);
}
}
}
The document settings further provide several options for printing header and footer info along with the control content. Text options, page numbering and a datetime can be included. Alignment, font and color preferences can also be set.
PrintDocumentComponent
is the derivative of the PrintDocument
base class taken from the article mentioned above. I will therefore not go into detail here about its contents. The fundamental ingredients is the CalculateSize
method, which provides sizing of the user control content that is to be printed. In here, several controls are given precise size calculations. Otherwise, GetPreferredSize
is the default. The other crucial part is the OnPrintPage
event handler. This takes care of the actual printing of bitmaps per page, taking an overlap area into account. Optionally, it stretches the target control to print and sizes it to the required dimensions. I mixed in the printing of header and footer texts as a minor extension.
Points of interest
The user controls covered currently in the PrintDocumentComponent
class are limited to only a few specific ones, with a default sizing for any other. It is important that the target user control support the DrawToBitmap
method. For this reason, not all user controls can be printed with the above utility. See the original article -- and specifically also the related message threads -- referenced above in the Background paragraph for more details concerning this PrintDocument
derivative.
Printing the content of specific controls might require some addition to the PrintDocumentComponent.CalculateSize
method, whereby you supply or calculate the height and width of the control content. Also, you might want to adjust the behaviour of expansions and the like, as the content printing is initially a WYSIWYG view. For a tree view, no expansion of nodes is done within CalculateSize. By contrast, for the printing of a PropertyGrid's contents, the grid items are expanded there. It illustrates the difference and adjusts it to your needs. This behaviour could also be integrated into the PrintDocumentSettings
class as a selectable option.
My experience in using the System.Drawing.Printing
namespace is that it's not an immediate friend. This namespace could be greatly improved to a true-blood object model, abstracting the current diversity and complexity. Who knows what may happen in the.NET 4.X future or so.
Known issues and improvement points
- I tried cloning the target user control to avoid having to optionally resize the target control -- as is done in the solution mentioned above that I used for this purpose -- but that didn't help. Fiddling around with the original controls is not a very elegant approach, so that is definitely a point still to be improved.
- The page range selection does not work adequately; i.e. when selecting a smaller range, the printer settings don't pick that up. I have no clue as yet why that doesn't work.
- The property grid might be somewhat slack in performance, depending on circumstances. The problem here is that I use some inline validation in a few getters, as well as an attribute like
RefreshProperties
, which is very useful but tends to downgrade performance.
- The date format optionally printed is currently fixed to a format with an ISO-standard character-based month abbreviation. The idea is that it cannot easily be misunderstood. Of course, as a European I'm more inclined to find logic in a day-month-year sequence. ;-)
- I put some effort into getting the solution "resharper-green," but this will depend on the Resharper build used. It is also, to a large extent, static-code-analysis-compliant.
History
- 15 June, 2007 -- Original version posted
- 11 July, 2007 -- Article edited and moved to the main CodeProject.com article base