前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >〔连载〕开始建立你自己的报表监听器

〔连载〕开始建立你自己的报表监听器

作者头像
加菲猫的VFP
发布2022-04-07 19:41:52
3480
发布2022-04-07 19:41:52
举报
文章被收录于专栏:加菲猫的VFP
译者:Fbilo

虽然内建的ReportListener类(甚至包括在FFC中提供的那些listener)有着大量的功能,但可以肯定你最终还是会需要做更多没有包含在其中的功能。幸运的是,你可以通过派生ReportListener或者_ReportListener来建立你自己的listener,并添加你需要的功能。本章的余下部分将探索一些用report listener可以实现的东西。

SFReportListener

当在工作中使用_ReportListener的时候,我们发现我们的listener们还需要一些别的功能。定义在SFReportListener.VCX中的SFReportListener是_ReportListener的一个子类,它能够处理一些_ReportListener做不到的事情。此外,它还提供了一些大多数Listener都会需要的工具方法。

在使用后继者的时候,有一件_ReportListener不会处理(这样的事情有几件,但确实不多)的复杂情况是:如果一个后继者调用了它的CancelReport方法来取消报表的输出,这个报表输出并不会真正取消,因为只有领头的listener的CancelReport方法才能取消报表输出。因此,(当前listener的)Successor属性的Assign使用BINDEVENT()来确保当后继者的CancelReport方法被调用的时候,当前listener的CancelReport方法也会被调用。这样一来,对CancelReport的调用就可以象波浪一样沿着责任链倒传到领头的listener上。

代码语言:javascript
复制
LPARAMETERS toSuccessor
DODEFAULT(toSuccessor)
IF VARTYPE(toSuccessor) = 'O'
    BINDEVENT(toSuccessor, 'CancelReport', THIS, 'CancelReport', 1)
ENDIF VARTYPE(toSuccessor) = 'O'

现在有了一个新的问题:_ReportListener.CancelReport的行为是沿着责任链条向下逐级调用,因此,当一个报表输出被取消的时候,(责任链上的每一个后继者的CancelReport都会被逐个调用,因此,在这个过程中)责任链上的每一个后继者都有机会干点自己的私活。但是因为上述代码所做的事件绑定,导致了当当前listener调用它的后继者的CancelReport方法的时候,由于事件绑定的原因,它自己的CancelReport也会被再次调用。这将导致产生一个死循环,除非我们做些什么来结束它。SFReportListener.CancelReport是这么处理这件事情的:它覆盖了_ReportListener.CancelReport,如果调用是从一个后继者通过事件绑定开始的,它就不会继续沿着责任链向下去调用后继者的方法。

代码语言:javascript
复制
local laEvents[1], lnEvents

if not This.IsSuccessor
    ReportListener::CancelReport()
    nodefault
endif not This.IsSuccessor

if not isnull(This.Successor)
    lnEvents = aevents(laEvents, 0)
    if lnEvents = 0 or ;
        not upper(laEvents[1, 1].Name) == upper(This.Successor.Name)
        This.SetSuccessorDynamicProperties()
        This.Successor.CancelReport()
    endif lnEvents = 0 ...
endif not isnull(This.Successor)

绘制是另一件麻烦事。绘制要求有一个GDI+句柄,类似于SQL passthrough命令通过一个SQL连接句柄来工作或者低级文件函数通过一个文件句柄来工作。这个句柄包含在GDIPlusGraphics属性中,这个属性由报表引擎来设置为适当的值。然后,既然报表引擎不清楚关于后继者的任何事情,所以它仅会设置领头的listener的属性。那么,你碰到的第一个问题就是,你无法将一个后继者的GDIPlusGraphics属性设置为一个正确的值,因为这个属性是只读的。_ReportListener对此的解决办法是:通过将后继者的一个自定义属性SharedGDIPlusGraphics属性设置为正确的值,如此,则一个listener的子类可以将SharedGDIPlusGraphics属性的值传递给它需要调用的任何GDI+函数。然而,第二个麻烦是你不能指望在一个后继者的Render方法中使用DODEFAULT()来得到正常的绘制;因为默认的行为是使用GDIPlusGraphics作为GDI+句柄,而现在,除了领头的listener以外,其它所有后继者的该属性中包含的则是错误的值(默认为0),所以绘制无法工作。唯一能顺利使用基类绘制行为的对象是领头的listener。

因此,现在你有一个问题:你想让一个listener改变在报表中绘制部分内容的途径,于是你使用一些GDI+函数来改变GDI+语句以完成适当的改动(例如旋转某些文本),但你无法使用DODEFAULT()来执行真正的绘制工作,因为它在除了领头的listener中以外的任何其它地方都是无效的。

幸运的是,有一个变通办法:SFReportListener.Render方法调用自定义的BeforeRender和AfterRender方法,而在一个子类中这两个方法可以在正常的绘制发生之前进行任何GDI+状态改动并在绘制发生之后进行任何重要的清理工作。注意这段代码使用ReportListner::Render而不是DODEFAULT()来得到基本的行为,因为你想要跳过在_ReportListener.Render中的行为:

代码语言:javascript
复制
lparameters tnFRXRecno, ;
    tnLeft, ;
    tnTop, ;
    tnWidth, ;
    tnHeight, ;
    tnObjectContinuationType, ;
    tcContentsToBeRendered, ;
    tiGDIPlusImage

with This
    if .BeforeRender(tnFRXRecno, tnLeft, tnTop, tnWidth, ;
        tnHeight, tnObjectContinuationType,;
        tcContentsToBeRendered, tiGDIPlusImage)
        ReportListener::Render(tnFRXRecno, tnLeft, tnTop, tnWidth,;
            tnHeight, tnObjectContinuationType,;
            tcContentsToBeRendered, tiGDIPlusImage)
        .AfterRender()
    endif .BeforeRender(tnFRXRecno ...
    nodefault
endwith
BeforeRender和AfterRender支持后继者listener。这里是BeforeRender的代码:
lparameters tnFRXRecno, ;
    tnLeft, ;
    tnTop, ;
    tnWidth, ;
    tnHeight, ;
    tnObjectContinuationType, ;
    tcContentsToBeRendered, ;
    tiGDIPlusImage
with This
    if vartype(.Successor) = 'O' and ;
        pemstatus(.Successor, 'BeforeRender', 5)
        .Successor.BeforeRender(tnFRXRecno, tnLeft, tnTop, tnWidth, ;
        tnHeight, tnObjectContinuationType, tcContentsToBeRendered, ;
        tiGDIPlusImage)
    endif vartype(.Successor) = 'O' ...
endwith

为了说清楚这个机制是如何工作的,我们先假设一个报表的领头listener是一个SFReportListener对象,而一个后继者是一个名为SFRotateDirective的子类,它执行文本旋转工作。当绘制一个对象的时候都发生了什么事情呢?报表引擎首先调用领头的listener的Render方法,而后者会调用BeforeRender方法,这个方法再沿着责任链向下调用,因此每个后继者的BeforeRender都有机会去做任何重要的事情。SFRotateDirective.BeforeRender调用一些GDI+函数去旋转将要被绘制的对象。一旦BeforeRender链条执行结束,

SFReportListener.Render就接着执行它的基本行为,并导致被SFRotateDirective处理过的对象以一种旋转的方式被绘制。接着Render会调用AfterRender,后者再沿着后继者责任链调用,于是每个后继者的AfterRender方法都有机会去完成自己的任务。SFRotateDirective.AfterRender将GDI+状态恢复正常,因此后面的对象们就不会被旋转。

一个listener子类可能会做一些GDI+绘制。由于在_GDIPlus.VCX中的GPGraphics类能够为我们完成大量艰苦的工作,SFReportListener在它的Init方法中将一个GPGraphics对象的实例建立在它的自定义oGDIGraphics属性中。GPGraphics需要有一个GID+句柄以完成它的工作,因此当一个正在被处理的带区是页标头(page header)或者主题(Title)带区的时候,BeforeBand方法调用GPGraphics的SetHandle方法来将这个句柄设置为SharedGDIPlusGraphics属性的值。不过,这里有一个问题:GDI+句柄在每一页上都会变动,因此,BeforeBand就得确保SharedGDIPlusGraphics是首先被更新的属性。

代码语言:javascript
复制
lparameters tnBandObjCode,tnFRXRecNo

with This
    if inlist(tnBandObjCode, FRX_OBJCOD_PAGEHEADER, FRX_OBJCOD_TITLE)
        if not .IsSuccessor
            .SharedGDIPlusGraphics = .GDIPlusGraphics
        endif not .IsSuccessor
        .oGDIGraphics.SetHandle(.SharedGDIPlusGraphics)
    endif inlist(tnBandObjCode ...
    dodefault(tnBandObjCode, tnFRXRecNo)
endwith

还有几个_ReportListener的方法同样有着后继者的问题。_ReportListener的DoStatus、EvaluateContents、和AdjustObjectSize根本不能处理后继者,而在SFReportListener中的这些方法可以做到。 GetReportObject为在FRX中某条指定的记录返回一个对象。这让我们可以更轻松的检查关于任何FRX对象的信息。

代码语言:javascript
复制
lparameters tnFRXRecno
local lnRecno, loObject

This.SetFRXDataSession()
lnRecno = recno()
go tnFRXRecno
scatter memo name loObject
go lnRecno
This.ResetDataSession()
return loObject

由于象Render和EvaluateContents这样的事件会为在FRX中的每一个记录和要被绘制的每一个对象各触发一次(意思就是它们触发的次数接近于在FRX中对象的数量乘以将要被报表输出的记录的数量),所以你应该最大限度的缩小在这些方法中要完成的工作量。例如,如果你在备注字段USER中存储了一个用于告诉一个listener如何去处理一个报表对象的指令(directive),那么,任何分析这个备注字段的代码都会被调用多次,尽管它其实只需要一次就够了(你可以在报表设计器中该对象的属性对话框中的other页上访问它的USER备注字段的内容)。因此,SFReportListener有一个名为aRecords的自定义数组属性,其中可以包含着你需要的关于FRX中记录们的任何信息。

为了支持这个构想,BeforeReport事件根据在FRX游标中记录的数量定义(dimension) aRecords,并为FRX中的每条记录各调用一次ProcessFRXRecord方法(抽象在这个类中)。你可以在子类中覆盖ProcessFRXRecord来以任何你认为重要的信息更新aRecords。

代码语言:javascript
复制
with This
* 切换到FRX 数据工作期,将aRecords的行数定义为在FRX中记录的数量
* 然后遍历在FRX游标中的每一条记录,因为我们需要收集关于记录的信息
* 最后切换回原来的数据工作期
    .SetFRXDataSession()
    if alen(.aRecords, 2) > 0
        dimension .aRecords[reccount(), alen(.aRecords, 2)]
    else
        dimension .aRecords[reccount()]
    endif alen(.aRecords, 2) > 0

    scan
        .ProcessFRXRecord()
    endscan
    .ResetDataSession()

    * 执行原来的行为
    dodefault()
endwith

SFReportListener的OutputPage方法中还有着代码。由于调用这个方法时还带着一个特定的页码参数,而后继者是不需要知道当前是在哪一页上的,所以在该方法中的代码将被传递进来的页码存储到一个自定义属性nOutputPageNo中,以供其它别的listeners使用,因为它们的属性是由SetSuccessorDynamicProperties方法更新的。 可以从www.hentzenwerke.com下载的本章附件中包含有SFReportListener.VCX。由于SFReportListener是_ReportListener的一个子类,而你系统上_ReportListener所在的路径可能与本类被编译时所在的路径不同,所以请确保在类设计器中打开SFReportListener一次,如果碰到提示, 则从FFC所在目录找到_ReportListener.VCX,并保存这个类以将对父类的路径更新为你系统上的路径。此外,还要留心其中一个包含文件SFReporting.H中引用了\Program files\Microsoft Visual FoxPro 9\FFC\FoxPro_Reporting.h。如果你没有将这些示例安装在VFP所安装在的驱动器上、或者VFP没有安装为\Program files\Microsoft Visual FoxPro 9目录,请确保将SFReporting.H中的#INCLUDE语句修改为指向正确的路径,然后选中“重新编译所有文件”重新编译Samples.PJX。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 加菲猫的VFP 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • SFReportListener
相关产品与服务
腾讯云 BI
腾讯云 BI(Business Intelligence,BI)提供从数据源接入、数据建模到数据可视化分析全流程的BI能力,帮助经营者快速获取决策数据依据。系统采用敏捷自助式设计,使用者仅需通过简单拖拽即可完成原本复杂的报表开发过程,并支持报表的分享、推送等企业协作场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档