RDLC报

        VS .NET开发中,用什么做报表?

        可能的回答是Crystal Report,自.NET“紧密”集成Crystal Report后,这可能是开发人员比较单一的选择。但是,这种集成似乎并不非常紧密,网络上充斥着关于使用Crystal Report的抱怨,太复杂也许是其最为令人诟病的地方,自定义性比较差也不能为程序员们所容忍。

        当然,必须承认Crystal Report的功能还是非常强大的,被Business Object收购以后,商业职能的成分也在逐渐增加,也形成了一定规模的用户群。

        Visual Studio .NET进入2005版本以后,Crystal Report与IDE的结合更“紧密”了,至少我们看不到那个讨厌的注册窗口了。但是,Microsoft似乎并不容忍在自己的超级工具中竟然没有报表工具,于是Report Viewer Control出现了,我把它的报表称为RDLC报表。

        在VS .NET 2005之前,SQL Server Reporting Services中已经提供了一种被称为报表定义语言(Report Definition Language, RDL)的语言;在VS .NET 2005中,Microsoft提供了针对这种报表的设计器,并提供了在WinForm和WebForm中使用这种报表的能力。Microsoft将这种报表的后缀定为RDLC,RDL仍然是Report Definition Language的缩写,那么C代表什么呢?C代表Client-side processing,凸显了它的客户端处理能力。

        这种报表的易用性和可定制性让我们完全有理由放弃Crystal Report,让我们来看看它的强大功能吧:
            1、简单易用的控件,特别是Table控件,非常方便字段在报表上的排列;
            2、灵活的可定制性,用XML来描述一个报表相关的一切,不用说了吧?
            3、高度可编程性,在你的项目中,甚至不需要有一个报表文件,通过代码就可以实现报表生成、预览和打印等一系列操作;
            4、支持DrillThrough数据钻取功能;
            5、导出的Excel文件格式非常完美,任何其它报表在这方面都不能与之比拟,而且并不需要安装Excel;
          需要说明的是,现在关于VS. NET 2005中的Report Viewer Control的内容非常少,我只能按照自己的理解来说一些东西,这其中肯定会有一些偏差,本文中所涉及的内容都是关于LocalReport的,对于ServerReport没有进行研究。

        下面分析一下GotReportViewer上的几个经典例子,我们可以从中看到ReportViewer Control的强大功能: 

        1、Web Log Analyzer 
   
        这是一个比较典型的OLAP应用,我们可以看到RDLC报表强大的Chart和Navigation功能。当然了,例子中解析W3C标准日志文件的代码也非常有借鉴意义。这个Starter Kit在我的随笔http://waxdoll.cnblogs.com/archive/2006/01/19/320280.html中曾经提到过,不再详述。 

        2、子报表 

        展示如何使用子报表显示主记录的详细信息,这种应用很像Access中的子报表功能。主要使用SubreportProcessing事件为子报表提供数据。 

        3、钻取报表 

        钻取报表是通过设置Navigation(HyperLink)和Parameters来实现的,通常在OLAP应用程序中很有用。 

        4、具有子报表的钻取报表 

        这个例子实现的功能类似Excel中数据透视表(Pivot Table)的功能,在一个复杂的交叉表中可以进行时间和商品两个维度的向下钻取。这在别的报表中恐怕是很难实现的。如图所示, 


        5、引用外部代码块

 

        此示例演示从另外一个类Util中读取函数返回值到报表中:首先,使用LocalReport的AddTrustedCodeModuleInCurrentAppDomain方法允许Util类中的方法在Report Viewer中执行,然后在报表中使用TextBox控件的Value节点调用Util中的静态方法Factorial在报表中显示其返回值。
        另外一个相关的示例基本上与此相同,只不过在Util类中访问一个文本文件并将该文本文件中的字符显示在报表中

        6、导出到Excel

        RDLC报表导出到Excel中的效果非常好,曾经看到另外一个报表设计器(好像就是Crystal Report)导出为Excel文件后的效果非常差,单元格根本不对齐,用户无法在其基础上进行二次操作,而RDLC报表导出的Excel文件就没有这个缺点,而且基本上完全保留了原报表设置的格式。如图所示,

        可以直接使用Report Viewer控件自带的按钮生成Excel文件,也可以使用如下代码来完成操作:

 

Microsot.Reporting.WinForms.Warning[] Warnings; 
string[] strStreamIds; 
string strMimeType; 
string strEncoding; 
string strFileNameExtension;

byte[] bytes = this.rptViewer.LocalReport.Render("Excel"nullout strMimeType, out strEncoding, out strFileNameExtension, out strStreamIds, out Warnings);

string strFilePath = @"D:\report.xls";

using (System.IO.FileStream fs = new FileStream(strFilePath, FileMode.Create)) 

fs.Write(bytes, 
0, bytes.Length); 
}

 


        对于LocalReport的Render方法,以后的随笔中将进行详细阐述。

        7、交互排序

        在RDLC报表的众多交互功能中,这个功能是比较新颖的,终端用户可以通过报表中列标题上的图标进行数据的排序,而预览及打印的效果完全取决于用户的排序。[此处好像有一个Bug,即对数据进行一种排序后的预览效果会保持不变,除非再次开启这个应用程序并重新排序。]而这一切不需要在代码中做任何操作,只需要在报表定义文件中添加<UserSort>节并指定相应的SortExpression和SortExpressionScope。

        8、RSS新闻阅读器

        用ReportViewer实现RSS新闻阅读器?是的!如图所示,

        当然这个sample是为了展示对Object类型数据源的支持,报表参数ReportParameter的使用也在其中得到体现。

 

        9、允许钻取的主子表

        另外一种允许向下钻取的主子报表。如图所示,

        10、从命令行中打印报表

        RDLC报表允许用户不通过ReportViewer图形界面直接使用代码控制输出和打印,还是使用LocalReport的Render方法,以后的随笔中将参照这个示例介绍一个如何自定义纸张的方法。

        11、票据生成

        这个示例允许用户输入一个单据及其明细后直接生成一个可供打印的票据。如图所示,

        12、动态生成一个RDLC文件

        这可能是一个最最重要的示例了,RDLC文件是用XML来描述的,可以直接使用代码生成之,这样的报表就可以非常灵活了。像网上比较流行的从DataGridView直接预览、打印数据的程序完全可以用这种方法来替代;进一步引申的话,完全可以实现一个自己的基于RDLC的报表设计器,这样可以让终端用户参与到报表的设计中,至少可以使他们能够修改报表中一些标题、表头等。

        GotReportViewer还提供了几个其他的例子,如设置报表参数、通过Email邮寄报表等,由于不是非常典型,不再赘述。

        GotReportViwer不知道为什么最近上不去了,有需要这些例程的朋友可以留下Email。

        仔细想了一下,我觉得一篇step by step的随笔似乎是不必要的,由于RDLC报表设计时的简易性,任何有报表经验的人都可以在摸索后很容易就掌握其报表的设计方法。本来在这篇随笔中想谈一下对RDLC报表文件的解析,但是MISGoldPrinter的作者flygoldfish(长流支流)已经对这方面进行了详细的总结(见http://blog.csdn.net/flygoldfish/archive/2005/12/16/554035.aspx),长江支流对报表非常有研究,建议大家到他的Blog上看看,不过我觉得他实现的金质打印通完全可以用RDLC报表中的内容所替代,这只是个人意见,希望以后能见到他的更多作品。

        另外,我手头有一份RDL规范(Report Definition Language Specification),非常值得阅读,推荐给大家https://files.cnblogs.com/waxdoll/RDLCS.rar。两幅截图:


Matrix


Table 

        本来打算不要这篇随笔了,因为没有什么具体的内容,但为了保证完整性,还是放在这吧,不过我不放在首页了,免得污染大家的视线
        RDLC报表中有一个概念叫数据区域(Data Region),数据区域是数据绑定的报表项目,在数据区域中可以显示来自数据集的多行数据。RDLC报表设计器中的数据区域包含控件面板上的一系列控件:List、Table、Matrix、Chart,如图所示,

        List控件的用处在于这是一个在其中可以自由安排像TextBox、Image等控件;Chart控件用于显示图表,和Excel中的图表比较相似。这篇随笔不会涉及到这两个控件,主要讲一下Table和Matrix两个控件。

        先来看Table控件。Table控件有多个部分组成,如标题行(header)、表尾(footer)、数据行(detailed rows)、分组表头(group header)、分组表尾(group footer)等,如图所示,

        之所以设计这样一个控件,我想Microsoft一定是在简化报表的设计:

        在这个控件未出现之前,我们看一下一个具有heaer、detailed rows和带统计信息的footer的报表时如何实现的。首先,这个报表需要显示报表页眉、主体和报表页脚三个部分,然后在报表页眉中拖曳进一系列Label形成表格的标题行,接着在主体部分拖曳进一系列TextBox用于显示数据行,并在报表页脚中拖曳进一系列Label或TextBox用于显示统计信息;要命的是,这些Label或TextBox需要在某个方向上进行对齐,控件宽度的调节也非常麻烦,如果需要显示表格线,那么没办法,需要使用Line控件手工画,这是非常麻烦的一件差事,相信没人会喜欢用这么麻烦的方法来设计这么一个简单的报表。

        现在,Microsoft推出了Table控件,你需要做的只是确定表格的列数(因为表格的列数需要是固定的),然后将字段从Data Sources面板中拖曳到Table控件的数据行中就可以了,报表设计器会自动为你生成标题行中的标题信息;至于footer中的统计信息Microsoft甚至为我们设计了一个表达式生成器,使用起来非常简便;列宽可以通过拖曳来调整,表格线可以通过设置Table控件的属性来完成。

        是的,一切就是这么简单!这也很可能是为什么你在Visual Studio 2005的报表设计器中找不到报表页眉和报表页脚的原因,只有页面页眉(page header)和页面页脚(page footer)就足够了!

        当然,可能报表中也需要不是像表格这么整齐排列的数据区域,这时候就需要用到List控件了。

        在实际应用中,发现一个表格控件可以具有零个、一个或多个header或footer,甚至可以没有detailed rows,这样的表格控件有什么作用呢?我们知道detailed rows是用来显示多行数据的,而heaer或footer都可以用来显示sum、count等统计信息。假如我们的数据集中同时具有主子表(如通过Inner Join获得的一个查询)的信息,而这时候我们需要将主报表的信息单独显示出来使整个报表形成一个主子表的样式,那么我们就可以用到没有detailed rows的Table控件了,为header或footer中的单元格指定First(Fields!字段名称.Value, "数据源名称")就可以了,这样至少减少了我们排列这些字段信息的烦恼。也就是说,Table控件是非常灵活的。CodeProject上有一篇文章One to Many Reports with VS.NET 2005 (2.0) Report Designer(http://www.codeproject.com/dotnet/1tomanyreports_vsnet2005.asp)即是用这种主子数据集显示的主子报表,但是个人觉得不如上面描述的方法来的简单,顶多给报表增加一个可以标识主记录的参数而已。当然,正儿八经的主子报表还是需要借助SubReport控件来实现。

        再来看一下Matrix控件,这个控件可以看作是Microsoft的又一个创新,以前的报表中可能会有交叉表(crosstab),但Matrix控件反映的不只是一个交叉表,还可以看作是一个带钻取功能的数据透视表(pivot table)。Matrix控件由以下部分组成:


        如果由我们自己使用一个普通的数据集来绘制一个crosstab,那会是一个非常麻烦的工作,我们需要:为数据透视报表设置行标题;计算可能的列标题数量并设置列标题,根据行标题和列标题在数据集中循环查询由当前行标题和列标题决定的值,整个过程的计算量就够受的了。作为被Microsoft封装过的一个控件,Matrix控件显然不需要这么麻烦,简单的拖曳操作并设置其属性就可以了。当然,在报表中使用交叉表最重要的一点是最终显示的结果必须是有意义的。

        需要注意的是,当包含Matrix控件的报表导出到Excel文件中以后,即使是未显示的带有钻取标志的区域也将被显示出来,可能你会有这样的疑问:既然是数据透视表,为什么在Excel中不能显示成折叠的样式呢?这是因为报表的导出功能是并不依赖于Excel的。 

      随着Visual Studio 2005中文版的推出,Microsoft汉化了MSDN的大部分内容,开发者再也不用啃英文了,本来想介绍一下LocalReport的Render方法,现在您可以到http://msdn2.microsoft.com/zh-cn/library/ms252207(VS.80).aspx获得关于这部分的详细信息。之所以以前想介绍这个方法,是因为我将想大家介绍一种在Crystal Report中无法实现的自定义票据打印纸张的方法。Anyway,现在我直接向大家介绍这种方法,可能这种方法并不是很好的,但是确实是我经过一段时间的摸索总结出来的。萝卜(http://luobos.cnblogs.com/)曾经提到过的变通的方法不知道是不是我要介绍的这一种,欢迎和我进行交流!

        要想使用RDLC报表并进行页面设置,我们先来看一下LocalReport是否有类似PageSettings的类、属性、方法或事件等,我仔细找了一下,发现Microsoft.Reporting.WinForms.ReportPageSettings类具有PaperSize属性和Margin属性,但可惜的是它们都是只读的,对我们来说没有意义;另外,LocalReport具有GetDefaultPageSettings()方法,这也只能是获取当前报表的页面设置。没办法,只能采用变通的方法了。在.NET中如果想使用自定义纸张,最好的方法莫过于使用System.Drawing.Printing.PrintDocument类了,还记得我在前面提到的一个GotReportViewer的例子吗?

    private int m_currentPageIndex;
    
private IList<Stream> m_streams;

    
private Stream CreateStream(string name, string fileNameExtension, Encoding encoding, string mimeType, bool willSeek)
    
{
        Stream stream 
= new FileStream(name + "." + fileNameExtension, FileMode.Create);
        m_streams.Add(stream);
        
return stream;
    }


    
private void Export(LocalReport report)
    
{
        
string deviceInfo =
          
"<DeviceInfo>" +
          
"  <OutputFormat>EMF</OutputFormat>" + 
          
"  <PageWidth>8.5in</PageWidth>" +
          
"  <PageHeight>11in</PageHeight>" +
          
"  <MarginTop>0.25in</MarginTop>" +
          
"  <MarginLeft>0.25in</MarginLeft>" +
          
"  <MarginRight>0.25in</MarginRight>" +
          
"  <MarginBottom>0.25in</MarginBottom>" +
          
"</DeviceInfo>";
        Warning[] warnings;
        m_streams 
= new List<Stream>();
        report.Render(
"Image", deviceInfo, CreateStream, out warnings);

        
foreach (Stream stream in m_streams)
            stream.Position 
= 0;
    }


    
private void PrintPage(object sender, PrintPageEventArgs ev)
    
{
        Metafile pageImage 
= new Metafile(m_streams[m_currentPageIndex]);
        ev.Graphics.DrawImage(pageImage, ev.PageBounds);

        m_currentPageIndex
++;
        ev.HasMorePages 
= (m_currentPageIndex < m_streams.Count);
    }


    
private void Print()
    
{
        
const string printerName = "Microsoft Office Document Image Writer";

        
if (m_streams == null || m_streams.Count == 0)
            
return;

        PrintDocument printDoc 
= new PrintDocument(); 
        printDoc.PrinterSettings.PrinterName 
= printerName;
        
if (!printDoc.PrinterSettings.IsValid)
        
{
            
string msg = String.Format("Can't find printer \"{0}\".", printerName);
            Console.WriteLine(msg);
            
return;
        }

        printDoc.PrintPage 
+= new PrintPageEventHandler(PrintPage);
        printDoc.Print();
    }


    
private void Run()
    
{
        LocalReport report 
= new LocalReport();
        report.ReportPath 
= "Report.rdlc";
        report.DataSources.Add(
new ReportDataSource("Sales", LoadSalesData()));

        Export(report);

        m_currentPageIndex 
= 0;
        Print();
    }

        对,就是那个通过命令行而不是ReportViewer的GUI界面进行打印报表的例子,这个例子就使用LocalReport的Render方法将报表的内容导出为EMF图像流,然后在PrintDocument的PrintPage事件中使用时事件参数System.Drawing.Printing.PrintEventArgs类的DrawImage方法将EMF图像流输出到打印机。我在上面说的变通的方法也要使用这种方法。具体的细节将在以后的随笔中陆续给出。


        既然我们使用这种方法进行报表的打印,那么Visual Studio的控件ReportViewer的工具栏就不再符合我们的要求了。因为这个报表浏览器的工具栏上的按钮虽然可以设置属性显示或隐藏其中的一部分,但是我们却不能自己往这个工具栏上添加按钮(显然,我们需要实现自己的页面设置、预览和打印按钮),在这一点上,建议Microsoft将工具栏和报表浏览器分离,应该做得和BindingNavigator那样就好了。

        我们先设置ReportViewer控件的ShowToolBar方法为false,然后在ReportViewer控件纸上添加除页面设置、预览、打印外的应该有的按钮,像刷新、终止、导出、缩放、搜索、导航等,这些按钮的Click事件定义如下:

        /// <summary>
        
/// 获取当前时间组成的字符串,用作生成不会重复的文件名
        
/// </summary>
        
/// <returns></returns>

        private string GetTimeStamp()
        
{
            
string strRet = string.Empty;
            System.DateTime dtNow 
= Pub.DateTimeEx.ServerTime;
            strRet 
+=   dtNow.Year.ToString() +
                        dtNow.Month.ToString(
"00"+
                        dtNow.Day.ToString(
"00"+
                        dtNow.Hour.ToString(
"00"+
                        dtNow.Minute.ToString(
"00"+
                        dtNow.Second.ToString(
"00"+
                        System.DateTime.Now.Millisecond.ToString(
"000");
            
return strRet;

        }


        
/// <summary>
        
/// 导出到Excel
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void toolExcel_Click(object sender, EventArgs e)
        
{

            Microsoft.Reporting.WinForms.Warning[] Warnings;
            
string[] strStreamIds;
            
string strMimeType;
            
string strEncoding;
            
string strFileNameExtension;

            
byte[] bytes = this.rptViewer.LocalReport.Render("Excel"nullout strMimeType, out strEncoding, out strFileNameExtension, out strStreamIds, out Warnings);

            
string strFilePath = @"D:\" + this.GetTimeStamp() + ".xls";

            
using (System.IO.FileStream fs = new FileStream(strFilePath, FileMode.Create))
            
{
                fs.Write(bytes, 
0, bytes.Length);
            }


            
if (Pub.WinForm.Msg.Question("报表打印: \r\n    成功导出Excel文件!" + strFilePath + "\r\n    要现在打开文件" + strFilePath + "吗?"== DialogResult.Yes)
            
{
                System.Diagnostics.Process.Start(strFilePath);
            }


        }


        
/// <summary>
        
/// 刷新报表数据
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool刷新_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.RefreshReport();
        }


        
/// <summary>
        
/// 在加载报表数据时终止报表数据的加载
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool终止_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.CancelRendering(0);
        }


        
/// <summary>
        
/// 从DrillThrough报表返回到导航页面
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool返回_Click(object sender, EventArgs e)
        
{
            
if (this.rptViewer.LocalReport.IsDrillthroughReport)
                
this.rptViewer.PerformBack();
        }


        
/// <summary>
        
/// 回到报表的第一页
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool第一页_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.CurrentPage = 1;
        }


        
/// <summary>
        
/// 跳转到报表的最后一页
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool最后一页_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.CurrentPage = this.rptViewer.LocalReport.GetTotalPages();
        }


        
/// <summary>
        
/// 以25%的比例显示报表
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool25_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.ZoomMode = ZoomMode.Percent;
            
this.rptViewer.ZoomPercent = 25;
        }


        
/// <summary>
        
/// 以50%的比例显示报表
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool50_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.ZoomMode = ZoomMode.Percent;
            
this.rptViewer.ZoomPercent = 50;
        }


        
/// <summary>
        
/// 以100%的比例显示报表
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool100_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.ZoomMode = ZoomMode.Percent;
            
this.rptViewer.ZoomPercent = 100;
        }


        
/// <summary>
        
/// 以200%的比例显示报表
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool200_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.ZoomMode = ZoomMode.Percent;
            
this.rptViewer.ZoomPercent = 200;
        }


        
/// <summary>
        
/// 以400%的比例显示报表
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool400_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.ZoomMode = ZoomMode.Percent;
            
this.rptViewer.ZoomPercent = 400;
        }


        
/// <summary>
        
/// 将缩放模式设置为整页
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool整页_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.ZoomMode = ZoomMode.FullPage;
        }


        
/// <summary>
        
/// 将缩放模式设置为页宽
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool页宽_Click(object sender, EventArgs e)
        
{
            
this.rptViewer.ZoomMode = ZoomMode.PageWidth;
        }


        
/// <summary>
        
/// 在报表中搜索txtSearch中的字符
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool搜索_Click(object sender, EventArgs e)
        
{
            
if (this.txtSearch.Text.Trim() == string.Empty)
                
return;

            
this.rptViewer.Find(this.txtSearch.Text.Trim(), 1);
        }


        
/// <summary>
        
/// 搜索报表中下一处txtSearch中的字符
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool搜索下一个_Click(object sender, EventArgs e)
        
{
            
if (this.txtSearch.Text.Trim() == string.Empty)
                
return;

            
this.rptViewer.FindNext();
        }


        
/// <summary>
        
/// 跳转到上一页
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool上一页_Click(object sender, EventArgs e)
        
{
            
if (this.rptViewer.CurrentPage != 1)
                
this.rptViewer.CurrentPage--;
        }


        
/// <summary>
        
/// 跳转到下一页
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool下一页_Click(object sender, EventArgs e)
        
{
            
if (this.rptViewer.CurrentPage != this.rptViewer.LocalReport.GetTotalPages())
                
this.rptViewer.CurrentPage++;
        }


        
/// <summary>
        
/// 跳转到由txt跳转中指定的页数
        
/// </summary>
        
/// <param name="sender"></param>
        
/// <param name="e"></param>

        private void tool跳转_Click(object sender, EventArgs e)
        
{
            
if (this.txt跳转.Text.Trim() == string.Empty)
                
return;

            
int intJump = 0;

            
if (System.Int32.TryParse(this.txt跳转.Text.Trim(), out intJump))
                
if (intJump <= this.rptViewer.LocalReport.GetTotalPages())
                    
this.rptViewer.CurrentPage = intJump;

        }

        有关LocalReport、DeviceInfo和PrintDocument的内容已经介绍得差不多了,稍后会给出一个继承自System.Drawing.Printing.PrintDocument的组件EMFStreamPrintDocument。但是现在,来看一下如何进行自定义纸张票据打印时的页面设置。页面设置窗体如下图所示:

        如何添加、删除自定义大小的纸张、枚举系统的打印机?以前在博客园的一篇随笔中参加过讨论,见http://wormday.cnblogs.com/archive/2005/12/22/302635.aspx。当然还是使用Win32 API,以下是我封装的一个关于打印机控制的类[以前用VB实现过比这个类还要多的关于打印机控制的功能,但是在C#中感到还是挺困难的,所以这次只给出了够用的功能:获取当前指定打印机的状态、删除已经存在的自定义纸张、指定的打印机设置以mm为单位的自定义纸张(Form)、获取本地打印机列表、获取本机的默认打印机名称、设置默认打印机、判断打印机是否在系统可用的打印机列表中、判断表单是否在指定的打印机所支持的纸张列表中、判断指定纸张的宽度和高度和在文本框中指定的宽度和高度是否匹配、英尺到厘米的转换]:

 

 

using System;
using System.Text;
using System.Runtime.InteropServices;
using System.Security;
using System.ComponentModel;
using System.Drawing.Printing;

namespace RDLCReport
{
 
    
public class Printer
    
{
        
private Printer()
        
{

        }


        
API声明
        
        
internal static int GetPrinterStatusInt(string PrinterName)
        
{
            
int intRet = 0;
            IntPtr hPrinter;
            structPrinterDefaults defaults 
= new structPrinterDefaults();

            
if (OpenPrinter(PrinterName, out hPrinter, ref defaults))
            
{
                
int cbNeeded = 0;
                
bool bolRet = GetPrinter(hPrinter, 2, IntPtr.Zero, 0out cbNeeded);
                
if (cbNeeded > 0)
                
{
                    IntPtr pAddr 
= Marshal.AllocHGlobal((int)cbNeeded);
                    bolRet 
= GetPrinter(hPrinter, 2, pAddr, cbNeeded, out cbNeeded);
                    
if (bolRet)
                    
{
                        PRINTER_INFO_2 Info2 
= new PRINTER_INFO_2();
                        
                        Info2 
= (PRINTER_INFO_2)Marshal.PtrToStructure(pAddr, typeof(PRINTER_INFO_2));
                        
                        intRet 
= System.Convert.ToInt32(Info2.Status);
                    }

                    Marshal.FreeHGlobal(pAddr);
                }

                ClosePrinter(hPrinter);
            }


            
return intRet;
        }


        
internal static PRINTER_INFO_2[] EnumPrintersByFlag(PrinterEnumFlags Flags)
        
{
            
uint cbNeeded = 0;
            
uint cReturned = 0;
            
bool ret = EnumPrinters( PrinterEnumFlags.PRINTER_ENUM_LOCAL, null2, IntPtr.Zero, 0ref cbNeeded, ref cReturned);

            IntPtr pAddr 
= Marshal.AllocHGlobal((int)cbNeeded);
            ret 
= EnumPrinters(PrinterEnumFlags.PRINTER_ENUM_LOCAL, null2, pAddr, cbNeeded, ref cbNeeded, ref cReturned);

            
if (ret)
            
{
                PRINTER_INFO_2[] Info2 
= new PRINTER_INFO_2[cReturned];

                
int offset = pAddr.ToInt32();

                
for (int i = 0; i < cReturned; i++)
                
{
                    Info2[i].pServerName 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pPrinterName 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pShareName 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pPortName 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pDriverName 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pComment 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pLocation 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pDevMode 
= Marshal.ReadIntPtr(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].pSepFile 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pPrintProcessor 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pDatatype 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pParameters 
= Marshal.PtrToStringAuto(Marshal.ReadIntPtr(new IntPtr(offset)));
                    offset 
+= 4;
                    Info2[i].pSecurityDescriptor 
= Marshal.ReadIntPtr(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].Attributes 
= (uint )Marshal.ReadIntPtr(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].Priority 
= (uint)Marshal.ReadInt32(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].DefaultPriority 
= (uint)Marshal.ReadInt32(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].StartTime 
= (uint)Marshal.ReadInt32(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].UntilTime 
= (uint)Marshal.ReadInt32(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].Status 
= (uint)Marshal.ReadInt32(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].cJobs 
= (uint)Marshal.ReadInt32(new IntPtr(offset));
                    offset 
+= 4;
                    Info2[i].AveragePPM 
= (uint)Marshal.ReadInt32(new IntPtr(offset));
                    offset 
+= 4;

                }


                Marshal.FreeHGlobal(pAddr);

                
return Info2;

            }

            
else
            
{
                
return new PRINTER_INFO_2[0];
            }

        }

                
        
/// <summary>
        
/// 获取当前指定打印机的状态
        
/// </summary>
        
/// <param name="PrinterName">打印机名称</param>
        
/// <returns>打印机状态描述</returns>

        public static string GetPrinterStatus(string PrinterName)
        
{
            
int intValue = GetPrinterStatusInt(PrinterName);
            
string strRet = string.Empty;
            
switch (intValue)
            
{
                
case 0:
                    strRet 
= "准备就绪(Ready)";
                    
break;
                
case 0x00000200:
                    strRet 
= "忙(Busy)";
                    
break;
                
case 0x00400000:
                    strRet 
= "门被打开(Printer Door Open)";
                    
break;
                
case 0x00000002:
                    strRet 
= "错误(Printer Error)";
                    
break;
                
case 0x0008000:
                    strRet 
= "正在初始化(Initializing)";
                    
break;
                
case 0x00000100:
                    strRet 
= "正在输入或输出(I/O Active)";
                    
break;
                
case 0x00000020:
                    strRet 
= "手工送纸(Manual Feed)";
                    
break;
                
case 0x00040000:
                    strRet 
= "无墨粉(No Toner)";
                    
break;
                
case 0x00001000:
                    strRet 
= "不可用(Not Available)";
                    
break;
                
case 0x00000080:
                    strRet 
= "脱机(Off Line)";
                    
break;
                
case 0x00200000:
                    strRet 
= "内存溢出(Out of Memory)";
                    
break;
                
case 0x00000800:
                    strRet 
= "输出口已满(Output Bin Full)";
                    
break;
                
case 0x00080000:
                    strRet 
= "当前页无法打印(Page Punt)";
                    
break;
                
case 0x00000008:
                    strRet 
= "塞纸(Paper Jam)";
                    
break;
                
case 0x00000010:
                    strRet 
= "打印纸用完(Paper Out)";
                    
break;
                
case 0x00000040:
                    strRet 
= "纸张问题(Page Problem)";
                    
break;
                
case 0x00000001:
                    strRet 
= "暂停(Paused)";
                    
break;
                
case 0x00000004:
                    strRet 
= "正在删除(Pending Deletion)";
                    
break;
                
case 0x00000400:
                    strRet 
= "正在打印(Printing)";
                    
break;
                
case 0x00004000:
                    strRet 
= "正在处理(Processing)";
                    
break;
                
case 0x00020000:
                    strRet 
= "墨粉不足(Toner Low)";
                    
break;
                
case 0x00100000:
                    strRet 
= "需要用户干预(User Intervention)";
                    
break;
                
case 0x20000000:
                    strRet 
= "等待(Waiting)";
                    
break;
                
case 0x00010000:
                    strRet 
= "正在准备(Warming Up)";
                    
break;
                
default:
                    strRet 
= "未知状态(Unknown Status)";
                    
break;
            }

            
return strRet;
        }

        
        
/// <summary>
        
/// 删除已经存在的自定义纸张
        
/// </summary>
        
/// <param name="PrinterName">打印机名称</param>
        
/// <param name="PaperName">纸张名称</param>

        public static void DeleteCustomPaperSize(string PrinterName, string PaperName)
        
{
            
const int PRINTER_ACCESS_USE = 0x00000008;
            
const int PRINTER_ACCESS_ADMINISTER = 0x00000004;

            structPrinterDefaults defaults 
= new structPrinterDefaults();
            defaults.pDatatype 
= null;
            defaults.pDevMode 
= IntPtr.Zero;
            defaults.DesiredAccess 
= PRINTER_ACCESS_ADMINISTER | PRINTER_ACCESS_USE;

            IntPtr hPrinter 
= IntPtr.Zero;

            
//打开打印机
            if (OpenPrinter(PrinterName, out hPrinter, ref defaults))
            
{
                
try
                
{
                    DeleteForm(hPrinter, PaperName);
                    ClosePrinter(hPrinter);
                }

                
catch
                
{
                    Pub.WinForm.Msg.Warning(
"删除自定义纸张时发生错误!");
                }

            }

        }

        
        
/// <summary>
        
/// 指定的打印机设置以mm为单位的自定义纸张(Form)
        
/// </summary>
        
/// <param name="PrinterName">打印机名称</param>
        
/// <param name="PaperName">Form名称</param>
        
/// <param name="WidthInMm">以mm为单位的宽度</param>
        
/// <param name="HeightInMm">以mm为单位的高度</param>

        public static void AddCustomPaperSize(string PrinterName, string PaperName, float WidthInMm, float HeightInMm)
        
{
            
if (PlatformID.Win32NT == Environment.OSVersion.Platform)
            
{
                
const int PRINTER_ACCESS_USE = 0x00000008;
                
const int PRINTER_ACCESS_ADMINISTER = 0x00000004;
                
const int FORM_PRINTER = 0x00000002;

                structPrinterDefaults defaults 
= new structPrinterDefaults();
                defaults.pDatatype 
= null;
                defaults.pDevMode 
= IntPtr.Zero;
                defaults.DesiredAccess 
= PRINTER_ACCESS_ADMINISTER | PRINTER_ACCESS_USE;

                IntPtr hPrinter 
= IntPtr.Zero;

                
//打开打印机
                if (OpenPrinter(PrinterName, out hPrinter, ref defaults))
                
{
                    
try
                    
{
                        
//如果Form存在删除之
                        DeleteForm(hPrinter, PaperName);
                        
//创建并初始化FORM_INFO_1
                        FormInfo1 formInfo = new FormInfo1();
                        formInfo.Flags 
= 0;
                        formInfo.pName 
= PaperName;
                        formInfo.Size.width 
= (int)(WidthInMm * 1000.0);
                        formInfo.Size.height 
= (int)(HeightInMm * 1000.0);
                        formInfo.ImageableArea.left 
= 0;
                        formInfo.ImageableArea.right 
= formInfo.Size.width;
                        formInfo.ImageableArea.top 
= 0;
                        formInfo.ImageableArea.bottom 
= formInfo.Size.height;
                        
if (!AddForm(hPrinter, 1ref formInfo))
                        
{
                            StringBuilder strBuilder 
= new StringBuilder();
                            strBuilder.AppendFormat(
"向打印机 {1} 添加自定义纸张 {0} 失败!错误代号:{2}",
                                PaperName, PrinterName, GetLastError());
                            
throw new ApplicationException(strBuilder.ToString());
                        }


                        
//初始化
                        const int DM_OUT_BUFFER = 2;
                        
const int DM_IN_BUFFER = 8;
                        structDevMode devMode 
= new structDevMode();
                        IntPtr hPrinterInfo, hDummy;
                        PRINTER_INFO_9 printerInfo;
                        printerInfo.pDevMode 
= IntPtr.Zero;
                        
int iPrinterInfoSize, iDummyInt;


                        
int iDevModeSize = DocumentProperties(IntPtr.Zero, hPrinter, PrinterName, IntPtr.Zero, IntPtr.Zero, 0);

                        
if (iDevModeSize < 0)
                            
throw new ApplicationException("无法取得DEVMODE结构的大小!");

                        
//分配缓冲
                        IntPtr hDevMode = Marshal.AllocCoTaskMem(iDevModeSize + 100);

                        
//获取DEV_MODE指针
                        int iRet = DocumentProperties(IntPtr.Zero, hPrinter, PrinterName, hDevMode, IntPtr.Zero, DM_OUT_BUFFER);

                        
if (iRet < 0)
                            
throw new ApplicationException("无法获得DEVMODE结构!");

                        
//填充DEV_MODE
                        devMode = (structDevMode)Marshal.PtrToStructure(hDevMode, devMode.GetType());


                        devMode.dmFields 
= 0x10000;

                        
//FORM名称
                        devMode.dmFormName = PaperName;

                        Marshal.StructureToPtr(devMode, hDevMode, 
true);

                        iRet 
= DocumentProperties(IntPtr.Zero, hPrinter, PrinterName,
                                 printerInfo.pDevMode, printerInfo.pDevMode, DM_IN_BUFFER 
| DM_OUT_BUFFER);

                        
if (iRet < 0)
                            
throw new ApplicationException("无法为打印机设定打印方向!");

                        GetPrinter(hPrinter, 
9, IntPtr.Zero, 0out iPrinterInfoSize);
                        
if (iPrinterInfoSize == 0)
                            
throw new ApplicationException("调用GetPrinter方法失败!");

                        hPrinterInfo 
= Marshal.AllocCoTaskMem(iPrinterInfoSize + 100);

                        
bool bSuccess = GetPrinter(hPrinter, 9, hPrinterInfo, iPrinterInfoSize, out iDummyInt);

                        
if (!bSuccess)
                            
throw new ApplicationException("调用GetPrinter方法失败!");

                        printerInfo 
= (PRINTER_INFO_9)Marshal.PtrToStructure(hPrinterInfo, printerInfo.GetType());
                        printerInfo.pDevMode 
= hDevMode;

                        Marshal.StructureToPtr(printerInfo, hPrinterInfo, 
true);

                        bSuccess 
= SetPrinter(hPrinter, 9, hPrinterInfo, 0);

                        
if (!bSuccess)
                            
throw new Win32Exception(Marshal.GetLastWin32Error(), "调用SetPrinter方法失败,无法进行打印机设置!");

                        SendMessageTimeout(
                           
new IntPtr(HWND_BROADCAST),
                           WM_SETTINGCHANGE,
                           IntPtr.Zero,
                           IntPtr.Zero,
                           Printer.SendMessageTimeoutFlags.SMTO_NORMAL,
                           
1000,
                           
out hDummy);
                    }

                    
finally
                    
{
                        ClosePrinter(hPrinter);
                    }

                }

                
else
                
{
                    StringBuilder strBuilder 
= new StringBuilder();
                    strBuilder.AppendFormat(
"无法打开打印机{0}, 错误代号: {1}",
                        PrinterName, GetLastError());
                    
throw new ApplicationException(strBuilder.ToString());
                }

            }

            
else
            
{
                structDevMode pDevMode 
= new structDevMode();
                IntPtr hDC 
= CreateDC(null, PrinterName, nullref pDevMode);
                
if (hDC != IntPtr.Zero)
                
{
                    
const long DM_PAPERSIZE = 0x00000002L;
                    
const long DM_PAPERLENGTH = 0x00000004L;
                    
const long DM_PAPERWIDTH = 0x00000008L;
                    pDevMode.dmFields 
= (int)(DM_PAPERSIZE | DM_PAPERWIDTH | DM_PAPERLENGTH);
                    pDevMode.dmPaperSize 
= 256;
                    pDevMode.dmPaperWidth 
= (short)(WidthInMm * 1000.0);
                    pDevMode.dmPaperLength 
= (short)(HeightInMm * 1000.0);
                    ResetDC(hDC, 
ref pDevMode);
                    DeleteDC(hDC);
                }

            }

        }


        
/// <summary>
        
/// 获取本地打印机列表
        
/// 可以通过制定参数获取网络打印机
        
/// </summary>
        
/// <returns>打印机列表</returns>

        public static System.Collections.ArrayList GetPrinterList()
        
{
            System.Collections.ArrayList alRet 
= new System.Collections.ArrayList();
            PRINTER_INFO_2[] Info2 
= EnumPrintersByFlag(PrinterEnumFlags.PRINTER_ENUM_LOCAL);
            
for (int i = 0; i < Info2.Length; i++)
            
{
                alRet.Add(Info2[i].pPrinterName);
            }

            
return alRet;
        }

                
        
/// <summary>
        
/// 获取本机的默认打印机名称
        
/// </summary>
        
/// <returns>默认打印机名称</returns>

        public static string GetDeaultPrinterName()
        
{
            StringBuilder dp 
= new StringBuilder(256);
            
int size = dp.Capacity;
            
if (GetDefaultPrinter(dp, ref size))
            
{
                
return dp.ToString();
            }

            
else
            
{
                
int rc = GetLastError();
                Pub.WinForm.Msg.Warning(
"获取默认打印机失败!错误代号:" + rc.ToString());
                
return string.Empty;
            }

        }

                
        
/// <summary>
        
/// 设置默认打印机
        
/// </summary>
        
/// <param name="PrinterName">可用的打印机名称</param>

        public static void SetPrinterToDefault(string PrinterName)
        
{
            SetDefaultPrinter(PrinterName);
        }


        
///// <summary>
        
///// 判断打印机是否在系统可用的打印机列表中
        
///// </summary>
        
///// <param name="PrinterName">打印机名称</param>
        
///// <returns>是:在;否:不在</returns>

        public static bool PrinterInList(string PrinterName)
        
{
            
bool bolRet = false;

            System.Collections.ArrayList alPrinters 
= GetPrinterList();

            
for (int i = 0; i < alPrinters.Count; i++)
            
{
                
if (PrinterName == alPrinters[i].ToString())
                
{
                    bolRet 
= true;
                    
break;
                }

            }


            alPrinters.Clear();
            alPrinters 
= null;

            
return bolRet;
        }


        
///// <summary>
        
///// 判断表单是否在指定的打印机所支持的纸张列表中
        
///// </summary>
        
///// <param name="PrinterName">打印机名称</param>
        
///// <param name="PaperName">纸张名称</param>
        
///// <returns>是:在;否:不在</returns>

        public static bool FormInPrinter(string PrinterName, string PaperName)
        
{
            
bool bolRet = false;

            System.Drawing.Printing.PrintDocument pd 
= new System.Drawing.Printing.PrintDocument();

            pd.PrinterSettings.PrinterName 
= PrinterName;

            
foreach (System.Drawing.Printing.PaperSize ps in pd.PrinterSettings.PaperSizes)
            
{
                
if (ps.PaperName == PaperName)
                
{
                    bolRet 
= true;
                    
break;
                }

            }


            pd.Dispose();

            
return bolRet;
        }


        
/// <summary>
        
/// 判断指定纸张的宽度和高度和在文本框中指定的宽度和高度是否匹配
        
/// </summary>
        
/// <param name="PrinterName">打印机名称</param>
        
/// <param name="FormName">表单名称</param>
        
/// <param name="Width">宽度</param>
        
/// <param name="Height">高度</param>
        
/// <returns></returns>

        public static bool FormSameSize(string PrinterName, string FormName, decimal Width, decimal Height)
        
{
            
bool bolRet = false;

            System.Drawing.Printing.PrintDocument pd 
= new System.Drawing.Printing.PrintDocument();

            pd.PrinterSettings.PrinterName 
= PrinterName;

            
foreach (System.Drawing.Printing.PaperSize ps in pd.PrinterSettings.PaperSizes)
            
{
                
if (ps.PaperName == FormName)
                
{
                    
decimal decWidth = FromInchToCM(System.Convert.ToDecimal(ps.Width));
                    
decimal decHeight = FromInchToCM(System.Convert.ToDecimal(ps.Height));
                    
//只要整数位相同即认为是同一纸张,毕竟inch到cm的转换并不能整除
                    if (Pub.MathEx.Round(decWidth, 0== Pub.MathEx.Round(Width, 0&& Pub.MathEx.Round(decHeight, 0== Pub.MathEx.Round(Height, 0))
                        bolRet 
= true;
                    
break;
                }

            }


            pd.Dispose();

            
return bolRet;
        }

        
        
/// <summary>
        
/// 英尺到厘米的转换
        
/// 米国人用的是英尺,中国人用的是厘米
        
/// 1 inch = 2.5400 cm
        
/// </summary>
        
/// <param name="inch">英尺数</param>
        
/// <returns>厘米数,两位小数</returns>

        public static decimal FromInchToCM(decimal inch)
        
{
            
return Math.Round((System.Convert.ToDecimal((inch / 100)) * System.Convert.ToDecimal(2.5400)), 2);
        }

    }

}

 

        页面设置窗体由报表浏览器窗体ReportViewer调用,使用以下结构的XML文件存储针对每个报表的页面设置:

 

<?xml version="1.0" standalone="yes"?> 
<ReportSettings> 
    
<采购订单> 
        
<ReportName>采购订单</ReportName> 
        
<PrinterName>EPSON LQ-1600KIII</PrinterName> 
        
<PaperName>Test</PaperName> 
        
<PageWidth>8.00</PageWidth> 
        
<PageHeight>8.00</PageHeight> 
        
<MarginTop>0.2</MarginTop> 
        
<MarginBottom>0.2</MarginBottom> 
        
<MarginLeft>0.2</MarginLeft> 
        
<MarginRight>0.2</MarginRight> 
        
<Orientation>横向</Orientation> 
    
</采购订单> 
</ReportSettings>

 

 

        当然,这种格式的XML配置文件是用DataSet来读写更新的,因为这种方法比较简单。

        页面设置窗体上有一个关于打印方向的设置,在(六)中有一个问题忘记阐述了,就是关于打印方向的。因为DeviceInfo结构中并没有打印方向的设置,所以在生成DeviceInfo结构的字符串时,应该根据打印方向来设置DeviceInfo结构的页高和页宽,见(五)中给出的EMFDeviceInfo类的属性DeviceInfoString。放在这里补充说一下。

        完整的页面设置窗体的代码下载

      是该结束的时候了,越到后来越觉得无法把握随笔的内容了,基本上的内容在前面都阐述得差不多了,如果还有什么不明白的话,看我的代码:https://files.cnblogs.com/waxdoll/RDLCReport.rar 。代码的主要内容是如何对RDLC报表进行自定义纸张单据打印

        下载内容包括:

        在其它窗体调用报表浏览器的代码如下:

 

//创建报表浏览器对象
RDLCReport.ReportViewer frmRPT = new RDLCReport.ReportViewer();
//指定列表报表显示的数据源,这个例子写的比较死,建议修改MainDataSet和DrillDataSet为System.Data.DataTable类型,这样可以更方便的调用
frmRPT.MainDataSet = this.dsMain;
//为列表报表设置数据源名称,这必须和报表中的数据区域的数据源名称相同
frmRPT.MainDataSourceName = "DataSet_Main";
//报表名称,这是用来存储报表页面设置的标识
frmRPT.ReportName = "DataList";
//指定钻取报表的数据源
frmRPT.DrillDataSet = this.dsDrill;
//指定钻取报表的数据源名称
frmRPT.DrillDataSourceName = "DataSet_Drill";
//指定报表路径
frmRPT.ReportPath = System.Windows.Forms.Application.StartupPath + @"\Reports\rptReport.rdlc";
//显示报表浏览器
frmRPT.ShowDialog();

 


       


    


posted @ 2008-06-11 15:07  RobertFang  阅读(16863)  评论(10编辑  收藏  举报