65.9K
CodeProject 正在变化。 阅读更多。
Home

轻松将 ListView 转换为精美的打印报告,并附带打印预览

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (73投票s)

2007年11月21日

CPOL

9分钟阅读

viewsIcon

252743

downloadIcon

18824

ListViewPrinter 类可以接收现有的 ListView,并轻松将其转换为漂亮的报告。

Screenshot - ReportOTTExample.jpg

引言

您崭新的应用程序已经完美完成——而且还提前了一周。在向管理层演示后,CEO 说:“太棒了。我喜欢。但我希望所有这些信息都能作为一份报告。我希望能够随意调整报告中的列,随意移动和调整它们的大小。它必须是可排序、可分组的,就像我们在屏幕上看到的那样。当然,在打印之前,我们必须能够进行打印预览。明天能完成吗?”你短暂地考虑过是否要提及 PrintScreen 键,但你怀疑他不会对那个解决方案感到兴奋。

管理层喜欢报告。程序员却不那么喜欢。报告通常是需求的最后考虑。然而,生成精美的报告并非易事。如果你有暴雪的预算并且有几个月的时间来弄清楚如何使用它们,你总是可以购买商业产品。至少有一个不错的开源 .NET 报表解决方案,但学习曲线仍然很长。

对我这样的懒惰而虚荣的程序员来说,我真正想要的是一个实施起来毫不费力,但却能产生惊人效果的东西。我想在同一天回到我的 CEO 那里,向他展示他想要的漂亮报告,然后提醒他我应该得到的加薪。

ListViewPrinter 的设计目标正是如此。

ListViewPrinter 解决方案

ListViewPrinter 的目的是很简单:它将一个 ListView 转换成一个漂亮的报告。生成的报告可以进行打印预览,调整页面设置,以及(当然)打印。

一如既往,ListViewPrinter 的一个主要设计目标是让它能够快速轻松地使用。因此,通常情况下,程序员会在 IDE 中创建并配置 ListViewPrinter 实例。然后,它的使用方式如下:

this.listViewPrinter1.PrintPreview();

就是这样!在 IDE 中,你可以指定要打印哪个列表视图,以及设置许多格式选项。只需一行代码,你就可以生成类似这样的内容:

Screenshot - Report1.jpg

这不是列表视图的屏幕截图。这是报告的打印预览,它处理了分页和缩小以适应——这份报告你可以打印出来交给你的 CEO,最终获得你应得的加薪。

从示例中可以看到,报告有六个部分,每个部分都可以自定义:

  1. 页面页眉
  2. 列表视图页眉
  3. 组页眉(仅当正在打印的列表视图显示组时显示)
  4. 列表中的行
  5. 页面页脚
  6. 水印

所有这些部分(水印除外)都可以:

  • 指定用于文本的字体和笔刷
  • 用任何笔刷绘制其背景
  • 用任何画笔绘制任何或所有边框

笔刷可以是渐变、纹理或纹理笔刷,所有这些结合起来可以让你制作出你想要的壮观报告——或者做得太过分,就像你在演示中看到的。

控制格式

输出的格式主要在 IDE 中完成。ListViewPrinter 的每个实例都公开以下属性:HeaderFormatFooterFormatListHeaderFormatGroupHeaderFormatCellFormat。这些可以展开以修改各个方面。

这很好,但有其局限性。在 IDE 中,没有标准的 PenBrush 对象编辑器。对于 Brush,最多只能公开一个 Color 并将其转换为 SolidBrush。对于 Pen,最多只能指定宽度和颜色。如果你想使用更花哨的 BrushPen(为了获得最佳报告效果,你应该这样做),你就必须编写一些代码。

Screenshot - BlockFormat.png

选择格式时,你应该记住报告是围绕块组织的:页面页眉和页脚、组和列表页眉以及列表单元格都是块。在一个块内,你可以指定:

  • 用于文本的字体
  • 用于绘制文本的笔刷
  • 用于填充边框之间区域的笔刷。

在块的每一侧,你可以指定:

  • 内边距(此块与其邻居之间应有多少空白)
  • 用于绘制边框的画笔。这包括画笔的宽度和用于绘制边框的笔刷。
  • 文本缩进(边框和文本之间应留有多少空间)

图表显示了顶部和底部侧的这些设置。它们同样可以设置在块的左侧和右侧。

格式灵活性的很大一部分来自 Brush 类的多功能性。有几种 Brushes

  • SolidBrush(如图所示),其中区域被绘制成单一颜色(可以是半透明的)。
  • HatchBrush,其中区域被绘制成图案。
  • TextureBrush,其中区域被绘制成图像。
  • LinearGradientBrush,其中区域被绘制成逐渐变化的颜色(如上面的页眉和页脚所示)。

花时间理解各种 Brush 类是值得的。

言归正传

配置完成后,ListViewPrinter 的主要编程接口很简单。命令直接对应于主要的打印命令:

void PageSetup();
void PrintPreview();
void PrintWithDialog();

其思想是将每个命令链接到相应的菜单事件处理程序,如下所示:

private void pageSetupToolStripMenuItem_Click(object sender, 

EventArgs e) {
    this.listViewPrinter1.PageSetup();
}
private void printPreviewToolStripMenuItem_Click(object sender, EventArgs e) {
    this.listViewPrinter1.PrintPreview();
}
private void printToolStripMenuItem_Click(object sender, EventArgs e) {
    this.listViewPrinter1.PrintWithDialog();
}

要进行无需用户交互的打印,你可以调用:

this.listViewPrinter1.Print();

有趣的代码片段

在编写这样一个项目时,总会有很多有趣的细节出现。

容错空间

PrintPageEventArgs.MarginBounds 中提供的边距并不可靠。好吧,技术上说是可靠的,但它们的意思并非你所想的。在打印预览时它们工作正常,但当你打印真实页面时,打印输出总是感觉有点偏离中心。而且这种偏离中心的程度因打印机而异。

问题在于页面的不可达区域。几乎总有一些区域是打印机无法触及的,而这些区域对于不同的打印机来说是不同的。.NET 2.0 将这些不可打印的区域表示为硬边距。

因此,为了计算打印输出的有效边距,代码需要如下所示:

if (this.PrintController.IsPreview)
    this.pageBounds = (RectangleF)e.MarginBounds;
else
    this.pageBounds = new RectangleF(e.MarginBounds.X - 

e.PageSettings.HardMarginX, 
        e.MarginBounds.Y - e.PageSettings.HardMarginY, e.MarginBounds.Width,
        e.MarginBounds.Height);

状态化执行

打印时需要注意的一点是,每个页面是单独打印的。当框架需要时,你的代码会被调用来打印下一页,然后再次退出。这意味着在每页的末尾,打印代码必须存储足够的状态信息,以便在需要下一页时能够恢复。如果你不习惯用有限状态机编程,这种状态化可能需要一点时间来适应。

打印水印

我不确定是否有人实际使用水印,但实现起来很有趣,所以它作为一项功能(不是推荐的项目范围确定方法)。水印问题有两个部分:如何旋转字符串,以及如何半透明地打印文本。

旋转文本需要一个不错的 DrawRotatedString() 方法或修改矩阵变换。.NET 没有 DrawRotatedString() 方法,所以我们只能依赖变换。这实际上是一件好事,因为变换功能更强大。一般变换定义了如何将点从一个坐标空间映射到另一个坐标空间。如果这似乎没有多大帮助,那么你只需要知道它们可以用来产生旋转、缩放和倾斜等效果。 .NET 有一个不错的矩阵类,它提供了创建各种变换矩阵的实用方法。一旦我们有了想要的变换矩阵,我们只需将其提供给我们的 Graphics 对象,之后的所有绘图都将被变换。

所以,为了绘制我们旋转的文本,我们创建一个旋转矩阵,然后将其应用于 Graphic 对象。

Matrix m = new Matrix();
m.RotateAt(watermarkRotation, Utils.CalculateCenter(this.pageBounds));
g.Transform = m;

半透明地打印文本仅仅是创建一个带有半透明颜色的笔刷,然后用该笔刷绘制文本。

int alpha = (int)(255.0f * (float)this.WatermarkTransparency / 

100.0f);
Brush brush = new SolidBrush(Color.FromArgb(alpha, 

this.WatermarkColorOrDefault));
g.DrawString(this.Watermark, this.WatermarkFontOrDefault, brush, 

this.pageBounds,
  strFormat);

当完成字符串绘制后,我们清除变换,以便绘图恢复正常。

g.ResetTransform();

限制

正如你可能从名称中猜到的,ListViewPrinter 只与 ListViews 一起工作。如果你不使用 ListView 来呈现数据,那么 ListViewPrinter 将无法帮助你。你必须拿出你的存折,然后取消你几周的社交生活。但是许多应用程序确实使用 ListViews,而这个类就是为了帮助它们而设计的。

ListView 必须处于 Details 视图模式,ListViewPrinter 才能知道如何打印它。如果它处于任何其他视图模式,报告将为空白。

ListViewPrinter 无法打印处于虚拟模式的 ListView。这是因为在虚拟模式下,你无法使用 Items 集合。因此,无法获取 ListView 中的第 n 个 ListViewItem。但请参阅下面的“无耻推广”,了解绕过此限制的方法。

ListViewPrinter 不是,也不旨在成为通用的报表解决方案。它没有提供运行总计、放置图形、绘制三角形或显示垂直文本的机制。它只是打印列表视图。

无耻推广

ListViewPrinter 可以很好地处理普通的 ListViews,但与 ObjectListViews 结合使用效果更好。例如,当与 ObjectListView 结合使用时,子项可以有图像,所有自定义渲染器都可以正常工作,甚至虚拟列表视图也可以打印。对于懒惰和虚荣的程序员来说,ObjectListView 是一个很大的帮助。 有关 ObjectListView 的详细信息,请参见此处

待办事项

允许在 IDE 中直接编辑 Pens 和 Brushes。

结论

希望这段代码能帮助你打印出基于 ListView 的数据,并最终获得你应得的加薪。

历史

2007 年 11 月 29 日 - 版本 1.2

  • 列表行现在会换行而不是省略文本。你可以通过 CellFormat.CanWrap = false; 关闭此行为。感谢 dgortemaker 的建议。
  • 处理 ListViewItem 的子项少于列表视图中列数的情况(感谢 Bernd Melchert)

2007 年 11 月 11 日 - 版本 1.0

© . All rights reserved.