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 >
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.
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)
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).
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.
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.
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
OnPrintPage
isn't overly complexDisadvantages
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();
}
}
}
}
// 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;
*/