Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

An Improved WYSIWYG Print Dialog and Page Preview for Rich Text

5.00/5 (6 votes)
13 Aug 2020CPOL4 min read 17.6K   1.1K  
Windows Forms Print Dialog for rich text with accurate page preview and zooming
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.

Image 1

 

Image 2

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.

C#
//Constructors:

   public ZprintForm()
   public ZPrintForm(string Document)
   public ZPrintForm(string Document,string Header,bool NumberPages, 
                     bool IncludeDate, int PointSize)
//
// Example:
//
   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:

C#
// CONVERT RTF TO FLOWDOC WYSIWYG
        private bool CreateFlowDocFromRtf(ref FlowDocument f, string sourcertf)
        {
            bool success = false;
            //f.SetValue(FlowDocument.TextAlignmentProperty, Left);
            TextRange r = new TextRange(f.ContentStart, f.ContentEnd);
            if (r.CanLoad(System.Windows.Forms.DataFormats.Rtf) && 
            (!string.IsNullOrEmpty(sourcertf)))      // 05-27-2020 ADDED SPECIFIC REF 
                                                     // TO DATAFORMATS AMBIG ERROR
            {
                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;
        }
        
    // CREATE FLOWDOCUMENT
        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);  // MATCH SCALE 
                                                        // FACTOR PLUS EMPIRICAL CORRECTION
            fd.ColumnWidth = Double.PositiveInfinity;   // fd.PageWidth;
            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);
                //EHT.GeneralExceptionHandler("Unable to Convert RTF", 
                //"CreateFlowDocumentFromRtf()", false, null);
                return;
            }
            GetPageCount(ref fd);
            this.userControl11.ViewerZoom = fitfactor + 50;
            if (_printersupportspagescaling)
            {
                this.userControl11.ViewerZoom = tbarPageScaling.Value;  // ADDED 
                                                                        // 05-23-2020 1145AM
            }
            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:

C#
public partial class UserControl1 : UserControl
    {
        public UserControl1()
        {
            InitializeComponent();            
        }
        // Dependency Property
        // SYNTAX = Register("Variable Name",typeof(variable),typeof(ownertype))
        // 05-23-2020 Note naming convention xxxxProperty in declaration 
        // matches public type xxxx in getter setter

        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;
        // .NET Property wrapper
        public FlowDocument PrintPreview
        {
            get { return (FlowDocument)GetValue(PrintPreviewProperty); }
            set
            {
                SetValue(PrintPreviewProperty, (FlowDocument)value);
                UpdatePreview();
            }
        }
        public double ViewerZoom
        {
            get { return viewerzoom; }
            set
            {                
                viewerzoom = value/4;       // scale for display purposes 05-23-2020
                
                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:

C#
// RESET PRINTDOCUMENT
        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();  // Called only when a 
                                                                // new printer is selected, 
                                                                // otherwise
                    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);
                    // EHT.GeneralExceptionHandler
                    // ("Error Getting PaperSizes From Print Driver", 
                    // "ResetPrintDocument()", false, ex);
                }
                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);
               // DT.NotifyDialog(this, "Print Ticket Conflict(s) were resolved.");
            }
            _printableareawidth = pd.PrintableAreaWidth;
            _printableareaheight = pd.PrintableAreaHeight;
            UpdateDimLabel();
            InitializeFlowDocument(ref FD, ref rtbSource);
        }
        // CREATE USER PRINTICKET
        // This is desired PT to be merged and validated with that from the printer 
        // (via printqueue)
        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.PagesPerSheet = _pagespersheet;
            PT.PageScalingFactor = fitfactor;
            if (_printersupportspagescaling)
            {
                PT.PageScalingFactor = tbarPageScaling.Value;
            }
            PT.OutputQuality = System.Printing.OutputQuality.High;
            PT.PageBorderless = System.Printing.PageBorderless.None; // Laser Printers 
                                                                     // Can't Print Borderless
            if (_landscapemodeon)
            {
                PT.PageOrientation = System.Printing.PageOrientation.Landscape;
            }
            else
            {
                PT.PageOrientation = System.Printing.PageOrientation.Portrait;
            }
            PT.CopyCount = (int) nUdCopies.Value;   // tested ok 05-23-2020
            if (rbCollated.Enabled == true && rbCollated.Checked)   // Only Applies 
                                                    // If Multiple Copies Made
            {
                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.

C#
// PRINT BUTTON HANDLER
    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;             //first paginator
        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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)