Search code examples
c#pdfprintingsystem.drawingsystem.printing

How can I create 'pages' from scratch to be printed or print previewed in c#?


Due to the overwhelming complexity and/or limited license capabilities of the components available for this job, I have decided to write this component from scratch. This is something I have fully functional in PHP, and in VB6. but I am hitting a wall when trying to add a page .

A lot of great examples on how to print from file, or how to print a single page (all graphics etc are hard coded for the pages inside the Print event), but nothing on how to setup a collection to hold the page data, and then send those to be printed.

In vb6, you can obtain the pagebounds and call new page, but in .NET, there doesn't seem to be a new page method.

Following is the source I have so far, which is pretty rough due to the apparent lack of this basic functionality.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Drawing;

using System.Drawing.Drawing2D;

using System.Windows.Forms;
using System.Windows.Forms.Design;

using PdfFileWriter;
using System.Drawing.Printing;
using System.ComponentModel;

using System.IO;
using System.Drawing.Printing;

class PDF : PrintDocument {
    /// <summary>
    /// Logo to display on invoice
    /// </summary>
    public Image Logo { get; set; }

    /// <summary>
    /// Pages drawn in document
    /// </summary>
    private List<Graphics> Pages;

    private int CurrentPage;

    private string directory;
    private string file;

    /// <summary>
    /// Current X position
    /// </summary>
    public int X { get; set; }

    /// <summary>
    /// Current X position
    /// </summary>
    public int Y { get; set; }

    /// <summary>
    /// Set the folder where backups, downloads, etc will be stored or retrieved from
    /// </summary>
    [Editor( typeof( System.Windows.Forms.Design.FolderNameEditor ), typeof( System.Drawing.Design.UITypeEditor ) )]
    public string Folder { get { return directory; } set { directory=value; } }

    public PDF() {
        file = (string)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds.ToString();
        directory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);

        CurrentPage = 0;

        // initialize pages array
        Pages = new List<Graphics>();

        PrinterSettings = new PrinterSettings() {
            PrinterName = "Microsoft Print to PDF",
            PrintToFile = true,
            PrintFileName = Path.Combine(directory, file + ".pdf"),
        };

        DefaultPageSettings = new PageSettings(PrinterSettings) {
            PaperSize=new PaperSize("Letter", 850, 1100 ),
            Landscape = false,
            Margins = new Margins(left: 50, right: 50, top: 50, bottom: 50),
        };
    }


    /// <summary>
    /// Get specific page
    /// </summary>
    /// <param name="page">page number. 1 based array</param>
    /// <returns></returns>
    public Graphics GetPage( int page ) {
        int p = page - 1;
        if ( p<0||p>Pages.Count ) { return null; }
        return Pages[p];
    }

    public Graphics GetCurrentPage() {
        return GetPage(CurrentPage);
    }

    protected override void OnBeginPrint( PrintEventArgs e ) {
        base.OnBeginPrint( e );
    }

    protected override void OnPrintPage( PrintPageEventArgs e ) {
        base.OnPrintPage( e );
    }

    protected override void OnEndPrint( PrintEventArgs e ) {
        base.OnEndPrint( e );
    }

    /// <summary>
    /// Add a new page to the document
    /// </summary>
    public void NewPage() {
        // Add a new page to the page collection and set it as the current page
        Graphics g = Graphics.CreateCraphics(); // not sure if this works, but no CreateGraphics is available
        Pages.Add( g );
    }

    /// <summary>
    /// Add a new string to the current page
    /// </summary>
    /// <param name="text">The string to print</param>
    /// <param name="align">Optional alignment of the string</param>
    public void DrawString(string text, System.Windows.TextAlignment align = System.Windows.TextAlignment.Left ) {
        // add string to document
        Pages[CurrentPage].DrawString(text, new Font("Arial", 10), new SolidBrush(Color.Black), new PointF(X, Y));
    }


    /// <summary>
    /// Save the contents to PDF
    /// </summary>
    /// <param name="FileName"></param>
    public void Save( string FileName ) {
        // Start the print job looping through pages.
        foreach ( Graphics page in Pages ) {
            // there doesn't seem to be an addpage method
        }

        /*
         *  From stackoverflow article on how to 'print' a pdf to filename as the poster complained
         *  that the PrinterSettings.PrintFileName property is ignored. Havn't tested yet. Also, no
         *  such function as 'PrintOut' so further research required.
         * 
                PrintOut(
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    FileName,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value,
                    System.Reflection.Missing.Value
                );
        */
    }

}

I am not looking for a really long winded massive project on how to write PDF documents as they are all very restrictive, each of them has at least one limitation which is a problem for the layout I intend to design (upgrade from PHP which was an upgrade from VB6). The end result layout looks like this >

First Page (Invoice main page )

Invoice First Page

Second Page(s) [summary]

This report may have more pages depending on how many items are in payments and services. Header of the sub-report that continues rolls over to the next page if there are many items. For example, if a customer has 200 services, those items will continue in a similar fashion using the same "Payments" header block at the start of each consecutive page.

Invoice Second Page

Detailed Reports

There may be multiple detail reports, each one starting at the beginning of a new page, and the page counter is reset and printed for those pages. So page 6 of the invoice might actually be Page 3 of the second detail report. Each report starts and ends like the following (and picture depicts layout of field data, etc)

Report first page

Report First Page

Report last page

Report Last Page

What am I looking for ?

A reliable way to make the above multi-report invoice layout work in Visual Studio .NET. I am looking to port code away from php and vb6, and I am not interested in using libraries that are either massive in distribution size, or ridiculously complex / limited license restrictions. Microsoft provides some very powerful tools built-in, and I am not adverse to using the built-in PDF print driver and spooling the data, even though that is a bit of a hack, it does seem to be the least complex method to make this functional without the restrictions or bloat of 3rd party controls. (including open source, as the ones I looked at tend to do some very strange conversions to char, then maybe latex or something, not entirely sure what all the conversion stuff is about).

NOTE

It is very important to understand that the combination of the above report styles make up ONE invoice, and thus only one pdf file per client. If it helps, here is a VB6 backwards compatability method exposing the traditional 'Print' object printing compatability vb6. This should help clarify the native functionality I am looking to create/use.

VB6

I am having a difficult time swallowing the above "no direct equivalent" statement, as adding a new page when creating a document in memory seems to be a pretty basic (and essential) function of creating a document for print. It doesn't make sense that everything needed to be printed MUST be loaded from a file first.


Solution

  • This is my implementation of a working solution to this problem, allowing a user to completely design the document in a reusable manner without ever requiring to send the .Print command.

    The concept of using an Image to store the data was in part due to a comment by Bradley Uffner on this question regarding combining two Graphics objects

    There are several advantages and disadvantages to handling the process in this manner broken down below.

    Advantages

    • The printer stuff is more modular, and reusable in other projects with different print requirements
    • Pages can be removed or even inserted (depending on the type of reports being done, this could save a lot of database query time where a cover page requires a summary, but the details for that summary are to be printed later)
    • Pages can be saved individually as image files
    • Pages can be serialized
    • The OnPrintPage isn't overly complex
    • Dynamic page positioning on current page or any other page in the array. Very easy to switch and place data elsewhere.

    Disadvantages

    • Uses a bit more resources. Potential memory limitations if the Image array gets really big.

    Class File

    This also demonstrates how portable this is, as anyone can reuse it quickly. I am still working on wrapping the Draw methods, however this code demonstrates the objective needing only to really extend with more draw methods, and possibly some other features I may have missed.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.Windows.Forms;
    using System.Windows.Forms.Design;
    
    using System.Drawing.Printing;
    using System.ComponentModel;
    
    using System.IO;
    
    class PDF : PrintDocument {
        /// <summary>
        /// Logo to display on invoice
        /// </summary>
        public Image Logo { get; set; }
    
        /// <summary>
        /// Current X position on canvas
        /// </summary>
        public int X { get; set; }
    
        /// <summary>
        /// Current Y position on canvas
        /// </summary>
        public int Y { get; set; }
    
        /// <summary>
        /// Set the folder where backups, downloads, etc will be stored or retrieved from
        /// </summary>
        [Editor( typeof( System.Windows.Forms.Design.FolderNameEditor ), typeof( System.Drawing.Design.UITypeEditor ) )]
        public string Folder { get { return directory; } set { directory=value; } }
    
        /// <summary>
        /// Current font used to print
        /// </summary>
        public Font Font { get; set; }
    
        /// <summary>
        /// Current font color
        /// </summary>
        public Color ForeColor { get; set; }
    
        private int CurrentPagePrinting { get; set; }
    
        /// <summary>
        /// Set printer margins
        /// </summary>
        public Margins PrintMargins {
            get { return DefaultPageSettings.Margins; }
            set { DefaultPageSettings.Margins = value; }
        }
    
        /// <summary>
        /// Pages drawn in document
        /// </summary>
        public List<Image> Pages { get; private set; }
    
        /// <summary>
        /// The current selected page number. 0 if nothing selected
        /// </summary>
        private int CurrentPage;
    
        /// <summary>
        /// The current working directory to save files to
        /// </summary>
        private string directory;
    
        /// <summary>
        /// The currently chosen filename
        /// </summary>
        private string file;
    
        /// <summary>
        /// Public acceisble object to all paperSizes as set
        /// </summary>
        public List<PrintPaperSize> PaperSizes { get; private set; }
    
        /// <summary>
        /// Object for holding papersizes
        /// </summary>
        public class PrintPaperSize {
            public string Name { get; set; }
            public double Height { get; set; }
            public double Width { get; set; }
    
            public PaperKind Kind { get; set; }
    
            public PrintPaperSize() {
                Height = 0;
                Width = 0;
                Name = "";
                Kind = PaperKind.Letter;
            }
    
            public PrintPaperSize( string name, double height, double width, PaperKind kind ) {
                Height=height;
                Width=width;
                Name=name;
                Kind=kind;
            }
        }
    
    
        /// <summary>
        /// Set the spacing between lines in percentage. Affects Y position. Range(%): 1 - 1000
        /// </summary>
        private int lineSpacing;
        public int LineSpacing {
            get {
                return lineSpacing;
            }
            set {
                if(value > 0 && value < 1000) {
                    lineSpacing = value;
                }
            }
        }
    
        /// <summary>
        /// Current papersize selected. used for some calculations
        /// </summary>
        public PrintPaperSize CurrentPaperSize { get; private set; }
    
        public PDF() {
            // set the file name without extension to something safe
            file = (string)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds.ToString();
    
            // set the save directory to MyDocuments
            directory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            CurrentPage = 0;
    
            // initialize pages array
            Pages = new List<Image>();
    
            // Set the initial font and color
            Font = new System.Drawing.Font("Arial", (float)11.25, FontStyle.Regular, GraphicsUnit.Point);
            ForeColor = Color.Black;
    
            lineSpacing = 100;
    
            // set the printer to Microsoft's PDF printer and generate and ensure it will save to a file
            PrinterSettings = new PrinterSettings() {
                PrinterName = "Microsoft Print to PDF",
                PrintToFile = true,
                PrintFileName = Path.Combine(directory, file + ".pdf"),
            };
    
            // hide the notice 'printing' while spooling job.
            PrintController = new StandardPrintController();
    
            // set the printer quality to maximum so we can use this for getting the dpi at this setting
            DefaultPageSettings.PrinterResolution.Kind = PrinterResolutionKind.High;
    
            // store all paper sizes at 1 dpi [ reference: https://social.msdn.microsoft.com/Forums/vstudio/en-US/05169a47-04d5-4890-9b0a-7ad11a6a87f2/need-pixel-width-for-paper-sizes-a4-a5-executive-letter-legal-executive?forum=csharpgeneral ]
            PaperSizes = new List<PrintPaperSize>();
            foreach ( PaperSize P in PrinterSettings.PaperSizes ) {
                double W=P.Width/100.0;
                double H=P.Height/100.0;
                PaperSizes.Add(
                    new PrintPaperSize() {
                        Height = H,
                        Width = W,
                        Name = P.PaperName,
                        Kind = P.Kind
                    }
                );
    
                if ( P.PaperName=="Letter" ) {
                    CurrentPaperSize = PaperSizes[PaperSizes.Count-1];
                }
            }
    
            // setup the initial page type, orientation, margins, 
            using ( Graphics g=PrinterSettings.CreateMeasurementGraphics() ) {
                DefaultPageSettings = new PageSettings(PrinterSettings) {
                    PaperSize=new PaperSize( CurrentPaperSize.Name, (Int32)(CurrentPaperSize.Width*g.DpiX), (Int32)(CurrentPaperSize.Height*g.DpiY) ),
                    Landscape = false,
                    Margins = new Margins(left: 100, right: 100, top: 10, bottom: 10),
                    PrinterResolution=new PrinterResolution() {
                        Kind = PrinterResolutionKind.High
                    }
                };
            }
    
            // constrain print within margins
            OriginAtMargins = false;
        }
    
    
        public void SetPaperSize( PaperKind paperSize ) {
            // TODO: Use Linq on paperSizes
        }
    
        /// <summary>
        /// Get specific page
        /// </summary>
        /// <param name="page">page number. 1 based array</param>
        /// <returns></returns>
        public Image GetPage( int page ) {
            int p = page - 1;
            if ( p<0||p>Pages.Count ) { return null; }
            return Pages[p];
        }
    
        /// <summary>
        /// Get the current page
        /// </summary>
        /// <returns>Image</returns>
         public Image GetCurrentPage() {
            return GetPage(CurrentPage);
        }
    
        /// <summary>
        /// Before printing starts
        /// </summary>
        /// <param name="e">PrintEventArgs</param>
        protected override void OnBeginPrint( PrintEventArgs e ) {
             CurrentPagePrinting=0;
             base.OnBeginPrint( e );
        }
    
        /// <summary>
        /// Print page event
        /// </summary>
        /// <param name="e">PrintPageEventArgs</param>
        protected override void OnPrintPage( PrintPageEventArgs e ) {
            CurrentPagePrinting++;
    
            // if page count is max exit print routine
            if ( CurrentPagePrinting==Pages.Count ) { e.HasMorePages=false; } else { e.HasMorePages=true; }
    
            // ensure high resolution / clarity of image so text doesn't fuzz
            e.Graphics.CompositingMode=CompositingMode.SourceOver;
            e.Graphics.CompositingQuality=CompositingQuality.HighQuality;
    
            // Draw image and respect margins (unscaled in addition to the above so text doesn't fuzz)
            e.Graphics.DrawImageUnscaled(
                Pages[CurrentPagePrinting-1],
    //          new Point(0,0)
                new Point(
                    DefaultPageSettings.Margins.Left,
                    DefaultPageSettings.Margins.Top
                )
            );
            base.OnPrintPage( e );
        }
    
        /// <summary>
        /// After printing has been completed
        /// </summary>
        /// <param name="e">PrintEventArgs</param>
        protected override void OnEndPrint( PrintEventArgs e ) {
            base.OnEndPrint( e );
        }
    
        /// <summary>
        /// Add a new page to the document
        /// </summary>
        public void NewPage() {
            // Add a new page to the page collection and set it as the current page
    
            Bitmap bmp;
            using(Graphics g = PrinterSettings.CreateMeasurementGraphics()) {
                int w=(Int32)( CurrentPaperSize.Width*g.DpiX )-(Int32)( ( ( DefaultPageSettings.Margins.Left+DefaultPageSettings.Margins.Right )/100 )*g.DpiX );
                int h=(Int32)( CurrentPaperSize.Height*g.DpiY )-(Int32)( ( ( DefaultPageSettings.Margins.Top+DefaultPageSettings.Margins.Bottom )/100 )*g.DpiY );
                bmp = new Bitmap( w, h );
                bmp.SetResolution(g.DpiX, g.DpiY);
            }
    
            // reset X and Y positions
            Y=0;
            X=0;
    
            // Add new page to the collection
            Pages.Add( bmp );
            CurrentPage++;
        }
    
        /// <summary>
        /// Change the current page to specified page number
        /// </summary>
        /// <param name="page">page number</param>
        /// <returns>true if page change was successful</returns>
        public bool SetCurrentPage( int page ) {
            if ( page<1 ) { return false; }
            if ( page>Pages.Count ) { return false; }
            CurrentPage = page - 1;
            return true;
        }
    
        /// <summary>
        /// Remove the specified page #
        /// </summary>
        /// <param name="page">page number</param>
        /// <returns>true if successful</returns>
        public bool RemovePage(int page) {
            if ( page<1 ) { return false; }
            if ( page>Pages.Count ) { return false; }
            if ( Pages.Count-page==0 ) {
                CurrentPage = 0;
                Pages.RemoveAt(page - 1);
            } else {
                if ( page==CurrentPage && CurrentPage == 1 ) {
                    Pages.RemoveAt(page - 1);
                } else {
                    CurrentPage = CurrentPage - 1;
                    Pages.RemoveAt(page -1);
                }
            }
            return true;
        }
    
        /// <summary>
        /// Add a new string to the current page
        /// </summary>
        /// <param name="text">The string to print</param>
        /// <param name="align">Optional alignment of the string</param>
        public void DrawString(string text, System.Windows.TextAlignment align = System.Windows.TextAlignment.Left ) {
            // add string to document
            using ( Graphics g=Graphics.FromImage( Pages[CurrentPage - 1] ) ) {
                g.CompositingQuality = CompositingQuality.HighQuality;
    
                // get linespacing and adjust by user specified linespacing
                int iLineSpacing=(Int32)( g.MeasureString( "X", Font ).Height*(float)( (float)LineSpacing/(float)100 ) );
                switch ( align ) {
                    case System.Windows.TextAlignment.Left:
                    case System.Windows.TextAlignment.Justify:
                        g.DrawString( text, Font, new SolidBrush( ForeColor ), new PointF( X, Y ) );
                        break;
                    case System.Windows.TextAlignment.Right:
                        g.DrawString( text, Font, new SolidBrush( ForeColor ), new PointF( Pages[CurrentPage - 1].Width - g.MeasureString( text, Font ).Width, Y ) );
                        break;
                    case System.Windows.TextAlignment.Center:
                        g.DrawString( text, Font, new SolidBrush( ForeColor ), new PointF( ( Pages[CurrentPage-1].Width+g.MeasureString( text, Font ).Width )/2, Y ) );
                        break;
                }
                Y+=iLineSpacing;
                if( Y + iLineSpacing > Pages[CurrentPage-1].Height ) {
                    NewPage();
                }
            }
    
        }
    }
    

    Example Usage

    // initialize a new PrintDocument
    PDF print = new PDF();
    
    // set the font
    print.Font = new Font("Helvetica", (float)12, FontStyle.Regular, GraphicsUnit.Point);
    
    // change the color (can be used for shapes, etc once their draw methods are added to the PDF() class)
    print.ForeColor = Color.Red;
    
    // create a new page !!!!
    print.NewPage();
    
    // add some text
    print.DrawString( "Hello World !!" );
    
    // add some right aligned text
    print.DrawString( "Aligned Right", System.Windows.TextAlignment.Right );
    
    // add some centered text
    print.DrawString( "Aligned Right", System.Windows.TextAlignment.Center );
    
    // change line spacing ( percentage between 1% and 1000% )
    print.LineSpacing = 50; // 50% of drawstrings detected line height
    
    // add another page
    print.NewPage();
    
    // print a couple lines
    print.DrawString( "Hello World" );
    print.DrawString( "Hello World" );
    
    // change the color again and print another line
    ForeColor = Color.Yellow;
    print.DrawString( "Hello World" );
    
    // duplicate a page (clone page 1 as page 3 )
    print.NewPage();
    print.Pages[print.Pages -1] = print.GetPage(1);
    
    // go back to page 1 and print some more text at specified coordinates
    print.SetCurrentPage(1);
    print.X = 400;
    print.Y = 300;
    print.DrawString( "Drawn after 3rd page created" );
    
    // send the print job
    print.Print();
    
    // reprint
    print.Print();
    
    // show a preview of the 2nd page
    /*
        Image img = print.GetPage(1);
        pictureBox1.Height=(Int32)(print.CurrentPaperSize.Height*img.VerticalResolution);
        pictureBox1.Width = (Int32)(print.CurrentPaperSize.Width*img.HorizontalResolution);
        pictureBox1.SizeMode = PictureBoxSizeMode.AutoSize;
        pictureBox1.Image = img;
    */