自定义报表预览,高度的可定制化,带来的无限可能
象第五章“在报表系统中的增强”中讲的那样,VFP9提供了一个新的报表预览窗口。一个新的系统变量_REPORTPREVIEW指定了一个应用程序的名称,该应用程序将被用作一个“工厂”(“工厂”是一个“并不自己提供客户端对象需要的功能,而是会建立一个提供这些功能的新对象”的设计模式),用于生成报表预览窗口。默认情况下,该系统变量指向在VFP主目录下的ReportPreview.APP,但是你可以根据自己的需要将它替换成你自己的APP。这种可以使用一个VFP应用程序来作为报表预览窗口的能力,与以前的版本相比,提供了大量对报表预览的表现和行为的控制。
当你预览一个报表的时候,默认情况下,用于该报表的Listener上的PreviewContainer属性为NULL。在这种情况下,报表引擎会调用由_REPORTPREVIEW所指定的应用程序,这个应用程序则建立一个VFP表单的实例来用作预览窗口,此时,一个对该表单的对象引用将被存储在PreviewContainer属性中。如果一开始PreviewContainer就不是NULL,那么报表引擎就不再操心关于调用一个报表预览工厂应用程序的事情了。
由于报表预览窗口就是一个VFP表单,因此你可以通过适当的设置属性来定制它的行为表现。为了在运行报表前建立一个预览窗口的实例,可以给ReportPreview.APP传递一个变量,ReportPreview.APP会建立预览窗口类的一个实例并将之放入该变量中。然后你就可以根据自己的需要设置这个表单变量的属性、再把这个变量存储到用于报表的那个Listener的PreviewContainer属性中去。
例如,下面的代码(取自CustomizePreview.PRG)会显示一个带有自定义标题、不带工具栏、从第4页开始以75%缩放比例显示两页的预览窗口:
local loPreview, loListener
do (_ReportPreview) with loPreview
with loPreview
.CurrentPage = 4
.ToolbarIsVisible = .F.
.CanvasCount = 2
.ZoomLevel = 4
.Width = _screen.Width - 20
.Caption = 'Chapter 7 Preview Window'
endwith
loListener = newobject('SFReportListenerDirective',;
'SFReportListener.vcx')
loListener.PreviewContainer = loPreview
use _samples + 'Northwind\orders'
report form TestDynamicFormatting object loListener preview
在象Universal Thread这样的论坛上一个很常见的问题是:“我要如何才能将打印按钮从打印预览工具栏上去掉呢?”在过去的版本中,你必须建立一个自定义的资源文件,定制报表预览工具栏来去掉打印按钮,并在你的应用程序中使用这个自定义的资源文件。在VFP9中,你只要简单的把工具栏上那个打印按钮的Visible属性设置为.F.就行了,不过这里还有两个小麻烦:
Listener的PreviewContainer属性不是指向报表预览表单、而是指向一个报表预览表单的代理(Proxy)对象,这个对象所起的作用就好像是Listener和预览表单之间的一个中间人。该代理对象有一个oForm属性用来引用真正的预览表单。预览表单上有一个toolbar属性,其中包含着对预览工具栏的一个引用。因此,隐藏打印工具栏需要将在loListener.PreviewContainer.oForm.Toolbar上的cmdPrint的Visible属性设置为.F.。
预览窗口还有一个带有打印菜单项的快捷菜单,这个快捷菜单是在报表预览窗口的InvokeContextMenu方法中被生成的,你也许会认为从菜单中删除打印功能会需要建立报表预览窗口的子类、并覆盖这个方法。幸运的是,VFP开发组考虑到了这种情况,并提供了一个钩子(hook)来让你可以对这个菜单进行改动。这个钩子是通过存储在ExtensionHandler属性中的一个对象来实现的。如果该属性中包含着一个对象,InvokeContextMenu会在生成菜单以后调用该对象的AddBarsToMenu方法。这样你就可以建立一个自定义对象,该对象带有一个能够删除打印菜单项的AddBarsToMenu方法,再将该对象的一个引用存储在ExtensionHandler属性中(调用SetExtensionHandler来做这件事情)。这样的一个自定义对象还必须有几个别的方法,因为如果ExtensionHandler属性引用了一个对象,别的一些方法也会使用这个对象。下面的代码是这样一个类的例子。
下面取自NoPrintButton.PRG的代码演示了如何来处理这个任务:
use _samples + 'Northwind\orders'
loListener=newobject('SFReportListenerDirective','SFReportListener.vcx')
report form TestDynamicFormatting.FRX preview ;
object loListener next 20 nowait
loExtension = createobject('ExtensionHandler')
loListener.PreviewContainer.SetExtensionHandler(loExtension)
loListener.PreviewContainer.oForm.Toolbar.cmdPrint.Visible = .F.
define class ExtensionHandler as Custom
function AddBarsToMenu(tcMenu, tnNextBar)
release bar 12 of &tcMenu
endfunc
function Release
if type('This.PreviewForm') = 'O'
This.PreviewForm.ExtensionHandler = .NULL.
This.PreviewForm = .NULL.
endif type('This.PreviewForm') = 'O'
endfunc
function Show(tnStyle)
endfunc
function Paint
endfunc
function HandleKeyPress(tnKeyCode, tnShiftAltCtrl)
endfunc
enddefine
你并非必须使用定义在ReportPreview.APP中的报表预览表单类来预览一个报表。SFPreviewForm.SCX(如图5所示)同时起着一个报表管理器和一个报表预览窗口的作用。从列表中选择一个报表,然后单击Preview按钮就可以在这个表单上对报表进行预览。Next和Previous按钮可以显示报表中的上一页和下一页。
图5、ReportListener可以输出到一个VFP表单,这样你就可以建立自定义的预览窗口
这里是取自表单的PreviewReport方法中的关键代码,该方法被Preview按钮的Click方法所调用:
with Thisform
.oListener = createobject('ReportListener')
.oListener.ListenerType = LISTENER_TYPE_ALLPGS
report form (lcReport) object .oListener
.oListener.OutputPage(1, .oPreviewContainer, LISTENER_DEVICE_TYPE_CTL)
endwith
由于这个ReportListener的ListenerType属性被设置为3了,所以它会以“一次处理所有页”模式去绘制所有的页、但不执行任何输出。当绘制完成后,表单会调用这个Listener的OutputPage方法,指示它去将第一页输出到oPreviewContainer图形(shape)上(LISTENER_DEVICE_TYPE_CTL是一个运算结果为2的常量,OutputPage使用这个值去指定一个VFP控件)。OutputPage并不会真正将一页输出到这个Shape上,只是使用这个Shape的大小和位置来作为输出的区域。另一个重要的方法是Paint。无论表单在何时被重绘,在这个方法中的代码都会重新显示当前页。没有这个方法的话,当象缩放表单之类的会导致表单被重绘的方法发生时,就会出现预览消失的情况,因为那个Shape也被重绘了。考虑到表单可能会在Listener完成绘制第一页前就被重绘,所以这里的代码被封装在一个TRY结构中:
with This
if vartype(.oListener) = 'O'
try
.oListener.OutputPage(.nCurrentPage, .oPreviewContainer, ;
LISTENER_DEVICE_TYPE_CTL)
catch
endtry
endif vartype(.oListener) = 'O'
endwith
注意这个SFPreviewForm只是一个简单的Demo。它并不能处理新的VFP报表预览窗口中的一些问题,比如从预览窗口中打印、或者一次显示多页等等。此外,因为PreviewReport只使用了一个listener的基类,所以也无法实现动态格式化、文本旋转、或者别的特殊效果。你当然可以根据自己的需要来添加这些功能。
如果你想要建立你自己的用作报表预览窗口的类,你的类上必须有一对方法:
SetReport:这个方法应该接收一个对Report Listener对象的引用,并把它储存在什么地方。为了预览一个报表,这个预览报表表单必须要调用这个Listener的一些方法,尤其是OutputPage,因此,它就必须要有一个对Listener的对象引用。当报表结束的时候,Listener将第二次调用SetReport,并向这个方法传递一个NULL,这样就会删除那个引用。否则的话,因为Listener和这个报表预览表单还存在着相互的引用的问题,就会导致这些对象不能被释放(请注意,SFPreviewForm.SCX没有这个方法,因为它不是作为一个报表预览窗口被报表引擎调用的,而是自己去驱动整个报表预览的过程)。
Show:该方法应该接收到跟一个表单的Show方法会接收到的同样的参数,这个参数表示表单是否应该是模式表单。
当预览表单被关闭的时候,它应该调用Listener的OnPreviewClose以确保环境被清理干净了。
NewPreview 类 (在 NewPreview.VCX中)是一个非常简单的例子。它只是一个简单的form基类,上面带有一个名为oPreviewContainer的Shape、以及一个名为oListener的自定义属性。它的SetReport方法有以下这样的代码:
lparameters toListener
This.oListener = toListener
它的Paint方法显示报表的第一页:
if vartype(This.oListener) = 'O'
This.oListener.OutputPage(1, This.oPreviewContainer, 2)
endif vartype(This.oListener) = 'O'
它的QueryUnload方法告诉Listener去清理环境:
if vartype(This.oListener) = 'O'
This.oListener.OnPreviewClose()
endif vartype(This.oListener) = 'O'
就这些了!这里是一些为一个报表使用这个类作为预览表单的代码(NewPreview.PRG):
local loPreview, loListener
loPreview = newobject('NewPreview', 'NewPreview.vcx')
loListener = createobject('ReportListener')
loListener.ListenerType = 1
loListener.PreviewContainer = loPreview
use _samples + 'Northwind\orders'
report form TestDynamicFormatting object loListener
当然了,这个预览窗口几乎没什么功能;它只是会显示报表的第一页而已。要建立你自己的带有完整功能的报表预览窗口,你也许会想要派生在ReportPreview.APP 中的FRXPreviewProxy 和 FRXPreviewForm 这两个类(当你解压了VFP主目录下的Tools\XSource\XSource.ZIP文件后,可以从Tools\XSource\VFPSource\ReportPreview找到它们的源代码)并添加你需要的其它功能。
新的SYS()函数
在VFP9中有一对跟Report Listener有关的新的SYS()函数。
当当前报表在运行结束之前被取消时,SYS(2024)返回”Y”;若没有当前报表、或者报表正常结束,则返回”N”。在UnloadReport事件触发之后,SYS(2024)被重置为”N”,所以你不能在执行一个REPORT或者LABEL命令的代码中使用这个值。它通常被用于Report Listener的一些根据报表是否运行结束来采取不同操作的方法中。
SYS(2040)表示一个报表的状态。如果没有当前报表,则它返回”0”;如果有一个报表正在被预览则返回”1”;如果报表正在被输出到一个文件或者打印机则返回”2”。这个函数可以被用在象一个报表对象的 Print When表达式中以根据该对象是否正在被预览或者打印来有条件的输出该对象。
总结
Microsoft已经为在设计时和运行时开放VFP的报表引擎做了大量令人难以置信的工作。通过将报表事件传递给ReportListener对象,他们让你可以对这些事件做出反应,从而完成从向用户提供自定义的反馈来提供不同类型的输出、到动态改变对象被绘制的途径等等你期望的任何事情。我们期待着看到VFP社群会用这些新功能做出更好的东西来。