如何从头开始创建 'pages' 以在 C# 中打印或打印预览?
How can I create 'pages' from scratch to be printed or print previewed in c#?
由于极其复杂 and/or 可用于此工作的组件的许可功能有限,我决定从头开始编写此组件。这是我在 PHP 和 VB6 中完全发挥作用的东西。但是我在尝试添加 page
时碰壁了。
关于如何从文件打印或如何打印单页的很多很好的示例(所有图形等都针对 Print 事件中的页面进行了硬编码),但没有关于如何设置 collection保存页面数据,然后发送要打印的。
在vb6中,可以获取pagebounds并调用new page,但是在.NET中,好像没有new page的方法。
以下是我目前拥有的源代码,由于明显缺少此基本功能,因此相当粗糙。
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 Whosebug 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
);
*/
}
}
我不是在寻找一个关于如何编写 PDF 文档的冗长的大型项目,因为它们都非常严格,每个都有至少一个限制,这对我打算设计的布局(升级来自 PHP,它是 VB6 的升级版)。最终结果布局如下所示 >
第一页(发票主页)
第二页[摘要]
此报告可能有更多页数,具体取决于付款和服务中的项目数量。项目多则继续sub-report的Header翻到下一页。例如,如果客户有 200 项服务,这些项目将以类似的方式在每个连续页面的开头使用相同的 "Payments" header 块继续。
详细报告
可能有多个详细报告,每个报告都从新页面的开头开始,页面计数器会重置并打印这些页面。因此,发票的第 6 页实际上可能是第二份详细报告的第 3 页。每个报告的开始和结束如下(图片描述了字段数据的布局等)
报告第一页
报告最后一页
我在找什么?
使上述 multi-report 发票布局在 Visual Studio .NET 中工作的可靠方法。我希望将代码从 php 和 vb6 移植出去,而且我对使用分布大小庞大或极其复杂/许可限制有限的库不感兴趣。 Microsoft 提供了一些非常强大的工具 built-in,我并不反对使用 built-in PDF 打印 driver 和假脱机数据,尽管这有点 hack,但看起来确实如此是使此功能不受第 3 方控件的限制或膨胀的最简单的方法。 (包括开源,因为我看到的那些倾向于对 char 进行一些非常奇怪的转换,然后可能是乳胶或其他东西,不完全确定所有转换的内容是什么)。
注意
了解上述报告样式的组合构成一张发票非常重要,因此每个客户只有一个 pdf 文件。如果有帮助,这里有一个 VB6 向后兼容方法,它公开了传统的 'Print' object printing compatability vb6。这应该有助于阐明我正在寻找的本机功能 create/use。
我很难吞下上面的 "no direct equivalent" 语句,因为在内存中创建文档时添加新页面似乎是创建打印文档的一个非常基本(和必要)的功能。必须首先从文件中加载需要打印的所有内容是没有意义的。
我创建了一个非常简单的 .NET 打印系统演示,它模仿了您指定的基本发票布局。完整代码可用 here,但我将在下面总结重要部分。我将继续努力并改进它,因为创建它很有趣。
截至目前,它产生的输出如下所示:
文本质量示例:
InvoiceDocument
负责打印一个 Invoice
:
的实例
class InvoiceDocument : PrintDocument
{
public InvoiceDocument(Invoice invoice)
{
_invoice = invoice;
_currentSection = new MainPage(this);
}
private Invoice _invoice;
public Invoice Invoice => _invoice;
private InvoiceSection _currentSection;
public InvoiceSection CurrentSection => _currentSection;
#region Fonts
private Font _titleFont = new Font(FontFamily.GenericSansSerif, 18, FontStyle.Bold);
public Font TitleFont => _titleFont;
private Font _headerFont = new Font(FontFamily.GenericSansSerif, 12, FontStyle.Regular);
public Font HeaderFont => _headerFont;
private Font _regularFont = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Regular);
public Font RegularFont => _regularFont;
private Font _boldFont = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Bold);
public Font BoldFont => _boldFont;
#endregion
protected override void OnPrintPage(PrintPageEventArgs e)
{
_currentSection?.Render(e);
}
public void ChangeSection(InvoiceSection nextSection)
{
_currentSection = nextSection;
}
}
InvoiceDocument
由 InvoiceSection
的子类组成。每个部分知道如何打印发票的不同部分,主要页面,摘要,详细信息等。它还负责知道何时以及如何换行到下一页:
abstract class InvoiceSection
{
protected InvoiceSection(InvoiceDocument invoiceDocument)
{
this.InvoiceDocument = invoiceDocument;
}
public InvoiceDocument InvoiceDocument { get; }
public abstract void Render(PrintPageEventArgs e);
public Invoice Invoice => InvoiceDocument?.Invoice;
}
internal class MainPage : InvoiceSection
{
public MainPage(InvoiceDocument invoiceDocument) : base(invoiceDocument) { }
public override void Render(PrintPageEventArgs e)
{
e.Graphics.FillEllipse(Brushes.Green, e.MarginBounds.Left, e.MarginBounds.Top, e.MarginBounds.Left + 100, e.MarginBounds.Top + 100);
e.Graphics.DrawString(Invoice.CompanyName, InvoiceDocument.TitleFont, Brushes.Black, e.MarginBounds.Left, e.MarginBounds.Top + 30);
e.HasMorePages = true;
InvoiceDocument.ChangeSection(new SummmarySection(InvoiceDocument));
}
}
internal class SummmarySection : InvoiceSection
{
public SummmarySection(InvoiceDocument invoiceDocument) : base(invoiceDocument)
{
}
public override void Render(PrintPageEventArgs e)
{
e.Graphics.FillRectangle(Brushes.LightGray, e.MarginBounds.Left, e.MarginBounds.Top, e.MarginBounds.Width, 20);
e.Graphics.DrawString("Payments", InvoiceDocument.HeaderFont, Brushes.Black, e.MarginBounds.Left + 200, e.MarginBounds.Top + 2);
int y = e.MarginBounds.Top + 25;
while (_currentPaymentIndex < Invoice.Payments.Count && y < e.MarginBounds.Bottom)
{
Payment payment = Invoice.Payments[_currentPaymentIndex];
e.Graphics.DrawString(payment.Description, InvoiceDocument.RegularFont, Brushes.Black, e.MarginBounds.Left + 150, y);
e.Graphics.DrawString($"{payment.Amount:C}", InvoiceDocument.RegularFont, Brushes.Black, e.MarginBounds.Right - 150, y);
y = y + InvoiceDocument.RegularFont.Height;
_currentPaymentIndex++;
}
if (_currentPaymentIndex < Invoice.Payments.Count)
{
e.HasMorePages = true;
}
}
private int _currentPaymentIndex = 0;
}
这是我针对此问题实施的有效解决方案,允许用户以可重用的方式完全设计文档,而无需发送 .Print
命令。
使用图像存储数据的概念部分是由于 Bradley Uffner on this question 关于合并两个 Graphics
对象的评论
以这种方式处理流程有几个优点和缺点,具体如下。
优势
- 打印机的东西更加模块化,可以在具有不同打印要求的其他项目中重复使用
- 可以删除甚至插入页面(取决于所完成的报告类型,这可以节省大量的数据库查询时间,因为封面需要摘要,但该摘要的详细信息将在稍后打印)
- 页面可以单独保存为图像文件
- 页面可以序列化
OnPrintPage
并不过分复杂
- 当前页面或数组中任何其他页面的动态页面定位。非常容易在其他地方切换和放置数据。
缺点
- 使用更多的资源。如果图像数组变得非常大,则可能存在内存限制。
Class 文件
这也证明了它的便携性,因为任何人都可以快速重用它。我仍在努力包装 Draw 方法,但是这段代码演示了 objective 只需要真正扩展更多的 draw 方法,并且可能还有一些我可能错过的其他功能。
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;
*/
由于极其复杂 and/or 可用于此工作的组件的许可功能有限,我决定从头开始编写此组件。这是我在 PHP 和 VB6 中完全发挥作用的东西。但是我在尝试添加 page
时碰壁了。
关于如何从文件打印或如何打印单页的很多很好的示例(所有图形等都针对 Print 事件中的页面进行了硬编码),但没有关于如何设置 collection保存页面数据,然后发送要打印的。
在vb6中,可以获取pagebounds并调用new page,但是在.NET中,好像没有new page的方法。
以下是我目前拥有的源代码,由于明显缺少此基本功能,因此相当粗糙。
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 Whosebug 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
);
*/
}
}
我不是在寻找一个关于如何编写 PDF 文档的冗长的大型项目,因为它们都非常严格,每个都有至少一个限制,这对我打算设计的布局(升级来自 PHP,它是 VB6 的升级版)。最终结果布局如下所示 >
第一页(发票主页)
第二页[摘要]
此报告可能有更多页数,具体取决于付款和服务中的项目数量。项目多则继续sub-report的Header翻到下一页。例如,如果客户有 200 项服务,这些项目将以类似的方式在每个连续页面的开头使用相同的 "Payments" header 块继续。
详细报告
可能有多个详细报告,每个报告都从新页面的开头开始,页面计数器会重置并打印这些页面。因此,发票的第 6 页实际上可能是第二份详细报告的第 3 页。每个报告的开始和结束如下(图片描述了字段数据的布局等)
报告第一页
报告最后一页
我在找什么?
使上述 multi-report 发票布局在 Visual Studio .NET 中工作的可靠方法。我希望将代码从 php 和 vb6 移植出去,而且我对使用分布大小庞大或极其复杂/许可限制有限的库不感兴趣。 Microsoft 提供了一些非常强大的工具 built-in,我并不反对使用 built-in PDF 打印 driver 和假脱机数据,尽管这有点 hack,但看起来确实如此是使此功能不受第 3 方控件的限制或膨胀的最简单的方法。 (包括开源,因为我看到的那些倾向于对 char 进行一些非常奇怪的转换,然后可能是乳胶或其他东西,不完全确定所有转换的内容是什么)。
注意
了解上述报告样式的组合构成一张发票非常重要,因此每个客户只有一个 pdf 文件。如果有帮助,这里有一个 VB6 向后兼容方法,它公开了传统的 'Print' object printing compatability vb6。这应该有助于阐明我正在寻找的本机功能 create/use。
我很难吞下上面的 "no direct equivalent" 语句,因为在内存中创建文档时添加新页面似乎是创建打印文档的一个非常基本(和必要)的功能。必须首先从文件中加载需要打印的所有内容是没有意义的。
我创建了一个非常简单的 .NET 打印系统演示,它模仿了您指定的基本发票布局。完整代码可用 here,但我将在下面总结重要部分。我将继续努力并改进它,因为创建它很有趣。
截至目前,它产生的输出如下所示:
文本质量示例:
InvoiceDocument
负责打印一个 Invoice
:
class InvoiceDocument : PrintDocument
{
public InvoiceDocument(Invoice invoice)
{
_invoice = invoice;
_currentSection = new MainPage(this);
}
private Invoice _invoice;
public Invoice Invoice => _invoice;
private InvoiceSection _currentSection;
public InvoiceSection CurrentSection => _currentSection;
#region Fonts
private Font _titleFont = new Font(FontFamily.GenericSansSerif, 18, FontStyle.Bold);
public Font TitleFont => _titleFont;
private Font _headerFont = new Font(FontFamily.GenericSansSerif, 12, FontStyle.Regular);
public Font HeaderFont => _headerFont;
private Font _regularFont = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Regular);
public Font RegularFont => _regularFont;
private Font _boldFont = new Font(FontFamily.GenericSansSerif, 10, FontStyle.Bold);
public Font BoldFont => _boldFont;
#endregion
protected override void OnPrintPage(PrintPageEventArgs e)
{
_currentSection?.Render(e);
}
public void ChangeSection(InvoiceSection nextSection)
{
_currentSection = nextSection;
}
}
InvoiceDocument
由 InvoiceSection
的子类组成。每个部分知道如何打印发票的不同部分,主要页面,摘要,详细信息等。它还负责知道何时以及如何换行到下一页:
abstract class InvoiceSection
{
protected InvoiceSection(InvoiceDocument invoiceDocument)
{
this.InvoiceDocument = invoiceDocument;
}
public InvoiceDocument InvoiceDocument { get; }
public abstract void Render(PrintPageEventArgs e);
public Invoice Invoice => InvoiceDocument?.Invoice;
}
internal class MainPage : InvoiceSection
{
public MainPage(InvoiceDocument invoiceDocument) : base(invoiceDocument) { }
public override void Render(PrintPageEventArgs e)
{
e.Graphics.FillEllipse(Brushes.Green, e.MarginBounds.Left, e.MarginBounds.Top, e.MarginBounds.Left + 100, e.MarginBounds.Top + 100);
e.Graphics.DrawString(Invoice.CompanyName, InvoiceDocument.TitleFont, Brushes.Black, e.MarginBounds.Left, e.MarginBounds.Top + 30);
e.HasMorePages = true;
InvoiceDocument.ChangeSection(new SummmarySection(InvoiceDocument));
}
}
internal class SummmarySection : InvoiceSection
{
public SummmarySection(InvoiceDocument invoiceDocument) : base(invoiceDocument)
{
}
public override void Render(PrintPageEventArgs e)
{
e.Graphics.FillRectangle(Brushes.LightGray, e.MarginBounds.Left, e.MarginBounds.Top, e.MarginBounds.Width, 20);
e.Graphics.DrawString("Payments", InvoiceDocument.HeaderFont, Brushes.Black, e.MarginBounds.Left + 200, e.MarginBounds.Top + 2);
int y = e.MarginBounds.Top + 25;
while (_currentPaymentIndex < Invoice.Payments.Count && y < e.MarginBounds.Bottom)
{
Payment payment = Invoice.Payments[_currentPaymentIndex];
e.Graphics.DrawString(payment.Description, InvoiceDocument.RegularFont, Brushes.Black, e.MarginBounds.Left + 150, y);
e.Graphics.DrawString($"{payment.Amount:C}", InvoiceDocument.RegularFont, Brushes.Black, e.MarginBounds.Right - 150, y);
y = y + InvoiceDocument.RegularFont.Height;
_currentPaymentIndex++;
}
if (_currentPaymentIndex < Invoice.Payments.Count)
{
e.HasMorePages = true;
}
}
private int _currentPaymentIndex = 0;
}
这是我针对此问题实施的有效解决方案,允许用户以可重用的方式完全设计文档,而无需发送 .Print
命令。
使用图像存储数据的概念部分是由于 Bradley Uffner on this question 关于合并两个 Graphics
对象的评论
以这种方式处理流程有几个优点和缺点,具体如下。
优势
- 打印机的东西更加模块化,可以在具有不同打印要求的其他项目中重复使用
- 可以删除甚至插入页面(取决于所完成的报告类型,这可以节省大量的数据库查询时间,因为封面需要摘要,但该摘要的详细信息将在稍后打印)
- 页面可以单独保存为图像文件
- 页面可以序列化
OnPrintPage
并不过分复杂- 当前页面或数组中任何其他页面的动态页面定位。非常容易在其他地方切换和放置数据。
缺点
- 使用更多的资源。如果图像数组变得非常大,则可能存在内存限制。
Class 文件
这也证明了它的便携性,因为任何人都可以快速重用它。我仍在努力包装 Draw 方法,但是这段代码演示了 objective 只需要真正扩展更多的 draw 方法,并且可能还有一些我可能错过的其他功能。
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;
*/