在运行时扩展报表系统之报表指令

2022-04-07 19:54:59 浏览数 (1)

译者:Fbilo

SFReportListenerDirective是SFReportListener的一个子类。它的目标,是支持在备注字段USER中的指令,这些指令用来告诉listener如何处理一个报表对象。举个例子,“:LISTENER ROTATE = -45”指令告诉listener去把这个对象逆时针旋转45度。因为USER可以被用于各种目的,所以SFReportListenerDirective支持的指令必须以“:LISTENER”开头(如果你在FOXPRO 2.x时代用过GENSCRNX,你就熟悉这种类型的指令了)。

不同的指令处理不同的对象。如果只是简单的改变将要被绘制的对象的一些属性,那么它们(译者注:我也没搞明白这里的“它们”指的到底是什么,只能先这么照翻再说。)并不一定得是ReportListener的子类(稍后你会看到一些基于Custom的示例)。因为你也许需要为同一个对象使用多个指令,所以SFReportListenerDirective会维护一个指令的集合,并根据需要调用其中一个。

Init方法建立这个指令处理器的集合,并向集合填充几个常用的处理器。你可以在一个子类中、或者在这个类被实例化之后向这个集合添加数据项来添加额外的处理器(请注意,用在集合中的关键字必须是大写的)。(在下面这段代码以及这个类别的代码中,ccDIRECTIVE_*是一些定义在SFReportListener.H中的常量。)

代码语言:javascript复制
with This
    .oDirectiveHandlers = createobject('Collection')
    loHandler = newobject('SFDynamicForeColorDirective', ;
        'SFReportListener.vcx')
    .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_FORECOLOR)
    loHandler = newobject('SFDynamicBackColorDirective', ;
        'SFReportListener.vcx')
    .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_BACKCOLOR)
    loHandler = newobject('SFDynamicStyleDirective', ;
        'SFReportListener.vcx')
    .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_STYLE)
    loHandler = newobject('SFDynamicAlphaDirective', ;
        'SFReportListener.vcx')
    .oDirectiveHandlers.Add(loHandler, ccDIRECTIVE_ALPHA)
endwith

从SFReportListener的Init方法中调用的ProcessFRXRecord会分析当前FRX记录的USER备注,从中查找*:LISTENER指令。任何它找到的东西都会通过“搜索在oDirectiveHandlers集合中是否有一个给它的处理器”来被进行有效性检查。如果是的话,这个指令就被添加到一个储存在该报表对象之aRecords元素里的集合对象中。

代码语言:javascript复制
local laLines[1], ;
    lnLines, ;
    lnI, ;
    lcLine, ;
    lnPos, ;
    lcClause, ;
    lcExpr
    loHandler, ;
    loDirective
with This
* 处理在USER备注中的任何代码行

    lnLines = alines(laLines, USER)
    for lnI = 1 to lnLines
        lcLine = alltrim(laLines[lnI])

* 如果我们找到了一个listener 指令,并且它是我们支持的指令,
* 就把该指令和它的表达式添加到我们的集合中去
* (这个集合在第一次被用到的时候建立).

        if upper(left(lcLine, 10)) = ccDIRECTIVE_LISTENER
            lcLine   = substr(lcLine, 12)
            lnPos    = at('=', lcLine)
            lcClause = alltrim(left(lcLine, lnPos - 1))
            lcExpr   = alltrim(substr(lcLine, lnPos   1))
            try
                loHandler   = .oDirectiveHandlers.Item(upper(lcClause))
                lcExpr      = loHandler.ProcessExpression(lcExpr)
                loDirective = createobject('Empty')
                addproperty(loDirective, 'DirectiveHandler', lcClause)
                addproperty(loDirective, 'Expression',       lcExpr)

* 建立一个当前记录中所有指令的集合

                if vartype(.aRecords[recno()]) <> 'O'
                    .aRecords[recno()] = createobject('Collection')
                endif vartype(.aRecords[recno()]) <> 'O'
                .aRecords[recno()].Add(loDirective)
            catch
            endtry
        endif upper(left(lcLine, 10)) = ccDIRECTIVE_LISTENER
    next lnI
endwith

EvaluateContents方法会检查当前FRX记录的在aRecords中的元素是否包含该报表对象的一个指令的集合(因为一个指定的对象可能会有超过一个指令)。如果是的话,在该集合中的每一个数据项都包含着一个在oDirectiveHandlers集合中的指令处理器对象的名称、以及这个指令的参数(例如,如果这个指令是“*:LISTENER ROTATE = -45”,那么参数就是“-45”)。EvaluateContents调用在集合中每个处理器的HandleDirective方法,将EvaluateContents所接收到的properties对象和指令参数传递给这个方法。这里是EvaluateContents的代码:

代码语言:javascript复制
lparameters tnFRXRecno,  toObjProperties
local loDirective,  loHandler

with This
    if vartype(.aRecords[tnFRXRecno]) = 'O'
        for each loDirective in .aRecords[tnFRXRecno]
            loHandler = ;
                .oDirectiveHandlers.Item(loDirective.DirectiveHandler)
            loHandler.HandleDirective(This, loDirective.Expression, ;
                toObjProperties)
        next loDirective
    endif vartype(.aRecords[tnFRXRecno]) = 'O'
endwith
dodefault(tnFRXRecno, toObjProperties)

指令处理器

SFReportDirective是一个抽象类,我们用它来派生出指令处理器。它是一个Custom的子类,只有两个抽象方法:HandleDirective,由SFReportListenerDirective的EvaluateContents调用来处理那些指令;还有ProcessExpression,由SFReportListenerDirective的ProcessFRXRecord方法调用以将指令参数由文本转换成可以被处理器使用的格式。

SFDynamicStyleDirective是一个为在报表数据集的每一条记录中某个基于动态运算的表达式的报表对象改变字体样式的处理器(就是处理字体是否正常、粗体、斜体、或者有下划线等等内容)。使用下面的语法在一个报表对象的USER备注中指定这个指令:

*:LISTENER STYLE = StyleExpression 这里的StyleExpression是一个运算结果为所期望得到的样式的表达式。

样式的一个组合被作为数值型值储存在一个FRX中。为了使用各种样式的方便,SFDynamicStyleDirective允许你使用#NORMAL#、#BOLD#、#ITALIC#、#STRIKETHRU#、和#UNDERLINE#来指定样式。这些值是可以相加的,因此,#BOLD# #ITALIC#将得到粗斜体的文本。 ProcessExpression方法负责将这样的样式文本转换成对应的数值型值(那些FRX_FONTSTYLE_*常量表示不同样式的数值型值)。

代码语言:javascript复制
lparameters tcExpression
local lcExpression
lcExpression = strtran(tcExpression, '#NORMAL#',;
    transform(FRX_FONTSTYLE_NORMAL))
lcExpression = strtran(lcExpression, '#BOLD#', ;
    transform(FRX_FONTSTYLE_BOLD))
lcExpression = strtran(lcExpression, '#ITALIC#',;
    transform(FRX_FONTSTYLE_ITALIC))
lcExpression = strtran(lcExpression, '#UNDERLINE#', ;
    transform(FRX_FONTSTYLE_UNDERLINED))
lcExpression = strtran(lcExpression, '#STRIKETHRU#', ;
    transform(FRX_FONTSTYLE_STRIKETHROUGH))
return lcExpression

这里是一个指令的例子(取自TestDynamicFormatting.FRX文件中的SHIPVIA字段),它在某些情况下以粗体显示一个报表对象,而在另一些情况下则以正常字体显示: *:LISTENER STYLE = iif(SHIPVIA=3, #BOLD#, #NORMAL#) HandleDirective方法负责运算这个表达式。如果该表达式有效,则它会将要设置属性的对象的FontStyle属性设置为期望的样式,并把Reload设置为.T.,如此,则报表引擎就知道这个报表对象已经被改动过了。

代码语言:javascript复制
lparameters toListener,  tcExpression,  toObjProperties
local lnStyle

lnStyle = evaluate(tcExpression)
if vartype(lnStyle) = 'N'
    toObjProperties.FontStyle = lnStyle
    toObjProperties.Reload    = .T.
endif vartype(lnStyle) = 'N'
SFDynamicAlphaDirective非常类似于SFDynamicStyleDirective,但它是把报表对象的PenAlpha属性设置为指定的值。给它指定指令要使用下面这样的语法:
*:LISTENER ALPHA = AlphaExpression
SFDynamicColorDirective非常类似于SFDynamicStyleDirective,但它处理的是报表对象的颜色而不是字体样式。跟样式一样,颜色必须以RGB值的形式来指定,因此SFDynamicColorDirective支持以文本来指定颜色,例如#RED#、#BLUE#、以及#YELLOW#,使用下面的语法来指定指令:
*:LISTENER FORECOLOR = ColorExpression
*:LISTENER BACKCOLOR = ColorExpression
这里的ColorExpression是一个可以被运算为期望的颜色的表达式。
在HandleDirective方法中的代码类似于SFDynamicStyleDirective,但它调用SetColor而不是设置FontStyle属性。SetColor抽象在这个类中,而它具体实现是在SFDynamicColorDirective的两个子类中:SFDynamicBackColorDirective和SFDynamicForeColorDirective。这里是来自SFDynamicBackColorDirective的代码,用来演示颜色是如何被设置的:
lparameters toObjProperties,  tnColor

with toObjProperties
    .FillRed   = bitand(tnColor, 0x0000FF)
    .FillGreen = bitrshift(bitand(tnColor, 0x00FF00), 8)
    .FillBlue  = bitrshift(bitand(tnColor, 0xFF0000), 16)
Endwith

在SFDynamicColorDirective的ProcessExpression方法中的代码也非常类似于SFDynamicStyleDirective;它将文本的颜色转换为适当的RGB值。

TestDynamicFormatting.FRX演示了这两个指令处理器(以及SFReportListenerDirective)是如何工作的。它回打印那些来自VFP自带的Northwind示例数据库的Orders表中的记录。SHIPPEDDATE字段报表控件的USER备注中有着以下内容: *:LISTENER FORECOLOR = iif(SHIPPEDDATE > ORDERDATE 10, #RED#, #BLACK#)

这会告诉listener,如果项目被是在被订购超过十天之后才销售出去的,那么就以红色显示,否则则为黑色。象前面讲过的那样,如果销售方法是3,则SHIPVIA字段以粗体显示,否则则为正常字体。该字段使用下面的表达式来显示期望的值: icase(SHIPVIA = 1, 'Fedex', SHIPVIA = 2, 'UPS', SHIPVIA = 3, 'Mail')

下面的代码(取自TestDynamicFormatting.PRG)演示了怎样使用SFReportListenerDirective作为Listener来运行这个报表。图1是出现的结果:

代码语言:javascript复制
use _samples   'Northwindorders'
loListener = newobject('SFReportListenerDirective', 'SFReportListener.vcx')
report form TestDynamicFormatting.FRX preview object loListener next 20

SFTranslateDirective可以通过指定对特定字段进行翻译来让你建立多语言的报表。它的Init方法打开一个STRINGS表,每个字符串在该表中各有一条记录,每种语言在记录中各自有一个字段。

HandleDirective从STRINGS表中查找将要被绘制文本中的每个单词,找到后就从期望的语言字段中返回对应的译文。(它假定一个名为gcLanguage的全局变量中用于报表上的语言;当然,你完全可以将之更改为你喜欢的其它机制。)如果译文与原文不同,就把译文写入到属性对象的Text属性中去,并把属性对象的Reload属性设置为.T.,这样的话报表引擎就会使用新的字符串了。

图1、使用象SFReportListenerDirective这样的自定义

ReportListener可以很轻松的实现动态格式化文本

代码语言:javascript复制
lparameters toListener,  tcExpression,  toObjProperties
local lcText,  lcNewText,  lnI,  lcWord

store toObjProperties.Text to lcText, lcNewText
for lnI = 1 to getwordcount(lcText)
    lcWord = getwordnum(lcText, lnI)
    if seek(upper(lcWord), 'STRINGS', 'ENGLISH')
        lcNewText = strtran(lcNewText, lcWord, trim(evaluate('STRINGS.'  ;
        gcLanguage)))
    endif seek(upper(lcWord) ...
next lnI

if not lcNewText == toObjProperties.Text
    toObjProperties.Text   = lcNewText
    toObjProperties.Reload = .T.
endif not lcNewText == toObjProperties.Text

要使用这个Listener,只要简单的把*:LISTENER TRANSLATE放在任何你想要翻译的字段对象的USER备注中、并将gcLanguage设置为期望的语言。要注意的是:由于系统只会对字段对象调用EvaluateContents,所以你必须得使用它们来代替label对象。TestTranslate.PRG演示了如何将SFTranslateDirective添加给能为SFReportListenerDirective接收的指令处理器的集合。为了增加乐趣,这个示例使用了Pig Latin(故意颠倒英文字母顺序拼凑而成的行话)。

图2是当它运行的时候报表的样子。

代码语言:javascript复制
use _samples   'Northwindcustomers'
loListener=newobject('SFReportListenerDirective', 'SFReportListener.vcx')
loHandler  = newobject('SFTranslateDirective', 'SFReportListener.vcx')
loListener.oDirectiveHandlers.Add(loHandler, 'TRANSLATE')
gcLanguage = 'PigLatin'
report form TestTranslate.FRX preview object loListener

图2、你可以动态的改变字段对象们的文本,例如建立多语种的报表

SFRotateDirective是另一个指令处理器,但它基于SFReportListener而不是SFReportDirective,因为它所做的不止是通过属性对象来改变报表对象的属性而已,它会覆盖Render方法来旋转报表对象。

要指定一个报表对象将被旋转,需要在USER备注中使用下面的语法放入一个指令: *:LISTENER ROTATE = AngleExpression 这里的AngleExpression是一个运算结果为所要旋转角度的表达式(顺时钟的角度被指定为正值,逆时钟的角度被指定为负值)。 在一个对象被绘制前发生的BeforeRender方法的运行,通过检查是否为当前报表对象指定了一个要旋转的角度来开始。(在ProcessFRXRecord中的代码就是干这个的。这里我们就不看这个方法的代码了,它类似于SFReportListenerDirective,但要简单一些。)如果一个旋转角度被指定好了,BeforeRender使用oGDIGraphics对象的那些方法来保存当前GDI 的状态、并为该对象改变绘图的角度,接着再使用DODEFAULT()来执行正常的行为,由它来调用任何后继者的BeforeRender方法。

代码语言:javascript复制
lparameters tnFRXRecno, ;
    tnLeft, ;
    tnTop, ;
    tnWidth, ;
    tnHeight, ;
    tnObjectContinuationType, ;
    tcContentsToBeRendered, ;
    tnGDIPlusImage
local lnAngle, lnState

with This
* 如果应该要旋转这个对象,就这么做
    lnAngle = evaluate(evl(.aRecords[tnFRXRecno], '0'))
    if lnAngle <> 0
        * 保存图形句柄的当前状态
        .oGDIGraphics.Save(@lnState)
        .nState = lnState

        * 将坐标原点0,0 移动到我们希望它处于的位置,
        * 这样,当我们开始旋转的时候,是在围绕着恰当的点旋转
        .oGDIGraphics.TranslateTransform(tnLeft, tnTop)

        * Change the angle at which the draw will occur.
        .oGDIGraphics.RotateTransform(lnAngle)

        * 恢复坐标原点 0,0
        .oGDIGraphics.TranslateTransform(-tnLeft, -tnTop)
    endif lnAngle <> 0

    * 执行正常的行为
    dodefault(tnFRXRecno, tnLeft, tnTop, tnWidth, tnHeight, ;
        tnObjectContinuationType, tcContentsToBeRendered, tnGDIPlusImage)
endwith

AfterRender会恢复GDI 的状态,以保证后面的对象们能够正确的被绘制。下面的做法纯属逗乐:试着把这个方法中的以下代码注释掉并运行一个报表。结果会非常酷、但完全不实用。

代码语言:javascript复制
with This
    if .nState <> 0
        .oGDIGraphics.Restore(.nState)
        .nState = 0
    endif .nState <> 0

    *执行正常的行为
    dodefault()
endwith

TestRotate.FRX是演示这个东西如何工作的一个示例报表。日期字段们的列标头有旋转指令,这样日期字段们就可以被放得更靠拢一些。下面的代码(取自TestRotate.PRG)演示了如何运行这个报表并使用SFRotateDirective作为它的Listener。结果如图3所示。

代码语言:javascript复制
use _samples   'Northwindorders'
loListener = newobject('SFRotateDirective', 'SFReportListener.vcx')
report form TestRotate.FRX preview object loListener next 20

图3、通过改变其被绘制的途径,文本可以被动态的旋转

0 人点赞