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 ;
……[ 偷偷懒,其实我并不擅长总结某某的特点,我只能从实际经验中得到一点点结论,而且我也不原意去抄袭帮助中的New Features ,呵呵……]
在以后的几篇随笔中,我将结合最近一段时间使用RDLC 报表的经验继续探讨相关的一些问题,大致内容包括:报表设计器的使用、LocalReport 的一些相关操作,如何自定义纸张等等,欢迎大家提出参考意见。
需要说明的是,现在关于VS. NET 2005 中的Report Viewer Control 的内容非常少,我只能按照自己的理解来说一些东西,这其中肯定会有一些偏差,欢迎各位的批评指正。另外,我所涉及的内容都是关于LocalReport 的,对于ServerReport 没有进行研究。
相关的资源:
MSDN 的Visual Studio Controls 论坛 http://forums.microsoft.com/MSDN/ShowForum.aspx?ForumID=75&SiteID=1
GotReportViewer http://www.gotreportviewer.com/ (有几个非常经典的例子,不知为什么最近我无法上这个网站了,呵呵,谁知道请告诉我原因,谢谢!)
晕死,总是见到朋友们评论要GotReportViewer 的实例,请在下面的连接中下载:http://files.cnblogs.com/waxdoll/RDLC.rar
在这篇随笔中,我主要分析一下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", null , out 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 ,即对数据进行一种排序后的预览效果会保持不变,除非再次开启这个应用程序并重新排序。] 而这一切不需要在代码中做任何操作,只需要在报表定义文件中添加
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 ),非常值得阅读,推荐给大家http://files.cnblogs.com/waxdoll/RDLCS.rar 。两幅截图:
Matrix
Table
偶然看到的一个关于RDL 的评论:
Microsoft use RDL as the linch pin in its new Reporting Services. Even MS Managers admit that Reporting Services’ success is dependent on adoption of RDL by third party BI ISVs. But Microsoft has consistently taken markets away from its BI partners and ISVs. Free Reporting Services is just the latest which starts with free OLAP Server, free Analytical/Data Mining Services, and a rash of free client side OLAP to Office 2003 utilities and templates.
Now Microsoft will argue that its just expanding the BI markets. ISV and BI vendors will have to decide if they want to help Microsoft “expand the BI markets”. So far Cognos has said it will support RDL at some time in the future; while Business Objects/Crystal says it is considering RDL. Curiously, except for Computerworld, the XML and BI community of publications have been virtually silent about RDL - neither its virtues/weaknesses nor the dilemma it poses to Microsoft ’s “BI partners” have made the usually boisterous trade press.
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 stream = new FileStream(name + "." + fileNameExtension, FileMode.Create);
m_streams.Add(stream);
return stream;
}
private void Export(LocalReport report)
{
string deviceInfo =
"
"
"
"
"
"
"
"
"
Warning[] warnings;
m_streams = new List
report.Render("Image", deviceInfo, CreateStream,
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 事件定义如下:
///
/// 获取当前时间组成的字符串,用作生成不会重复的文件名
///
///
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;
}
///
/// 导出到 Excel
///
///
///
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", null , out 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);
}
}
///
/// 刷新报表数据
///
///
///
private void tool 刷新 _Click( object sender, EventArgs e)
{
this .rptViewer.RefreshReport();
}
///
/// 在加载报表数据时终止报表数据的加载
///
///
///
private void tool 终止 _Click( object sender, EventArgs e)
{
this .rptViewer.CancelRendering(0);
}
///
/// 从 DrillThrough 报表返回到导航页面
///
///
///
private void tool 返回 _Click( object sender, EventArgs e)
{
if ( this .rptViewer.LocalReport.IsDrillthroughReport)
this .rptViewer.PerformBack();
}
///
/// 回到报表的第一页
///
///
///
private void tool 第一页 _Click( object sender, EventArgs e)
{
this .rptViewer.CurrentPage = 1;
}
///
/// 跳转到报表的最后一页
///
///
///
private void tool 最后一页 _Click( object sender, EventArgs e)
{
this .rptViewer.CurrentPage = this .rptViewer.LocalReport.GetTotalPages();
}
///
/// 以 25% 的比例显示报表
///
///
///
private void tool25_Click( object sender, EventArgs e)
{
this .rptViewer.ZoomMode = ZoomMode.Percent;
this .rptViewer.ZoomPercent = 25;
}
///
/// 以 50% 的比例显示报表
///
///
///
private void tool50_Click( object sender, EventArgs e)
{
this .rptViewer.ZoomMode = ZoomMode.Percent;
this .rptViewer.ZoomPercent = 50;
}
///
/// 以 100% 的比例显示报表
///
///
///
private void tool100_Click( object sender, EventArgs e)
{
this .rptViewer.ZoomMode = ZoomMode.Percent;
this .rptViewer.ZoomPercent = 100;
}
///
/// 以 200% 的比例显示报表
///
///
///
private void tool200_Click( object sender, EventArgs e)
{
this .rptViewer.ZoomMode = ZoomMode.Percent;
this .rptViewer.ZoomPercent = 200;
}
///
/// 以 400% 的比例显示报表
///
///
///
private void tool400_Click( object sender, EventArgs e)
{
this .rptViewer.ZoomMode = ZoomMode.Percent;
this .rptViewer.ZoomPercent = 400;
}
///
/// 将缩放模式设置为整页
///
///
///
private void tool 整页 _Click( object sender, EventArgs e)
{
this .rptViewer.ZoomMode = ZoomMode.FullPage;
}
///
/// 将缩放模式设置为页宽
///
///
///
private void tool 页宽 _Click( object sender, EventArgs e)
{
this .rptViewer.ZoomMode = ZoomMode.PageWidth;
}
///
/// 在报表中搜索 txtSearch 中的字符
///
///
///
private void tool 搜索 _Click( object sender, EventArgs e)
{
if ( this .txtSearch.Text.Trim() == string .Empty)
return ;
this .rptViewer.Find( this .txtSearch.Text.Trim(), 1);
}
///
/// 搜索报表中下一处 txtSearch 中的字符
///
///
///
private void tool 搜索下一个 _Click( object sender, EventArgs e)
{
if ( this .txtSearch.Text.Trim() == string .Empty)
return ;
this .rptViewer.FindNext();
}
///
/// 跳转到上一页
///
///
///
private void tool 上一页 _Click( object sender, EventArgs e)
{
if ( this .rptViewer.CurrentPage != 1)
this .rptViewer.CurrentPage--;
}
///
/// 跳转到下一页
///
///
///
private void tool 下一页 _Click( object sender, EventArgs e)
{
if ( this .rptViewer.CurrentPage != this .rptViewer.LocalReport.GetTotalPages())
this .rptViewer.CurrentPage++;
}
///
/// 跳转到由 txt 跳转中指定的页数
///
///
///
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;
}