报表生成器






4.75/5 (33投票s)
一个简单的报表生成器文章。
摘要
这是一个简单的报表生成器组件,能够支持在设计时和运行时设计报表,并且支持多种数据源。
目录
引言
尽管在.NET中打印比以往任何时候都容易,但它仍然不是人人都能轻松实现的。该组件的目标是为通常在数据库驱动的应用程序中处理的数据提供一个简单、直接且可定制的打印解决方案。
ReportBuilder
不针对特定的控件,如DataGrid
或ComboBox
,而是专注于这些控件使用的数据源。其架构基于DataView
,它可以从任何可以提取数据源的地方接收数据源:DataTable
、DataView
、DataGrid
、ComboBox
、ListBox
和IReportSource
。
演示项目展示了使用该组件的两种不同方式
- 自动创建具有“自动打印支持”的控件的报表。要检查这一点,请单击工具栏上标有“打印助手”的按钮。将鼠标移到不同的控件上,您会注意到鼠标光标会发生变化。如果它变成
Cursors.Hand
(我太懒了,没有做一个更有指示性的光标),则表示该控件具有“自动打印支持”。单击它,然后查看接下来会发生什么。 - 创建自定义报表。这些报表由程序员在设计时设计。单击标有“打印网格”的按钮,查看窗体上
DataGrid
的自定义报表。请注意,对于第一列,您既不能更改Width
也不能更改BackColor
。这会因列而异。
此组件未经严格测试。请自行承担使用风险,并请发送反馈,指出发现的错误。
首次打开解决方案时,应先进行编译,然后再进行任何其他操作。
工作原理
为了理解该组件的工作原理,您应该熟悉.NET中的打印过程。
ReportBuilder
是一个包装类,它包含并公开了两个专用类的主要字段:PrintEngine
和PaintEngine
。PrintEngine
继承自PrintDocument
并执行与打印过程相关的任务,而PaintEngine
则专门负责将数据源中的数据绘制到位图上。它们通过两个接口进行通信:IPrintableClient
和IPaginationClient
。
从技术上讲,报表是页面的集合。一页包含五个组成部分(或块):报表页眉、页面页眉、正文、页面页脚和报表页脚。页脚和页眉是打印命令的集合(文本、图片、日期等)。PrintCommand
非常适合打印文本、线条和其他简单内容,它始终具有页面的宽度,并且高度有限(在此情况下为200像素)。它既简单又有效,但对于表示可以跨越多页的复杂结构(在两个方向上)来说,并不是一个好的解决方案。正文的布局(与DataGrid
的布局非常相似)由PaintEngine
提供。
PrintEngine
一个报表可以有多页,并且它们的架构可能不尽相同(只有第一页有报表页眉,只有最后一页有报表页脚等)。这就是为什么PrintEngine
需要PaginationEngine
来为报表中的每一页提供布局。
初始化后,PaginationEngine
会创建一个页面集合,其中包含有关每一页应如何构建的完整详细信息。之后,PrintEngine
会遍历PaginationEngine
的页面集合,要求每一页的所有组成部分(页眉、页脚和正文)提供其图形表示(图片)的位图,然后将它们逐一绘制到页面上。为此,每个块都必须能够报告其高度并提供正确大小的位图。
对于页眉和页脚,技术是相同的,它们都属于PrintElement
类型。对于正文,位图由PaintEngine
提供。
PaintEngine
由于使用打印命令来打印报表正文过于复杂,因此有一个专用类完全致力于此任务:PaintEngine
。该类以数据源和表样式作为输入,能够生成数据的虚拟图像,非常类似于DataGrid
的布局。但在大多数情况下,这对于页面来说太大了。因此,它有一个第三个输入参数,一个矩形,表示大图像中所需的部分。结果,正如您可能已经猜到的,是一个位图。单元格区域(GetGridBitmap
)与列标题区域(GetColumnsHeaderBitmap
)分开提供。这两个函数是IPrintableClient
接口所必需的。该接口继承自另一个接口:IPaginationClient
。最后一个接口确保实现者(在此情况下为PaintEngine
)能够返回其虚拟矩形(想象一下将DataGrid
拉伸直到不需要滚动条,并且所有数据都可见;这就是虚拟矩形)(ClientVirtualRectangle
),并且能够返回包含在所需矩形中的最大连续数据矩形(没有被截断的列或行)(GetPageRectangle
)。
PaginationEngine
该类以IPaginationClient
、报表的页眉和页脚以及页面矩形作为输入,创建Page
对象的集合。Page
对象用于映射报表中的实际页面。它包含PrintEngine
构建报表页面所需的所有信息:页面在页面数组中的行和列(报表可能在两个方向上扩展),两个布尔字段,用于指示页面是否包含报表页眉或页脚,以及页面正文的矩形。
PrintElement
PrintElement
(报表页眉、报表页脚等)类是PrintCommand
(TextCommand
、PictureCommand
、BlankLineCommand
等)的集合,可以使用特殊的集合编辑器PrintElementCollEdit
在设计时和运行时进行图形编辑。PrintElement
通过对其组成打印命令的高度求和来计算其高度,并通过遍历命令并要求它们在它提供的绘图表面(位图)上逐一绘制来创建其位图。
PrintCommand
打印命令是页眉和页脚的构建单元。默认情况下,已经创建了六个简单的打印命令:BlankLineCommand
、ColumnsHeaderCommand
、DateCommand
、HRuleCommand
、PictureCommand
、TextCommand
。尽管在大多数情况下这些命令可能已足够,但您可能希望添加自己的打印命令。
如何创建自己的打印命令
HRuleCommand
将作为示例。您可以在*CustomControls*项目、*Printing.cs*文件中找到其实现。
PrintCommand
抽象类。public class HRuleCommand:PrintCommand
{// class body}
PrintCommand
类执行两项操作
- 声明两个抽象函数
GetHeight
和Draw
,PrintElement
类需要它们来测量和绘制自身。 - 继承自
DynamicTypeDescriptor
,并重写GetLocalizedName
和GetLocalizedDescription
成员,以便在运行时为命令属性提供本地化支持。PropertyCommands
和CategoryCommands
集合在设计时不会出现在PropertyGrid
中,并且它们始终为空,因为我认为没有必要使用属性控件进行打印命令。有关DynamicTypeDescriptor
的更多信息,请参阅使用动态属性和全球化释放PropertyGrid。 - 重写并实现两个抽象函数
GetHeight
和Draw
。public override int GetHeight(Graphics g, PrintEngine pe, int maxWidth) { return Width; } public override int Draw(Graphics g,PrintEngine pe, Point startPoint, int maxWidth) { int yOffset=Width/2; using(Pen pen= new Pen(Color,Width)) { pen.DashStyle=DashStyle; g.DrawLine(pen,new Point(startPoint.X ,startPoint.Y + yOffset), new Point(startPoint.X + maxWidth, startPoint.Y + yOffset)); } return Width; }
此时,您可以在代码中使用您的命令,但您将无法受益于在设计时或运行时使用特殊编辑器的优势。为此,还有一些额外的步骤。如果您不熟悉使用
CollectionEditor
编辑集合的技术,请参阅本文:如何使用CollectionEditor编辑和持久化集合。 - 为您的类创建一个
TypeConverter
。internal class HRule_Converter:ExpandableObjectConverter { public override bool CanConvertTo(ITypeDescriptorContext context, Type destType) { if (destType == typeof(InstanceDescriptor)) { return true; } return base.CanConvertTo(context, destType); } public override object ConvertTo(ITypeDescriptorContext context, CultureInfo info,object value,Type destType ) { if (destType == typeof(InstanceDescriptor)) { return new InstanceDescriptor(typeof( HRuleCommand).GetConstructor(new Type[] {typeof(int), typeof(Color), typeof(DashStyle)}), new object[] {((HRuleCommand)value).Width,((HRuleCommand)value).Color, ((HRuleCommand)value).DashStyle},true);} return base.ConvertTo(context,info,value,destType); } }
- 将新创建的
TypeConverter
与您的PrintCommand
类关联起来。[TypeConverter(typeof(HRule_Converter))] public class HRuleCommand:PrintCommand {// class body}
- 将您的
PrintCommand
集成到PrintElement
的编辑器中。ReportBuilder
使用一个特殊的集合编辑器PrintElementCollEditForm
来在设计时和运行时编辑页眉和页脚的打印命令。它在*CustomControls*项目、*Printing.cs*文件中实现。为了使您的类类型在编辑器中“添加”按钮的下拉列表中可用,您必须将您的类类型添加到可用类型数组中
protected override Type[] CreateNewItemTypes(IList coll) { return new Type[]{typeof(HRuleCommand),typeof(BlankLineCommand), typeof(TextCommand), typeof(DateCommand),typeof(PictureCommand), typeof(ColumnsHeaderCommand)}; }
在
SetProperties
方法中,设置所需的样式。protected override void SetProperties(TItem titem, object reffObject) { base.SetProperties (titem, reffObject); if(reffObject is HRuleCommand ) { titem.Text="Horizontal Rule"; titem.ForeColor=Color.Gray; titem.ImageIndex=2; titem.SelectedImageIndex=2; } }
正如您所见,可以为命令关联一个图像索引。编辑器有一个
ImageList
属性。在构造函数中,我分配了一个在ImageRes
类中找到的静态ImageList
。public PrintElementCollEditForm() { this.ImageList= CustomControls.BaseClasses.ImageRes.ImageList; }
此图像列表从*ImageRes.resx*资源文件中的
ImageListStreamer
(“ImageStream
”)获取其图像。要更改此图像列表的图像,请在窗体上添加一个ImageList
,添加您想要的图像,转到窗体的资源文件,找到序列化了ImageList
的“ImageSteam
”属性的位置,然后复制<value>
和</value>
标签之间的块。之后,转到*ImageRes.resx*文件,并将您从图像列表复制的块替换到“ImageSteam
”元素<value>
和</value>
标签之间的块。
如何使用它
自动打印支持
在开始一个大型项目时,留下一些“后门”总是一个好主意。可以显著影响项目结构灵活性的一个方面是为其创建一个私有库,并继承您将在项目中使用的所有.NET标准控件。
public class PToolBar:ToolBar{}
public class PButton:Button{}
切勿直接使用标准控件,仅使用您库中的控件。
- 这是基于这样的假设:迟早,您将需要更改一些控件,添加新属性、事件等,或者仅仅是修改其行为。(在项目中间,您的老板可能希望拥有主题支持,或者想要一个更好看的菜单。)您只需在一个地方修改控件,更改就会自动传播到整个项目。
- 在设计时标记一些简单的问题可能非常有帮助,这些问题否则会传播到运行时并且更难检测。考虑
ComboBox
缺少ValueMember
,DataGrid
没有TableStyle
,缺少格式字符串等。如何捕获这些错误?您可以在此处看到一个示例:PComboBoxDesigner,它在设计时在PComboBox
周围绘制一个轮廓,如果它有一个空的ValueMember
。
遵循上述思路,*ReportBuilder*项目有一个私有库,其中包含在*ProjectLibrary.cs*文件中实现的派生控件。
考虑拥有数十个窗体的大型数据驱动应用程序,每个窗体都有许多数据控件,如DataGrid
、ComboBox
、ListBox
等。客户端突然需要为某些数据控件(尤其是DataGrid
)提供打印功能。通常,您需要修改所有现有窗体,并为每个数据控件添加和自定义ReportBuilder
组件。拥有一个私有库将使您能够为项目创建自动打印支持。在*ReportBuilder*项目中,实现方式如下:窗体上有一个Toolbar
,其中包含其他标准按钮,还有一个专门用于打印的按钮,标有“打印助手”。单击此按钮将启动一个跟踪机制,该机制会检查鼠标悬停的每个控件是否“可打印”(实现IPrintable
接口)。如果是,鼠标光标会变为Cursors.Hand
,如果单击,则禁用跟踪并显示PrintSettingsDialog
。否则,它会变为Cursors.No
。如果用户仍然单击,则光标会重置,跟踪会停止。这可以分为两部分:创建一个接口(IPrintable
)并将其实现到我们想要打印支持的所有控件中,然后在Form
上创建跟踪机制。
IPrintable
接口有三个成员
- 一个函数
GetSource
,它返回一个DataView
对象,ReportBuilder
需要它作为DataSource
。 - 一个函数
GetTableStyle
,它返回一个CustomControls.ApplicationBlocks.TableStyle
对象,ReportBuilder
需要它作为TableStyle
。 - 一个
boolean
属性IsValid
,指示数据源和表样式是否有效,这是跟踪机制所必需的。
PDataGrid
、PListBox
和PComboBox
控件实现此接口非常简单,无需更多解释。
跟踪机制完全由PForm
类实现。它实现了IMessageFilter
接口,并监听WM_MOUSEMOVE
和WM_LBUTTONDOWN
消息。有一个protected
成员StartTracking
,可以从派生类调用。不一定要使用Toolbar
按钮来开始跟踪,只需调用此过程即可。
自定义打印
基本上,自定义打印意味着为每个要打印的数据源分配一个ReportBuilder
组件。这需要更多的工作,但为您提供了更多的可能性来定制输出。这里有一个示例,它使用了一个ReportBuilder
组件(rp
)和一个Button
(btn_PrintGrid
)。
如前所述,此组件不打印控件而是打印其数据源。ReportBuilder
创建的目的是打印DataView
对象。由于使用这种类型的打印,您通常会使用TableStyleEditor
来创建TableStyle
,因此您需要首先设置DataSource
属性。设置DataSource
非常简单。只需单击DataSource
属性的下拉按钮,就会显示窗体上可用源的列表。为了出现在该列表中,一个对象应该是以下之一:DataSet
、DataTable
、DataView
、DataGrid
、ComboBox
、ListBox
或实现IReportSource
接口。请注意,当您更改DataSource
属性时,如果新值指向不同的表,TableStyle
的ColumnStyles
集合可能会被擦除。
设置DataSource
后,您可以轻松地为报表正文创建TableStyle
。为此,单击TableStyle
属性的省略号按钮,TableStyleEditor
将弹出。这与DataGridTableStyle
的集合编辑器非常相似,因此应该很容易理解它的工作原理。您可以添加所需的列,但请注意,用户只能从您提供的列中选择打印。即使列已添加到ColumnStyles
集合中,仅当Visible
属性设置为true
并且具有有效的MappingName
时,它才可见。用户可以通过设置Visible
属性在运行时切换列的可见性。
您可能会注意到TableStyle
和ColumnStyles
在PropertyControl
类别下都有两个奇怪的属性:PropertyCommands
和CategoryCommands
。这些集合用于属性控制,允许程序员在设计时设置可见性,并在运行时设置父类属性的只读状态。例如,程序员决定第一列应始终可见,宽度固定,并且具有特定的BackColor
。当您创建TableStyle
或ColumnStyles
时,这两个集合为空,并且仅在编译项目后才会填充。有关此主题的更多信息,请参阅使用动态属性和全球化释放PropertyGrid。
大多数时候,您会希望节省尽可能多的墨水,并使报表保持简单,仅使用黑白。为此,TableStyleEditor
有一个下拉按钮,提供两种常见的格式选项。
您可以通过设计页眉和页脚来进一步自定义报表。您有报表和页面的页眉和页脚。设计这些非常简单,因为您有一个特殊的集合编辑器来帮助您:PrintElementCollEdit
。您实际上可以在构建元素(页眉或页脚)时看到它的外观。如果元素图像的底部出现了虚线红线和剪刀,则表示当前显示该元素的窗口太小,无法容纳它。如果在使用或打印报表时出现此情况,则表示该元素的高度超过了为其分配的空间(ReportHeader
的最大高度为400像素,其他元素为200像素,并且始终与页面宽度相同),您需要重新设计该元素。
全球化
全球化是每个大型应用程序的一个重要问题。不幸的是,实现它并不总是那么简单。为了帮助您将此组件集成到全球化应用程序中,ProjectBuilder
为全球化功能提供了自己的实现。为此,全球化问题仅出现在两个编辑器中:PrintElementCollEdit
和TableStyleEditor
。它们可以完全本地化(甚至包括PropertyGrid
中属性的名称和描述)。全球化的实现基于一个专用类:Dictionary
,位于CustomControls.Globalization
命名空间中。它有一个简单的机制,会在可执行文件目录中查找名为*Dictionary.resx*的*resx*文件中的本地化资源。它会自动根据System.Globalization.CultureInfo.CurrentCulture
值选择本地化语言。但您可以强制使用特定语言(在本例中为罗马尼亚语),如下所示
System.Threading.Thread.CurrentThread.CurrentCulture=
new System.Globalization.CultureInfo("ro-RO");
Dictionary.resx文件中的资源名称必须符合特定格式。要创建有效的资源名称,您需要向中性文本值附加两个字母的ISO文化名称“_”。例如,要在罗马尼亚语中本地化OK按钮的文本,您需要创建一个名为RO_OK的资源。当然,资源的值由您选择。显示在PropertyGrid
中的属性的显示值也是如此(对于BackColor
属性,创建一个名为RO_BackColor的资源)。对于属性的描述,情况略有不同。由于之前的格式,您需要在资源名称末尾添加“_Descr”:RO_BackColor_Descr
。为了消除所有疑虑并查看其工作原理,请查看Dictionary.resx文件中的一些示例。
页码
为此,您需要在TextCommand
中引入特定的令牌。以下是所有可用的令牌
- [PgNum] 表示当前页码。
- [PgCol] 表示当前页面在二维页面数组中的列。
- [PgRow] 表示当前页面在二维页面数组中的行。
- [PgNumArr] 表示当前页面在二维页面数组中的二维位置。
- [TotalPgs] 表示总页数。
例如,“Page number [PgNum]”在第二页上的输出将是:“Page number 2”,在第三页上是:“Page number 3”。
结论
这还有很长的路要走,许多部分应该改进。这不是最终产品,只是一个入门指南。我写这篇文章是希望它能对您有所帮助,并希望您能提供一些建设性的反馈。
参考文献
修订历史
- 原始文章。
DataSource
属性的范围得到了极大的扩展,可以直接接受一些常见的控件。