Zoomprint is a reusable Windows Forms component in DLL form that emulates the print preview dialogs found in commercial software. It can be used to print rich text from any C# application such as an editor, report generator, etc. The form accurately displays what the printed pages will look like, updating the preview depending on the printer selection, page size, orientation, margins and zoom level, if the printer supports zooming. It also allows direct access to the built-in settings page of the print driver.
Introduction
This project evolved from my first attempt to create a useful print preview / print dialog for a rich text box in C#, something which is not provided by the standard Visual Studio tools or components.
While the simple dialog I created above worked well, I wanted something closer to what is found in most commercial software, such as Open Office or Acrobat. Specifically, I wanted a dialog that would accurately display what the printed pages will look like, changing the pagination in real-time depending on the printer chosen and whatever features are selected by the user. This includes the ability to zoom the pages, re-paginating accordingly. Multiple pages per sheet can also be printed, if supported. Since not all printers support all features, a requirement was the enumeration of features available in the print driver selected.
Background
While NET offers minimal support for printing from a richtextbox
, Windows Presentation Foundation Classes offer support for viewing and printing the FlowDocument
Class, which I found was the best way to work with the Rich Text markup code (RTF) produced by the RichTextBox
and any extended class based on it, such as an editor. Rtf can be readily converted into a FlowDocument
.
Using the Code
The included source code generates a class library zoomprint.dll that contains a ZPrintForm
class with three constructors. The first creates a Form
with a Demo
Rtf document to show how it looks. In the second override, the String "Document
" is the Rtf from a richtextbox
. The third form allows the programmer to preselect default options for the dialog. They can still be changed by the User before printing.
public ZprintForm()
public ZPrintForm(string Document)
public ZPrintForm(string Document,string Header,bool NumberPages,
bool IncludeDate, int PointSize)
ZPrintForm ZPF = new ZPF(myRichTextBox.Rtf);
ZPF.ShowDialog(this);
ZPF.Dispose();
Points of Interest
The first step is to convert the Rtf into a FlowDocument
and initialize it, using these two methods:
private bool CreateFlowDocFromRtf(ref FlowDocument f, string sourcertf)
{
bool success = false;
TextRange r = new TextRange(f.ContentStart, f.ContentEnd);
if (r.CanLoad(System.Windows.Forms.DataFormats.Rtf) &&
(!string.IsNullOrEmpty(sourcertf)))
{
byte[] data = Encoding.ASCII.GetBytes(sourcertf);
using (MemoryStream stream = new MemoryStream(data))
{
r.Load(stream, System.Windows.Forms.DataFormats.Rtf);
}
success = true;
}
else
{
success = false;
}
return success;
}
private void InitializeFlowDocument(ref FlowDocument fd,
ref System.Windows.Forms.RichTextBox rtb)
{
string richtext = rtb.Rtf;
fd = new FlowDocument();
fd.PageWidth = _printableareawidth * (100 / fitfactor);
fd.PageHeight = _printableareaheight * (100 / fitfactor);
fd.ColumnWidth = Double.PositiveInfinity;
fd.PagePadding = new System.Windows.Thickness
(_marginleft, _margintop, _marginright, _marginbot);
fd.TextAlignment = System.Windows.TextAlignment.Left;
if (!CreateFlowDocFromRtf(ref fd, richtext))
{
System.Windows.Forms.MessageBox.Show(this,"Unable
to create flowdocument from Rtf", "Initialize FlowDocument Error",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
return;
}
GetPageCount(ref fd);
this.userControl11.ViewerZoom = fitfactor + 50;
if (_printersupportspagescaling)
{
this.userControl11.ViewerZoom = tbarPageScaling.Value;
}
this.userControl11.PrintPreview = fd;
}
The resulting flowdocument
is displayed in a FlowDocument
Reader which is a class available in the Presentation Foundation Classes and which is hosted on the Print Preview Form by a user Control for WPF, which is available in the Tools Panel in Visual Studio. Note that the XAML code for the control and the "code behind" to expose it must be edited manually:
public partial class UserControl1 : UserControl
{
public UserControl1()
{
InitializeComponent();
}
public static readonly DependencyProperty PrintPreviewProperty =
DependencyProperty.Register("PrintPreview", typeof(FlowDocument),
typeof(UserControl1));
public static readonly DependencyProperty ViewerZoomProperty =
DependencyProperty.Register("ViewerZoom", typeof(double), typeof(UserControl1));
private static Action EmptyDelegate = delegate () { };
private Size viewersize = new Size(603, 502);
private double viewerzoom = 100;
public FlowDocument PrintPreview
{
get { return (FlowDocument)GetValue(PrintPreviewProperty); }
set
{
SetValue(PrintPreviewProperty, (FlowDocument)value);
UpdatePreview();
}
}
public double ViewerZoom
{
get { return viewerzoom; }
set
{
viewerzoom = value/4;
UpdatePreviewZoom();
UpdatePreview();
}
}
public void PrintFlowDocument()
{
Previewer.Print();
}
public Size DesiredViewerSize
{
set
{
viewersize = value;
}
}
public void UpdateViewerSize()
{
Previewer.RenderSize = viewersize;
Previewer.Dispatcher.Invoke(EmptyDelegate,
System.Windows.Threading.DispatcherPriority.Render);
}
private void UpdatePreviewZoom()
{
Previewer.Zoom = viewerzoom;
}
private void UpdatePreview()
{
Previewer.Document = PrintPreview;
}
}
}
For more details about the UserControl
hosting the FlowDocument Reader, see the source code included. Once the FlowDocument is created and displayed in the Reader Control, the next key operation is the generation of a PrintDocument
with a PrintQueue
that incorporates the properties of the selected printer as well as an available options chosen by the user. When the selected printer or the options are changed, the FlowDocument
in the Reader control updates immediately so is always shows how the resulting document pages will look in thumbnail form. The ResetPrintDocument()
combines features of the selected printer with those chosen by the user by merging PrintTickets
:
private void ResetPrintDocument()
{
System.Printing.ValidationResult VR = new System.Printing.ValidationResult();
System.Printing.PrintCapabilities PC;
pd = null;
pd = new System.Windows.Controls.PrintDialog();
pd.PrintQueue = new System.Printing.PrintQueue
(new System.Printing.PrintServer(), _selectedprintername);
if (_printerchanged)
{
try
{
PC = pd.PrintQueue.GetPrintCapabilities();
CurrentPrinterCapabilities = PC;
GetPaperSizesFromPrintCapabilties(PC);
PopulatePaperSizes();
_printerchanged = false;
}
catch (Exception ex)
{
System.Windows.Forms.MessageBox.Show
("Unable to load Paper Sizes from Driver", "Print Driver Error",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
GetCollatingOptions(CurrentPrinterCapabilities);
GetDuplexingCaps(CurrentPrinterCapabilities);
GetPrinterPageScalingSupportStatus(CurrentPrinterCapabilities);
GetPrinterPagesPerSheetOptions(CurrentPrinterCapabilities);
}
VR = pd.PrintQueue.MergeAndValidatePrintTicket
(pd.PrintTicket, CreateUserTicket());
pd.PrintTicket = VR.ValidatedPrintTicket;
if (VR.ConflictStatus != System.Printing.ConflictStatus.NoConflict)
{
System.Windows.Forms.MessageBox.Show
("Print Ticket Conflict Resolved",
"Validation Conflict", MessageBoxButtons.OK,
MessageBoxIcon.Information);
}
_printableareawidth = pd.PrintableAreaWidth;
_printableareaheight = pd.PrintableAreaHeight;
UpdateDimLabel();
InitializeFlowDocument(ref FD, ref rtbSource);
}
private System.Printing.PrintTicket CreateUserTicket()
{
System.Printing.PrintTicket PT = new System.Printing.PrintTicket();
PT.PageMediaSize = AvailablePageMediaSizes[SizeSelectedIndex];
if ( _printersupportspagespersheet && cbPagesPerSheet.SelectedItem != null)
{
PT.PagesPerSheet =(int) cbPagesPerSheet.SelectedItem;
}
PT.PageScalingFactor = fitfactor;
if (_printersupportspagescaling)
{
PT.PageScalingFactor = tbarPageScaling.Value;
}
PT.OutputQuality = System.Printing.OutputQuality.High;
PT.PageBorderless = System.Printing.PageBorderless.None;
if (_landscapemodeon)
{
PT.PageOrientation = System.Printing.PageOrientation.Landscape;
}
else
{
PT.PageOrientation = System.Printing.PageOrientation.Portrait;
}
PT.CopyCount = (int) nUdCopies.Value;
if (rbCollated.Enabled == true && rbCollated.Checked)
{
PT.Collation = Collation.Collated;
}
return PT;
}
After creating the PrintDocument
and its associated PrintQueue
, all that remains is to use its built in print method to print the document. However, to be able to print a custom range of pages and to add a header to each page, both features I wanted to offer, it was necessary to create two custom DocumentPaginator
classes, one for each option and run the pages through these before they are sent to the printer. The code for these was quite complicated and I have tried to credit every original source I referenced. It involves using the abstract
class Page.Visual
which is a vector graphics based primitive used in the Presentation Foundation. See the source code for details.
private void btnPrint_Click(object sender, EventArgs e)
{
DocumentPaginator dp2 = null;
DocumentPaginator PRDP = null;
if (_printersupportspagescaling)
{
pd.PrintTicket.PageScalingFactor = tbarPageScaling.Value;
}
DocumentPaginator paginator =
((IDocumentPaginatorSource)FD).DocumentPaginator;
if (rbHeaderOn.Checked)
{
dp2 = new CustomStyleDocumentPaginator(FD, paginator, paginator.PageSize,
new System.Windows.Size(12, 12), cbNumberPages.Checked,
cbAddDateToHeader.Checked,
tbHeader.Text,(int)nudHeaderPointSize.Value);
}
if (_printcustompagerange && ParsePageRangeTextBox())
{
System.Windows.Controls.PageRange PR =
new System.Windows.Controls.PageRange();
PR.PageFrom = _firstpage;
PR.PageTo = _lastpage;
pd.PageRange = PR;
pd.PageRangeSelection = System.Windows.Controls.PageRangeSelection.UserPages;
pd.SelectedPagesEnabled = true;
pd.UserPageRangeEnabled = true;
if (dp2 != null)
{
PRDP = new PageRangeDocumentPaginator(dp2, _firstpage, _lastpage);
}
else
{
PRDP = new PageRangeDocumentPaginator(paginator, _firstpage, _lastpage);
};
pd.PrintDocument(PRDP, "Printing Document");
} else
{
if (dp2 != null)
{
pd.PrintDocument(dp2, "Printing Document");
} else
{
pd.PrintDocument(((IDocumentPaginatorSource)FD).DocumentPaginator,
"Printing Document");
}
}
this.Close();
}
One final programming issue that came up was how to access the built-in Settings Dialog that is part of every Windows Printer Driver. This is displayed when the user clicks the "Properties" button. This process requires four different Windows API calls and direct memory allocations. The source code is documented. Note that only some of the changes you make using the native settings dialog of the driver persist, and I'm not sure why.
Version 1026 Adds Page dimension labels to the preview display, margin presets to the page dimensions dialog, and a Current page Button to the Print Range Dialog
History
- 13th August, 2020: First version 1.0.1.9
- 13th December 2022: 1.0.2.6