Windows Forms 的可打印报表库






4.29/5 (20投票s)
一篇关于使用 C# 生成可打印报表的文章。
引言
报表库是一个 .NET 类库,它使用 GDI+ 生成可打印的报表。它将 DataTable
作为报表源。报表可以轻松自定义以满足业务需求。
背景
尽管 .NET 框架支持查看 Crystal Reports,但报表生成需要安装 Crystal Reports 运行时到客户端机器。在本文中,我将解释如何创建一个 C# 类,该类读取 DataTable
并使用 GDI+ 将数据呈现为可打印格式。
使用此类相比 Crystal Reports 的优势
- 主要优势是便携性。此类完全用 C# 编写。与 Crystal Reports 不同,无需安装。
- 易于使用。报表以
DataTable
作为数据源,并且大多数格式选项都设置了默认值,如有需要可以自定义。
GDI+ 简介
GDI+ 是 Windows API 提供的旧 GDI (Graphics Device Interface) 函数的更新版本。GDI+ 为图形函数提供了一个新的 API,它利用了 Windows 图形库。
GDI+ 函数可以在 System.Drawing
命名空间中找到。如果您使用过 Win32 GDI 函数,那么此命名空间中的一些类和成员看起来会很熟悉。提供了诸如画笔、刷子和矩形之类的类。
Using the Code
假设您已经在 VS 中创建了一个 C# Windows 应用程序项目,请将 ReportLibrary
项目文件添加到您的解决方案中。编译 ReportLibrary
项目,并将 ReportLibrary
引用添加到您的 Windows 应用程序项目。向 Windows 窗体添加一个 PrintPreviewDialog
控件,并将以下代码添加到您的打印预览按钮的 Click
事件中。为了演示目的,我创建了一个具有几个单选按钮和文本框的简单 Windows 窗体,演示了可用的几种不同的布局和格式选项。
以下代码演示了 ReportClass
中可用的几种布局和格式选项。在本文中,我们将使用 XML 文件作为报表源。
ReportClass objRepLib = new ReportClass();
//Declare the PrintDocument
PrintDocument myDoc = new PrintDocument();
//Load Default properties
LoadProperties();
//Get the DataTable for the report data source
DataTable dt = new DataTable();
DataSet ds = new DataSet();
ds.ReadXml(DataSourceTxt.Text);
dt = ds.Tables[0];
//Set the PrintDocument from the ReportClass
myDoc = objRepLib.ReportPrintDocument;
//Set the report DataSource
objRepLib.ReportSource = dt;
//Set the PrintDocument to the applications PrintPreviewDialog
//control
printPrevDiag.Document = myDoc;
//Set the ReportTitleFirstPageOnly property
objRepLib.ReportTitleFirstPageOnly = ShowTitleAllPages;
//Show or Hide the logo
objRepLib.ReportHasLogo = Showlogo;
Image img;
if (objRepLib.ReportHasLogo == true)
{
img = Image.FromFile(LogoTxt.Text);
objRepLib.ReportLogo = img;
}
//Show or Hide the report title
objRepLib.HasTitle = true;
//Show or Hide totals for all numeric columns
objRepLib.ReportShowTotals = ShowReportTotals;
//Set report page orientation to landscape or portrait
objRepLib.IsLandscape = PageOrientation;
//Show or hide the table border
objRepLib.HideGrid = ShowTableBorder;
//Set the report title text
objRepLib.ReportTitle = ReportTitleTxt.Text ;
//Show the print preview dialog
printPrevDiag.ShowDialog();
在本文中,我将讨论大多数选项,一旦我解释了概念,应该会非常直接。目标是使用 GDI+ 渲染 DataGrid
。复杂性(如果有的话)在于您必须实现“分页”,即何时停止打印并在新页面上开始?这不能是基于行数的固定值,因为报表的页面方向、行高和字体大小都是可自定义的。
生成报表
我们首先设置 ReportSource
属性,它接受 DataTable
类型的值。报表表是通过循环遍历 DataTable
并为表中的每个单元格使用 Graphics.DrawRectangles
方法渲染网格来生成的。为此,我们需要以下信息
- 一行中的单元格数:这将是
DataTable
中的列数。 - 报表表中的行数:这将是
DataTable
中的行数。 - 报表表中的总单元格数:这将是行数乘以列数 + 列数(用于报表标题行)+ 列数(用于报表总计行)。
int tabCol;
int tabRows;
int tabArrayMax;
//Store the number of cells in a row for the report table.
tabCol = dt.Columns.Count;
//Store the number of rows for the report table.
tabRows = dt.Rows.Count;
//Store the number of cells in the table which would be
//the number of rectangles to render as table cells.
//Since the product of TabCol and TabRows represents the
//number of cells in the table body, we need to add
//TabCol to the result to include the header row cells.
tabArrayMax = (tabCol * tabRows) + tabCol;
报表总计
报表具有 ReportShowTotals
属性,该属性显示表中所有数字列的总计(不包括第一列)。此功能需要一个数字数组来存储数字列的总和。数组 TotCol
在 PrintPage
方法外部定义为公共范围,以便在页面之间保留值。TotCol
数组大小在渲染第一页时设置。定义的数组必须是 decimal 类型才能存储带有精度的数字。
if (PageCount == 0)
{
ReDim(ref TotCol,tabCol) ;
}
渲染报表
报表表的宽度、标题行高度和正文行高度具有默认值,可以使用以下属性进行自定义
TableWidth
:默认值基于页面的内部宽度,即页面宽度减去左右边距宽度。这意味着报表表将自动跨越可用区域,具体取决于页面方向。此值可以自定义。如果自定义宽度大于页面的内部宽度,则将忽略自定义值并使用默认值。
要获取单个单元格的宽度,我们将表宽度除以列数。要开始渲染网格,我们需要获取 X 和 Y 坐标作为起始点,这基于页面的内部宽度和高度计算。
//Check if the table width specified for the report is greater than
//the inner page width (excluding margins)
if (iTableWidth == 0 || iTableWidth > e.MarginBounds.Width)
{
iTableWidth = e.MarginBounds.Width;
}
//Find out the avg width of each cell by dividing the total table width
//by the number of columns.
int colWidth = Convert.ToInt32(iTableWidth / tabCol);
Pen vbPen = new Pen(Color.Black);
//Declare an Array of RectangleF to render the table grid using
//the DrawReclangles method.
RectangleF[] rectCell = new RectangleF[tabArrayMax + tabCol];
//Declare a RectangleF to store the report title.
RectangleF titleRect = new RectangleF() ;
//Declare a RectangleF to store the report logo.
RectangleF logoRect = new RectangleF();
//Store the X coordinate from where to start rendering.
int startX = e.MarginBounds.Top;
//Store the Y coordinate from where to start rendering.
int startY = e.MarginBounds.Top - 35;
报表标题和徽标
标题和徽标通过定义 Rectangle
s 作为标题和徽标的容器来渲染。徽标大小默认为 logoRect
矩形的大小。标题使用 titleRect
矩形渲染。这样做是为了确保标题文本会在其容器内换行。
通常,您只需要设置 ReportHasLogo
、ReportLogo
、HasTitle
和 ReportTitle
属性即可显示徽标和标题,但可以使用以下属性进一步自定义它们。
ReportLogo
:设置报表徽标。默认情况下,徽标位于页面左侧。此属性接受Image
类型的值。ReportHasLogo
:设置显示或隐藏报表徽标的布尔属性。默认值为false
。将此值设置为true
以显示徽标。LogoHeight
:设置徽标图像的高度。默认值为75
。由于徽标和标题并排显示,因此徽标高度受限于为报表标题设置的高度。如果徽标高度超过标题高度,则默认为标题高度。LogoWidth
:设置徽标图像的宽度。默认值为150
。HasTitle
:设置显示或隐藏报表标题的布尔属性。默认值为false
。ReportTitle
:设置报表标题。默认值为空字符串。设置此属性不会自动将HasTitle
属性设置为true
。TitleFont
:设置报表标题的字体。接受Font
类型的值。默认值为Arial 14号
。TitleFontColor
:设置报表标题的字体颜色。接受Brush
类型的值。默认值为Brushes.Black
。TitleAlignment
:设置报表标题的对齐方式。接受StringAlignment
类型的值。默认值为StringAlignment.Center
。TitleHeight
:设置报表标题的高度。默认值为40
。ReportTitleFirstPageOnly
:这是一个布尔属性,用于显示或隐藏后续页面的报表标题。默认值为true
。将此属性设置为false
可在所有页面上显示标题。
StringFormat modSf = new StringFormat();
int i = 0;
int k = 0;
//Check if Logo needs to be displayed
if (bReportHasLogo)
{
//Check if the Logo height does not exceed the report title height.
if (iReportLogoHeight > iReportTitleHeight)
{
iReportLogoHeight = iReportTitleHeight;
}
logoRect = new RectangleF(startX, startY,
iReportLogoWidth, iReportLogoHeight);
//Render the logo.
e.Graphics.DrawImage(imReportLogo, logoRect);
}
else
{
iReportLogoWidth = 0;
iReportLogoHeight = 0;
}
//Check if report title needs to be displayed.
if (bReportHasTitle && (PageCount == 0 || !bReportTitleFirstPageOnly))
{
modSf.LineAlignment = saReportTitleAlignment;
modSf.Alignment = saReportTitleAlignment;
titleRect = new RectangleF(startX + ReportLogoWidth, startY,
e.MarginBounds.Width - iReportLogoWidth, iReportTitleHeight);
//Render the title
e.Graphics.DrawString(sReportTitle, fntReportTitleFont,
brReportTitleFontColor, titleRect, modSf);
}
else
{
iReportTitleHeight = 0;
}
报表标题行
报表标题行是通过循环遍历 DataTable
的列并使用 Graphics.DrawString
方法渲染而创建的。Graphics.DrawString
方法使用 Rectangle
作为字符串的容器。这确保文本保留在矩形边界(表单元格)内,并且还通过显式使用 Graphics.DrawRectangles
方法打印 Rectangle
s 来提供显示网格线的选项。
可以通过以下属性自定义标题行格式
HeaderCellAlignment
:设置标题行的文本对齐方式。接受StringAlignment
类型的值。默认值为StringAlignment.Near
。HeaderFont
:设置标题行的字体。接受Font
类型的值。默认值为Arial 10号
。HeaderFontColor
:设置标题行的字体颜色。接受Brush
类型的值。默认值为Brushes.Black
。HeaderBackgroundColor
:设置报表标题的背景颜色。接受Brush
类型的值。默认值为Brushes.White
。
modSf.LineAlignment = saReportHeaderCellAlignment;
modSf.Alignment = saReportHeaderCellAlignment;
//Set the Y coordinates for the next line.
startY += Math.Max(iReportLogoHeight,iReportTitleHeight ) + 25;
//create an array of header row values
object[] hString = new object[tabCol] ;
dt.Columns.CopyTo(hString,0);
//get the maximum header row height
iHeaderRowHeight = Convert.ToInt32(GetRowHeight
(hString,fntReportHeaderFont,colWidth,iHeaderRowHeight,e));
//Render the report table header.
//Loop through each column of the table and render the column title.
foreach (DataColumn dc in dt.Columns)
{
Rectangle r1 = new Rectangle(startX, startY, colWidth, iHeaderRowHeight);
//Renders the report table grid
e.Graphics.FillRectangle(brReportHeaderBackgroundColor,r1 );
if (!bReportHideGrid)
{
e.Graphics.DrawRectangle(vbPen,r1);
}
e.Graphics.DrawString(dc.ToString(), fntReportHeaderFont,
brReportHeaderFontColor, r1, modSf);
//Set X coordinates for the next cell
startX += colWidth;
k ++;
}
报表正文和分页
报表正文是通过循环遍历 DataTable
的每一行并使用 Graphics.DrawString
方法渲染来生成的。Graphics.DrawString
方法使用 Rectangle
作为字符串的容器。这确保文本保留在矩形边界(表单元格)内,并且还通过显式使用 Graphics.DrawRectangles
方法打印 Rectangle
s 来提供显示网格线的选项。
本节还解释了“总计”和“分页”功能。可以通过设置以下属性来显示所有数字列的总计
ReportShowTotals
:设置显示或隐藏报表中所有数字列总计的布尔属性。默认值为false
。NumericAlignment:
设置报表正文中所有数字值的文本对齐方式。接受StringAlignment
类型的值。默认值为StringAlignment.Near
。
总计是通过循环遍历每一行的列并将数字值的总和存储在数组中来计算的。如果 ReportShowTotals
属性设置为 true
,则数组值将在最后一页的最后一行之后显示。此行的第一列将包含单词“Total”。
“分页”功能通过 Page
的 HasMorePages
属性设置,该属性告诉打印机是否应打印其他页面。此属性应设置为 true
,最后一个页面除外。要确定渲染的页面不是最后一页,我们会跟踪最后渲染行的位置,并检查其 Y 坐标是否等于或大于页面的下边距。如果报表中的总行数大于当前行数,则将 HasMorePages
属性设置为 true
。
//Set the X coordinate for the next row.
startX = e.MarginBounds.Top;
//Set the Y coordinates for the next row.
startY += iHeaderRowHeight;
//Store current coordinate on the page.
int yPos = 0;
//Render report table body till the bottom margin of the page
while (yPos <= e.MarginBounds.Bottom)
{
int j = 0;
DataRow dr ;
//Check if current rendered row count = the total number of rows
//and set the HasMorePages property to false.
if (ReportRowCount == dt.Rows.Count)
{
//Write the total row
if (bReportShowTotals)
{
foreach(DataColumn dc in dt.Columns)
{
rectCell[k] = new RectangleF(startX, startY, colWidth, iRowHeight);
//Renders the report table grid
if (!bReportHideGrid)
{
e.Graphics.DrawRectangle(vbPen,startX,startY,colWidth,iRowHeight);
}
if (j == 0)
{
modSf.LineAlignment = StringAlignment.Near;
modSf.Alignment = StringAlignment.Near;
e.Graphics.DrawString("Total", fntReportBodyFont,
Brushes.Black, rectCell[k], modSf);
}
else
{
if(TotCol[j] != -1)
{
modSf.LineAlignment = StringAlignment.Near;
modSf.Alignment = saReportNumericAlignment;
e.Graphics.DrawString(TotCol[j].ToString(),
fntReportBodyFont, Brushes.Black, rectCell[k], modSf);
}
}
//Set the X coordinate for the next cell.
startX += colWidth;
j ++;
k ++;
}
}
e.HasMorePages = false;
ReportRowCount = 0;
break;
}
dr = dt.Rows[ReportRowCount];
//gets the maximum height required for the row.
iRowHeight = Convert.ToInt32(GetRowHeight(dr.ItemArray,
fntReportBodyFont,colWidth,iRowHeight ,e));
//Loop through each row and column to render the cells.
foreach (DataColumn dc in dt.Columns)
{
rectCell[k] = new RectangleF(startX, startY, colWidth, iRowHeight);
//Renders the report table grid
if (!bReportHideGrid)
{
e.Graphics.DrawRectangle(vbPen,startX,startY,colWidth,iRowHeight);
}
//Right align numeric values
if (IsNumeric(dr.ItemArray[j].ToString()))
{
modSf.LineAlignment = StringAlignment.Near;
modSf.Alignment = saReportNumericAlignment;
//Get totals for all numeric columns
TotCol[j] = TotCol[j] + Convert.ToDecimal(dr.ItemArray[j]);
}
else
{
modSf.LineAlignment = StringAlignment.Near;
modSf.Alignment = StringAlignment.Near;
TotCol[j] = -1;
}
e.Graphics.DrawString(dr.ItemArray[j].ToString(), fntReportBodyFont,
Brushes.Black, rectCell[k], modSf);
//Set the X coordinate for the next cell.
startX += colWidth;
j ++;
k ++;
}
//Set the X coordinate for the next row.
startX = e.MarginBounds.Top;
//Set the Y coordinate for the next row.
startY += iRowHeight;
//Set the last rendered coordinate.
yPos = startY;
i ++;
ReportRowCount ++;
//Check if current rendered row count < the total number of rows
//and set the HasMorePages property to true.
if (ReportRowCount < dt.Rows.Count)
{
e.HasMorePages = true;
}
}
PageCount ++;
报表可以通过以下属性进一步自定义
BodyFont
:设置表正文的字体。接受Font
类型的值。默认值为Arial 8号
。HideGrid
:设置显示或隐藏表格边框的布尔属性。默认值为true
。IsLandscape
:设置布尔属性,将页面方向从默认的Portrait
更改为Landscape
。默认值为false
。如果报表需要更大的页面宽度,则将其设置为true
。
//Sets the page orientation
ReportPrintDocument.DefaultPageSettings.Landscape = IsLandscape;
//Renders the page count
e.Graphics.DrawString(PageCount.ToString(),fntReportHeaderFont,
brReportHeaderFontColor,Convert.ToInt32 (Math.Round
(Convert.ToDouble (e.PageBounds.Width / 2))),
e.MarginBounds.Bottom + 30);
//Reset properties after final page
if (!e.HasMorePages)
{
PageCount = 0;
ReportTitleHeight = 40;
}
限制
此库适用于创建简单的报表。它没有 Crystal Reports 中可用的高级报表功能。它与 Crystal Reports 的主要优势在于其便携性和易用性。
关注点
调用打印预览对话框不会打印文档;是预览对话框上的打印图标重新运行打印事件并将输出发送到打印机。在预览完最后一页后,打印事件中的变量需要重新初始化;否则,基于页面计数的打印页数和格式将不正确。
参考
- GDI+ 的介绍摘自 Wrox 出版的 Professional .NET 2003 Third Edition