怎样用C#实现完整文档打印功能

在windows应用程序中文档的打印是一项非常重要的功能,在以前一直是一个非常复杂的工作,Microsoft .net Framework的打印功能都以组件的方式提供,为程序员提供了很大的方便,但是这几个组件的使用还是很复杂的,有必要解释一下。

  打印操作通常包括以下四个功能

  1 打印设置 设置打印机的一些参数比如更改打印机驱动程序等
  2 页面设置 设置页面大小纸张类型等
  3 打印预览 类似于word中的打印预览
  4 打印

  下面我把我编写的记事本(全部源代码可以在http://www.cndot.net中下载)中用到的打印功能的代码进行解释希望能给大家一些帮助实现打印功能的核心是PrintDocument类这个类属于System.Drawing.Printing名字空间这个类封装了当前的打印设置页面设置以及所有的与打印有关的事件和方法

  这个类包括以下几个属性 事件 和方法

  1、PrinterSettings 属性

    存放打印机的设置信息这个属性不需要程序员设置因为它是由打印对话框获取的

  2、PrintCountroller 属性 

    控制打印过程

  3、DefaultPageSettings 属性

    存放页面设置信息 打印纸大小方向等也不需要程序员设置因为它是由页面设置对话框获取的

  4、DocumentName 属性

    指定文档名称,出现在打印机状态窗口中

  1。 BeginPrint事件

    在打印之前发出

  2. PrintPage事件

    每打印一页是发出,事件接受一个PrintPageEventArgs参数该参数封装了打印相关的信息

  PrintPageEventArgs参数有很多重要的属性

  1 Cancel 取消打印
  2 Graphics 页面的绘图对象
  3 HasMorePages 是否还有要打印的页面

  Print 方法 该方法没有参数 调用它将按照当前设置开始打印 若实现打印功能首先构造PrintDocument对象添加打印事件

PrintDocument printDocument;
private void InitializeComponent()
{
...
printDocument=new PrintDocument();
printDocument.PrintPage += new PrintPageEventHandler (this.printDocument_PrintPage);
...
}

  实现打印事件功能

  打印和绘图类似都是调用Graphics 类的方法进行画图 不同的是一个在显示器上一个在打印纸上并且打印要进行一些复杂的计算如换行 分页等。

private void printDocument_PrintPage(object sender,PrintPageEventArgs e)
{
Graphics g = e.Graphics; //获得绘图对象
float linesPerPage = 0; //页面的行号
float yPosition = 0;   //绘制字符串的纵向位置
int count = 0; //行计数器
float leftMargin = e.MarginBounds.Left; //左边距
float topMargin = e.MarginBounds.Top; //上边距
string line = null; 行字符串
Font printFont = this.textBox.Font; //当前的打印字体
SolidBrush myBrush = new SolidBrush(Color.Black);//刷子
linesPerPage = e.MarginBounds.Height / printFont.GetHeight(g);//每页可打印的行数
//逐行的循环打印一页
    while(count < linesPerPage && ((line=lineReader.ReadLine()) != null))
    {
       yPosition = topMargin + (count * printFont.GetHeight(g));
       g.DrawString(line, printFont, myBrush, leftMargin, yPosition, new StringFormat());
       count++;
    }

  如果本页打印完成而line不为空说明还有没完成的页面这将触发下一次的打印事件在下一次的打印中lineReader会自动读取上次没有打印完的内容因为lineReader是这个打印方法外的类的成员它可以记录当前读取的位置

    if(line != null)
        e.HasMorePages = true;
    else
        e.HasMorePages = false; 
}

  打印设置,构造打印对话框 将对话框中设置的Document属性赋给printDocument这样会将用户的设置自动保存到printDocument的PrinterSettings属性中

protected  void FileMenuItem_PrintSet_Click(object sender,EventArgs e)
{
PrintDialog printDialog = new PrintDialog();
printDialog.Document = printDocument;
printDialog.ShowDialog();
}

  页面设置和打印预览与打印设置原理相同都是构造对话框将用户在对话框中的设置保存到相应的类的属性中

protected  void FileMenuItem_PageSet_Click(object sender,EventArgs e)
{
  PageSetupDialog pageSetupDialog = new PageSetupDialog();
  pageSetupDialog.Document = printDocument;
  pageSetupDialog.ShowDialog();
}

  打印预览

protected void FileMenuItem_PrintView_Click(object sender,EventArgs e)
{
   PrintPreviewDialog printPreviewDialog = new PrintPreviewDialog();
   printPreviewDialog.Document = printDocument;
   lineReader = new StringReader(textBox.Text);
   try
      {
    printPreviewDialog.ShowDialog();
      }
    catch(Exception excep)
    {
    MessageBox.Show(excep.Message, "打印出错", MessageBoxButtons.OK, MessageBoxIcon.Error);
    }
}

  打印就可以直接调用printDocument的Print()方法因为用户可能在打印之前还要再更改打印设置所以在这里再次显示打印设置对话框

  protected void FileMenuItem_Print_Click(object sender,EventArgs e)
  {
   PrintDialog printDialog = new PrintDialog();
   printDialog.Document = printDocument;
   lineReader = new StringReader(textBox.Text);
   if (printDialog.ShowDialog() == DialogResult.OK)
   {
    try
       {
       printDocument.Print();
       }
       catch(Exception excep)
            {
              MessageBox.Show(excep.Message, "打印出错", MessageBoxButtons.OK, MessageBoxIcon.Error);
              printDocument.PrintController.OnEndPrint(printDocument,new PrintEventArgs());
            }
       }
  }

  总结打印的过程是

  1 在应用程序窗体初始化时构造PrintDocument对象  添加 printDocument 的 PrintPage 方法

  2 实现PrintPage方法  4 在用户的单击事件中调用 printDocument 的 Print方法实现打印功能在这中间可能要用到  PrintDialog PrintPreviewDialog PageSetupDialog 设置和查看打印效果这些方法通常是由菜单的单击触发的。

引言

  前段时间为客户开发一套打印机配套的软件,对C#中调用打印机做了些研究。

  ---------------------------------------------

  问题

  .Net Framework 1.1给我们提供了一个PrinterSettings类,以提供指定有关文档打印方式的信息,其中包括打印文档的打印机。其中的静态属性InstalledPrinters可以使我们获取安装在计算机上所有打印机的名称。

  但是可惜的是,该属性仅仅能够提供已安装的打印机的名称。对于获取该打印机的相关信息(如打印机类型等)却无能为力。问题就产生了,由于客户无法提供打印机的SDK,所以对打印机的筛选(处于商业目的,客户要求软件只能在使用他们的打印机时才能输出)只能通过打印机驱动的辨认来实现。

  

  ----------------------------------------------

  解决方案一 使用WMI获取打印机信息

  WMI,全称Windows Management Instrumentation。是可伸缩的系统管理结构,它采用一个统一的、基于标准的、可扩展的面向对象接口。WMI 为您提供与系统管理信息和基础 WMI API 交互的标准方法。WMI 主要由系统管理应用程序开发人员和管理员用来访问和操作系统管理信息。

  .Net Framework中System.Management类提供了对WMI的支持,其中ManagementObjectSearcher用于根据指定的查询或枚举检索 ManagementObject 或 ManagementClass 对象的集合。

  /**//// <summary>

   /// Code 1:WMI搜索示例

   /// <summary>

   /// <param name="strDrivername">驱动名称</param>

   /// <returns>返回找到的打印机列表</returns>

   /// <remarks>strDrivername支持”%“以及”_“通配符查询,类似于SQL语句中的查询<remarks>

   public StringCollection GetPrintsWithDrivername( string strDrivername )

   {

   StringCollection scPrinters = new StringCollection();

   string strcheck = "";

   if( strDrivername !="" && strDrivername != "*" )

   strcheck = " where DriverName like \'" + strDrivername + "\'";

   string searchQuery = "SELECT Name FROM Win32_Printer" + strcheck;

   ManagementObjectSearcher searchPrinters =

   new ManagementObjectSearcher(searchQuery);

   ManagementObjectCollection printerCollection = searchPrinters.Get();

  

   foreach(ManagementObject printer in printerCollection)

   {

   string printname = printer.Properties["Name"].Value.ToString();

   scPrinters.Add(printname);

   }

   searchPrinters.Dispose();

   printerCollection.Dispose();

   return scPrinters;

   }

  问题看上去基本解决了,运行程序的确是获得了正确的打印机列表。可是用户用了一段时间后发现,有的时候打印机无法正确获得,看来DOTNET调用WMI稳定性的确有点问题啊。。。。。。

  WMI本身功能还是相当强大的,通过VBS基本可以涵盖WINDOWS最基本的操作。详细可以参加MSDN的文档。

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/wmi_start_page.asp

  

  -------------------------------------------

  解决方案二 使用WIN32API获取打印机

  转来转去,又回到WIN32API上来了,无奈啊。。。。。。怪不得C++依然这么吃香 啊。。。。。

  .Net给我们提供了DllImport来操作非托管的DLL(发现C#如此的强啊~~~~暗自偷笑)。

  主要使用到winspool.drv中的EnumPrinters函数,代码如下:

  [DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Auto)]

   [return: MarshalAs(UnmanagedType.Bool)]

   private static extern bool EnumPrinters ([MarshalAs(UnmanagedType.U4)] PRINTER_ENUM flags,

   [MarshalAs(UnmanagedType.LPStr)] string sName,

   uint iLevel,

   IntPtr pPrinterDesc,

   uint iSize,

   [MarshalAs(UnmanagedType.U4)] ref uint iNeeded,

   [MarshalAs(UnmanagedType.U4)] ref uint iReturned

   );

  说明:Marshal属性提供了对托管代码与非托管代码见数据封送。

  EnumPrinters 的 WIN32 API的定义如下:

  BOOL EnumPrinters(

   DWORD Flags, // printer object types

   LPTSTR Name, // name of printer object

   DWORD Level, // information level

   LPBYTE pPrinterEnum, // printer information buffer

   DWORD cbBuf, // size of printer information buffer

   LPDWORD pcbNeeded, // bytes received or required

   LPDWORD pcReturned // number of printers enumerated

  );

  问题又来啦,EnumPrinters通过Level来获取PRINTER_INFO,而能获得打印机驱动的是PRINTER_INFO_2,而C#中又没有PRINTER_INFO_2结构,偶又开始晕了。。。。。

  查了半天资料,网上基本上都是PRINTER_INFO_1的定义,而PRINTER_INFO_2不同与PRINTER_INFO_1,其中还包括DEVMODE结构,非托管的结构套结构,偶开始飘了~~~~

  

  最后发现与其在C#中定义结构来对应非托管的结构,还不如直接用类来替代。所以定义了两个类

  PRINTER_INFO_2以及DEVMODE(注:由于PRINTER_INFO_2中只用到了DEVMODE结构来接收打印机驱动的信息,所以只定义了这个类,对于其他类都没有做具体实现)。

  在PRINTER_INFO_2中,对于所有的DWORD类型数据,全部对应到Int32类型上面,而对于所有LPTSTR、LPDEVMODE以及PSECURITY_DESCRIPTOR一律对应到IntPtr指针类型。

  

  为了获取非托管中的数据,使用了一下函数获取打印机信息

  .

   PRINTER_INFO_2 pi = new PRINTER_INFO_2();

   //把数据从非托管内存传送到到托管内存

   for(int i = 0; i < numPrinters; i++)

   {

   Marshal.PtrToStructure( prInfo, pi ); //prInfo是由上面EnumPrinters获得的打印机

   string driver = Marshal.PtrToStringAuto( pi.pDriverName );

   if ( printerdriver == "" || driver.ToLower().IndexOf( printerdriver ) != -1)

   {

   // 做相关处理

   }

   prInfo = new IntPtr(prInfo.ToInt32() + Marshal.SizeOf(typeof(PRINTER_INFO_2))); // 获取下一个打印机信息段开始

   }

  .

  问题至此基本解决。但C#中对非托管函数的调用,以及相互之间的数据封装还是一个比较难的地方,有空还需要整理一下。

  文章来源:http://spaces.msn.com/sharkoo/Blog/cns!D8E832CE4545AF!158.entry

  补充:在2.0中,fixed关键字可以用于定义一个固定大小的数组缓存,而不是像1.x中那样还需要定义一个数字大小。但这种方式只能用于结构(struct)而不能用于类(class)的定义。

引言前段时间为客户开发一套打印机配套的软件,对C#中调用打印机做了些研究。 --------------------------------------------- 问题 .Net Framework 1.1给我们提供了一个PrinterSettings类,以提供指定有关文档打印方式的信息,其中包括打印文档的打印机。其中的静态属性InstalledPrinters可以使我们获取安装在计算机上所有打印机的名称。但是可惜的是,该属性仅仅能够提供已安装的打印机的名称。对于获取该打印机的相关信息(如打印机类型等)却无能为力。问题就产生了,由于客户无法提供打印机的SDK,所以对打印机的筛选(处于商业目的,客户要求软件只能在使用他们的打印机时才能输出)只能通过打印机驱动的辨认来实现。 ---------------------------------------------- 解决方案一 使用WMI获取打印机信息 WMI,全称Windows Management Instrumentation。是可伸缩的系统管理结构,它采用一个统一的、基于标准的、可扩展的面向对象接口。WMI 为您提供与系统管理信息和基础 WMI API 交互的标准方法。WMI 主要由系统管理应用程序开发人员和管理员用来访问和操作系统管理信息。 .Net Framework中System.Management类提供了对WMI的支持,其中ManagementObjectSearcher用于根据指定的查询或枚举检索 ManagementObject 或 ManagementClass 对象的集合。 /**//// <summary> /// Code 1:WMI搜索示例 /// <summary> /// <param name="strDrivername">驱动名称</param> /// <returns>返回找到的打印机列表</returns> /// <remarks>strDrivername支持”%“以及”_“通配符查询,类似于SQL语句中的查询<remarks> public StringCollection GetPrintsWithDrivername( string strDrivername ) { StringCollection scPrinters = new StringCollection(); string strcheck = ""; if( strDrivername !="" && strDrivername != "*" ) strcheck = " where DriverName like \'" + strDrivername + "\'"; string searchQuery = "SELECT Name FROM Win32_Printer" + strcheck; ManagementObjectSearcher searchPrinters = new ManagementObjectSearcher(searchQuery); ManagementObjectCollection printerCollection = searchPrinters.Get(); foreach(ManagementObject printer in printerCollection) { string printname = printer.Properties["Name"].Value.ToString(); scPrinters.Add(printname); } searchPrinters.Dispose(); printerCollection.Dispose(); return scPrinters; } 问题看上去基本解决了,运行程序的确是获得了正确的打印机列表。可是用户用了一段时间后发现,有的时候打印机无法正确获得,看来DOTNET调用WMI稳定性的确有点问题啊。。。。。。 WMI本身功能还是相当强大的,通过VBS基本可以涵盖WINDOWS最基本的操作。详细可以参加MSDN的文档。 [url=http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/wmi_start_page.asp]http://msdn.microsoft.com/library/default.asp?url=/library/en-us/wmisdk/wmi/wmi_start_page.asp[/url] ------------------------------------------- 解决方案二 使用WIN32API获取打印机 转来转去,又回到WIN32API上来了,无奈啊。。。。。。怪不得C++依然这么吃香 啊。。。。。 .Net给我们提供了DllImport来操作非托管的DLL(发现C#如此的强啊~~~~暗自偷笑)。 主要使用到winspool.drv中的EnumPrinters函数,代码如下: [DllImport("winspool.drv", SetLastError = true, CharSet = CharSet.Auto)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool EnumPrinters ([MarshalAs(UnmanagedType.U4)] PRINTER_ENUM flags, [MarshalAs(UnmanagedType.LPStr)] string sName, uint iLevel, IntPtr pPrinterDesc, uint iSize, [MarshalAs(UnmanagedType.U4)] ref uint iNeeded, [MarshalAs(UnmanagedType.U4)] ref uint iReturned ); 说明:Marshal属性提供了对托管代码与非托管代码见数据封送。 EnumPrinters 的 WIN32 API的定义如下: BOOL EnumPrinters( DWORD Flags, // printer object types LPTSTR Name, // name of printer object DWORD Level, // information level LPBYTE pPrinterEnum, // printer information buffer DWORD cbBuf, // size of printer information buffer LPDWORD pcbNeeded, // bytes received or required LPDWORD pcReturned // number of printers enumerated ); 问题又来啦,EnumPrinters通过Level来获取PRINTER_INFO,而能获得打印机驱动的是PRINTER_INFO_2,而C#中又没有PRINTER_INFO_2结构,偶又开始晕了。。。。。 查了半天资料,网上基本上都是PRINTER_INFO_1的定义,而PRINTER_INFO_2不同与PRINTER_INFO_1,其中还包括DEVMODE结构,非托管的结构套结构,偶开始飘了~~~~ 最后发现与其在C#中定义结构来对应非托管的结构,还不如直接用类来替代。所以定义了两个类 PRINTER_INFO_2以及DEVMODE(注:由于PRINTER_INFO_2中只用到了DEVMODE结构来接收打印机驱动的信息,所以只定义了这个类,对于其他类都没有做具体实现)。 在PRINTER_INFO_2中,对于所有的DWORD类型数据,全部对应到Int32类型上面,而对于所有LPTSTR、LPDEVMODE以及PSECURITY_DESCRIPTOR一律对应到IntPtr指针类型。 为了获取非托管中的数据,使用了一下函数获取打印机信息 . PRINTER_INFO_2 pi = new PRINTER_INFO_2(); //把数据从非托管内存传送到到托管内存 for(int i = 0; i < numPrinters; i++) { Marshal.PtrToStructure( prInfo, pi ); //prInfo是由上面EnumPrinters获得的打印机 string driver = Marshal.PtrToStringAuto( pi.pDriverName ); if ( printerdriver == "" || driver.ToLower().IndexOf( printerdriver ) != -1) { // 做相关处理 } prInfo = new IntPtr(prInfo.ToInt32() + Marshal.SizeOf(typeof(PRINTER_INFO_2))); // 获取下一个打印机信息段开始 } . 问题至此基本解决。但C#中对非托管函数的调用,以及相互之间的数据封装还是一个比较难的地方,有空还需要整理一下。 文章来源:http://spaces.msn.com/sharkoo/Blog/cns!D8E832CE4545AF!158.entry 补充:在2.0中,fixed关键字可以用于定义一个固定大小的数组缓存,而不是像1.x中那样还需要定义一个数字大小。但这种方式只能用于结构(struct)而不能用于类(class)的定义。

 
 
 
 
 
原文地址:https://www.cnblogs.com/zahxz/p/2811533.html