向应用程序添加报告: 人们都希望为数据库应用程序至少创建一个报告。即使是像我们在本章创建的这样一个小的应用程序,也可能因报告类型丰富多采而容光无限。例如,概述报告通过摘要相似的数据来从整体上显示数据库的内容(甚至摘要的方式也可以创建多个报告),细节报告则显示所有的记录。除了报告所包含的数据及其出现的顺序以外,还有一些事情要考虑,比如从审美观点来讲的报告的外观。你可能不想花时间用最佳的文字和图形为工作组会议打印报告,但我可以肯定,经理会议通常需要这样的报告。
我无法在一个章节中告诉你创建报告的每一个要点。本章要讨论的是关于创建一批复杂报告的技巧,其中包括浏览数据的各种方法,并介绍一个如何完成上述部分目标的程序设计示例。
存在打印困难吗 创建一个你所需要的报告可能有点困难,在某些情况下也确实是如此。处理一项工程时,可能五个经理都想把他们自己的一组报告添加到应用程序中去,而你知道,在允许的时间范围内,你确实只有工夫创建一两组报告。不要失望,如果花点时间分析一下经理们在期待些什么,并且在打印过程中增加一点灵活性的话,就可能根本没有必要去编写所有的报告了。
当你试图减低应用程序的复杂性时,应该做的第一件事是,把所需的报告分解成几个简单的需求。以这样的列来创建一个表是个好主意,即按列说明简单的需求,然后在旁边列出报告。如果在报告中找到了某种对比方式,就有可能把几个报告合并成一个,然后按需要添加配置对话框。下面是关于如何把报告分解成一组部件的一些想法。
排序 每一个报告都要求某种排序。否则,就会形成一系列没有人能够理解的紊乱数据。请记住,数据库的一个主要目标是,把所包含的数据组织起来,使它更容易被理解。你的报告也应该是一样。排序是一种把两个或多个报告合并到单个程序设计任务中的最容易的方式。
分组 有时候你想要细节报告在单个标题下组织数据。例如,你可能在合同数据库中用ZIP代码创建分组。ZIP代码作为标题显示,该ZIP代码区中的所有合同都在该标题下按顺序排列。谈到组织数据时,分组和排序并没有太大的差别。但是,即使打印输出的主要部分都相同,把两个报告和不同的组结合起来也是非常困难的。原因很简单:把各种报表头组织在一起将使得打印例程十分复杂。然而,你可以把一个包含分组,另一个不包含分组的两个相似报告结合起来。此时,你只需要在程序中提供一个开关,用它打开标题或者关闭标题就可以了。
分组合计/摘要 许多报告会提供这样的脚注,整个报告或一组有关的数字在那里被记录下来。脚注还有很多其它的统计用法,但数字记录是最普通的类型。在有组标题的情况下,如果有可能,要把两个有不同合计或者摘要的报告结合起来是很困难的。但是,可以把未使用任何类型脚注的报告和使用了脚注的报告结合起来,只要它们的列是相同的即可。
报告外观 如果你有两个报告使用相似的数据但具备不同的外观风格,那么可以试着把它们结合起来。如果唯一的差别只是文字所用的字体或者是像添加到报告顶端的公司徽标这样的小窗口,那么创建两个不同的打印例程就没有什么实际意义了。
细节数据的层次 某些报告只是其它报告的简单综述。换言之,有一些报告完全只使用父数据。通过以某种方式摘要子记录来提供综述信息。把综述报告和显示数据库详细内容的报告结合起来相当容易。当然,这也存在着报告中包含什么内容的问题。例如,如果一个报告以售货员顺序显示本月的每一张发票,而另一个报告显示各位售货员的总销售额,那么你就可以把这两个报告合成为一个。使用一个简单的开关来判断用户是要摘要数据或者要按原样打印子表中的每一个记录。
筛选 在数据库中寻找部分而非全部的记录,对人们来说十分普通。如果两个报告唯一的差别是用来决定该筛选哪些记录,那么你就可以把这些报告结合起来。筛选是你对DBMS所做的查询的函数,而不是你编写的代码的函数。换言之,你要做的只是更改一两行代码。
此时,你可能会感到奇怪,为什么不创建一个带有众多配置对话框的单个报告,让用户来产生他们所需的报告呢?不这样做有几个充足的理由。第一,大多数用户会对创建报告所需的大量对话框不知所措。你会发现,和自己设计报告相比,你还要花大量的时间让用户学会提高创建报告的速度。第二,你要引进自定义报告,还要为用户创建某种把那些报告存盘的方法,这样他们以后才能在无需很多支持的情况下,创建相同的报告。
避免通用报告的另一个原因是与
安全有关的风险。如果提供给用户太多不同的创建自定义报告的方法,你可能会发现,这也带来了很多的
安全漏洞。管理上通常要保证公司的秘密数据不会误入他人之手。这意味着,除了要增强数据库本身的
安全性外,还要增强已打印文件的
安全性并把它牢牢地控制住。
可移植性 当使用Windows碰到打印机不兼容的问题时,不要误以为是
安全性问题。当然,Windows为打印例程提供了一致的程序设计API,这使编程工作变得更加容易。你还得到了打印驱动程序支持的明显优点棗至少不必像DOS环境下的程序员那样,还要编写自己的驱动程序。但是,优点也就这么多。厂商仍然有可能提供单薄的驱动程序,这使得很难从看上去正确的应用程序中得到正确的输出。你还会发现,由于产生输出的相对复杂性,与其它开发人员相比,这些问题会更多地发生在数据库程序员身上。仅仅把文本和图形添加在一起并保持各种表格元素对齐就可能存在某些问题。可以这么说,把应用程序从测试环境转移到实用环境中时,会遇到大量与打印机有关的问题。即使是世界上最好的测试也不能帮助你避免设计粗糙的打印驱动程序所带来的问题以及其它各种各样的兼容性问题。
给应用程序添加打印功能 看过了为数据库应用程序编写打印例程的复杂性,现在我们来看一看解决这些问题的办法。幸运的是,在我们使用的ADO1应用程序中,现在已经有了所需的三个函数框架。我们只看一看CADO1View类。有三个函数:OnPreparePrinting(),OnBeginPrinting()和OnEndPrinting()。第一个例程让你设置打印机并完成其它的预备任务,如在查询中保存当前的记录数,这样才能在打印作业完成后返回到那里。第二个例程OnBeginPrinting()让你准备设备的上下文信息。可以把设备上下文看作是画家的调色板棗用它来绘制想要发送给打印机的信息。在本示例中,我们会经常碰到Windows GDI(图形设备接口)这么一个特别的东西,所以你迟早会很好地理解设备上下文究竟意味着什么。最后,OnEndPrinting()例程让你把应用程序的总体设置恢复成打印前的条件。例如,你应该在这里把查询指针返回到其打印前的记录处。
哎,还遗漏了一个函数。我们还没有用来向打印机发送数据的打印例程。我们要做的第一件事是使用MFC ClassWizard向程序中添加一个OnPrint()函数。OnPrint()函数包含实际把打印信息输出到打印机所需的代码。其它与打印有关的任务也调用这个例程,如大多数应用程序提供的Print|Preview(打印|预览)命令。使用View|ClassWizard(视图|类向导)命令打开MFC ClassWizard对话框。在Class Name(类名)域中选择CADO1View,加亮Object IDs(对象ID)列表中的CADO1View和Messages(消息)列表中的OnPrint。单击AddFunction(添加函数)。Member Functions(成员函数)列表中应该添加了一个新函数,如下图所示。

单击Edit Code(编辑代码)按钮。现在向应用程序中添加一些与打印有关的代码。程序列表7.3包含了需要向OnPrint()函数添加的代码。








这看上去代码可真不少,也确实如此。处理打印输出是件相当困难的工作。但是,如果将任务分成几个小块,就不难看出该怎么做了。
需要了解的第一件事是CDC类的神秘的pDC对象。这就是我在前面提到的设备上下文。如果把pDC看作是调色板,也就是内存中绘制想要发送给打印机的信息的地方,会很有帮助。事实上,Microsoft已尽其所能在函数的命名和处理GDI的结构中孕育了这个观点。
我们应该做的第一件事(除了产生似乎有成千上万个变量之外)是创建一些绘图
工具。如果没有必需的绘图
工具,就不能在调色板上绘图。在这里,我要说明如何创建三个基本的绘图
工具,你会在大多数程序中用到它:刷子、画笔和字体。画笔可以是任何颜色,任何宽度,你甚至可以选择像点这样的绘图图案。用画笔可以画线,它也包括了对象的轮廓。例如,矩形不需要轮廓,但大多数其它对象都需要。你可以画出一个只用填充色填充的矩形。我们来看一看刷子。刷子为实线对象提供填充色。刷子还可以有特别的颜色,你可以选择创建带图案的刷子,如交叉阴影线。最后,字体用来在屏幕上写出一些东西 。它有很多的特性,但大多数特性对典型的程序员来说是比较陌生的。程序列表7.3中的源代码显示了创建字体的几种方式,使用它们并不要求全面的桌面排版知识。
注 Windows支持三种基本的绘图
工具:字体、画笔和刷子。
仅仅有了绘图
工具并不表示就可以使用它。画家可能在墙上挂了一支笔,但并不表示就可以用它来画画。同样地,当你在Windows中绘图时,首先必须选择
工具。这就是SelectObject()函数要完成的任务。它让你为绘图工作选择一个对象。
注 在使用
工具之前必须首先选择
工具。
对Windows中的每个设备上下文来说,都有一个刷子、画笔和字体对象。可以把它看成是有三只手,每只手都以不同的
工具装备起来。在使用另一种
工具之前,必须把当前的
工具放下,然后选择新的
工具。Windows自动为你处理放下
工具的任务。每次调用SelectObject()函数时,它就放下旧
工具并捡起新的
工具。
每一个
工具在很多重要的方面都很独特。如果想用Arial 10点字体,就需要一种特殊的字体
工具。如果需要该字体的粗体格式,或者突然想以Times NewRoman 12字体显示文字,就必须创建另一种字体
工具。幸运的是,不必为想要使用的每一种颜色都创建分离的字体——这个特征由设备上下文来控制。说明
工具的独特性的另一个示例是画笔。你要为想要的每一种宽度和风格的线创建画笔。刷子在有些方面显得更为独特。可以用相同的函数创建想要的任何字体或画笔,但对每种刷子来说,就要调用不同的函数。例如,如果你想创建交叉阴影线刷子,就要在代码中调用CreateHatchBrush()函数而不是CreateSolidBrush()函数。认识到现有
工具的局限性并按需要创建新的
工具是很重要的。
注释 本示例的绘图命令做了打印机优化,分辨率范围是每英寸300到360点。如果你的打印机分辨率更高的话,可能需要修改位置值以获得清晰的画面。但是,不论打印机的分辨率有多高,你都可以看到这些命令是如何相互配合、从而为数据库应用程序提供某种形式的输出的。
在绘图方面我们为本例做的第一件事是,用Ellipse()函数创建一个椭圆。和大多数实线绘图命令一样,要指定图形左上角和右下角的坐标。Windows假定你想使用当前选定的刷子和画笔,所以无需指定这些值。
绘制椭圆之后,代码会在其中放入一些文本。我们要做的第一件事是,把背景绘图模式设置成透明方式,这样我们就不用擦去椭圆的任何部分了。另一种模式是不透明的,意思是在前景中添加某种绘图的同时,要替换背景的颜色。现在看看代码是如何用GetOutputTextExtent()函数来确定文本的长度的,其单位是像素。这大概是方便地确定一行文本行长度的唯一方式。确定了文本长度后,你就可以把这些文本在打印页面上居中放置了。下一行用TextOut()函数向打印机发送文本。本节代码的最后一行用SetBkMode()函数把背景绘图模式复位成不透明方式(缺省设置)。
现在讨论一下字体。创建字体有两种便利的方法,下一段中的代码对此做了说明。第一种方法是用CreatePointFont()函数。你需要提供字体名称和想要创建的字体的大小,字体的单位是十分之一点。所以,如果你想创建10点的字体,就要指定字体大小值为100。
字体使用特殊的LOGFONT结构传送它们所需的全部参数。如果你使用了CreatePointFont()函数,Windows就会为你创建这个结构,并在创建过程中设置一些缺省值。如果不想要这些缺省值时该怎么办呢?我们可以使用GreatePointFont()创建简单的字体(就像我们刚做的那样),然后用GetLogFont()函数填充LOGFONT结构。现在,你已经有了功能完整的结构,可以稍微做一些修改。我之所以强调“稍微”是因为,如果你试图在Windows刚刚返回的结构中做大的更改,那是搬起石头砸自己的脚。我把字体的磅数从正常改成粗体,只要字体磅数没有太大的差别,这个修改就是可以接受的。现在可以用CreateFontIndirect()函数创建字体的粗体风格。下面的工作只需要把Windows提供的、修改了IfWeight成员的LOGFONT结构传送回去也就可以了。
请注意,一旦代码把字体选进了设备上下文,就用粗体字体创建了一组报表头。请始终谨记,首先要创建
工具,然后根据在设备上下文上绘图的需要选择
工具。我们还创建了一组变量,用来跟踪列的定位信息。两个报表头和细节信息使用了这些定位变量。请注意,用来导出定位信息的方法和设备无关。换言之,不论你是用300 dpi的打印机还是600 dpi的打印机,它都应该看上去差不多。当然,这里的随机成分是,我们用当前字体提供的文本所占用的空间量,Windows到底安排得怎么样。在大多数情况下,你会发现这个算法是精确的,但是要在公司里所有打印机上都打印出精美的报表,还是需要下一些功夫的。
下一步在标题下面绘制一条线段。请注意,绘制线段的过程分为两个步骤。第一,用MoveTo()函数把画笔移到打印页上的特定点,然后用LineTo()函数实际绘制这条线。可以把MoveTo()看成是抬起画笔的移动,把LineTo()看成是按下画笔的移动。
打印例程的最后一部分使用在DataGrid1中已有的数据。该代码完成的工作是在数据网格中的单元间移动,并在打印机或屏幕上打印其内容。那么,我们的应用程序的打印输出看起来是什么样的呢?下图就是个打印页面示例。
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/10294527/viewspace-123261/
,如需转载,请注明出处,否则将追究法律责任。