Showing posts with label Reporting. Show all posts
Showing posts with label Reporting. Show all posts

Tuesday, August 23, 2016

Rendering a Report Image in the Foreground

A feature I’d forgotten about in the VFP 9 reporting enhancements is the ability to absolutely position objects. As Cathy Pountney discusses in What's New in the Visual FoxPro 9.0 Report Writer, you can use this feature to add a watermark to a report. One problem, though, is that regardless of how you arrange objects using Sent to Back or Bring to Front, the image always appears in the background of the report. What if you want it to appear in the foreground (that is, on top of the detail band objects)?

A little-used reporting enhancement to the rescue. FX and GFX objects extend a report listener by hooking into the various events, allowing you to change the behavior of the report run. These objects can be of any base class, as long as they have an ApplyFX method that accepts certain parameters, but it’s easier to subclass FXAbstract in _ReportListeners.vcx in the FFC folder. The difference between an FX and a GFX object isn’t in the class definition; it’s just that the report listener pays attention to the return value of a GFX object’s ApplyFX method, whereas it doesn’t for an FX object. To add an FX or GFX object to a listener, use:

loListener.AddCollectionMember(Class, Library, APP or EXE, .T. for singleton, .T. for GFX or .F. for FX)

I created a subclass of FXAbstract that does the following:

  • When the report run starts (tcMethodToken is “BEFOREREPORT”), create an array of Empty objects, one for each image in the FRX file that has “FOREGROUND” in its Comment property (the record number of the image in the FRX is used as the index for the object in the array).
  • When such an image is rendered (tcMethodToken is “RENDER”), save its rendering information (left, top, height, width, etc.) to the corresponding Empty object in the array and tell the listener to not render the image by returning 2.
  • When the page footer band is entered (tcMethodToken is “BEFOREBAND” and the first of the variable parameters is 7), tell the listener to render each of the images. Because they’re being drawn after the detail band text, they’ll appear on top of the text.

Here’s the code for the class:

define class SFFXRenderInForeground as FXAbstract of home() + ;

     'FFC\_ReportListener.vcx'
    dimension aForeground[1]
        && an array of rendering information
    lSelfCall = .F.
        && .T. if we're being called from ourselves (we call Render which calls
        && us)
   
    function ApplyFX(toListener, ;
        tcMethodToken, ;
        tP1, ;
        tP2, ;
        tP3, ;
        tP4, ;
        tP5, ;
        tP6, ;
        tP7, ;
        tP8, ;
        tP9, ;
        tP10, ;
        tP11, ;
        tP12)
        local lnReturn, ;
            lnSession, ;
            lnSelect, ;
            lnCurrRecno, ;
            lnRecno, ;
            lnI, ;
            loForeground
        lnReturn = 0 && OUTPUTFX_BASERENDER_AFTERRESTORE
        do case

* Before the report runs, create an array of information about objects we'll
* process.

             case tcMethodToken = 'BEFOREREPORT'
                lnSession = set('DATASESSION')
                if toListener.FRXDataSession > -1
                    set datasession to toListener.FRXDataSession
                endif toListener.FRXDataSession > -1
                lnSelect = select()
                select FRX
                lnCurrRecno = recno()
                scan for 'FOREGROUND' $ upper(COMMENT)
                    lnRecno = recno()
                    dimension This.aForeground[lnRecno]
                    This.aForeground[lnRecno] = createobject('Empty')
                endscan for 'FOREGROUND' $ upper(COMMENT)
                if between(lnCurrRecno, 1, reccount())
                    go lnCurrRecno
                endif between(lnCurrRecno, 1, reccount())
                select (lnSelect)
                set datasession to lnSession

* If we're rendering an object that's supposed to be in the foreground, save
* the rendering information.

            case not This.lSelfCall and tcMethodToken = 'RENDER' and ;
                vartype(This.aForeground[tP1]) = 'O'
                loForeground = This.aForeground[tP1]
                addproperty(loForeground, 'FRXRecno', tP1)
                addproperty(loForeground, 'Left',     tP2)
                addproperty(loForeground, 'Top',      tP3)
                addproperty(loForeground, 'Width',    tP4)
                addproperty(loForeground, 'Height',   tP5)
                addproperty(loForeground, 'ContType', tP6)
                addproperty(loForeground, 'Contents', tP7)
                addproperty(loForeground, 'Image',    tP8)
                lnReturn = 2 && OUTPUTFX_BASERENDER_NORENDER

* If we're about to do the page footer band, render all foreground objects.

             case tcMethodToken = 'BEFOREBAND' and tP1 = 7
                 && FRX_OBJCOD_PAGEFOOTER
                for lnI = 1 to alen(This.aForeground)
                    loForeground = This.aForeground[lnI]
                    if vartype(loForeground) = 'O'
                        This.lSelfCall = .T.
                        toListener.Render(loForeground.FRXRecno, loForeground.Left, ;
                            loForeground.Top, loForeground.Width, loForeground.Height, ;
                            loForeground.ContType, loForeground.Contents, ;
                            loForeground.Image)
                        This.lSelfCall = .F.
                    endif vartype(loForeground) = 'O'
                next lnI
        endcase
        return lnReturn
    endfunc
enddefine

The following code tests the GFX object by running a report with and without it.

open database _samples + 'Data\TestData'

loListener = newobject('FXListener', home() + 'FFC\_ReportListener.vcx')
loListener.ListenerType = 1 && LISTENER_TYPE_PRV
report form Foreground object loListener
loListener.AddCollectionMember('SFFXRenderInForeground', ;
    'SFFXRenderInForeground.prg', '', .T., .T.)
report form Foreground object loListener

Here’s what the report looks like without the GFX object:

Background

You can see the image is drawn under the text. Here’s the report when the GFX object is used:

Foreground

Notice the image now appears on top of the text.

You can download the source and sample files for this from the Technical Papers page of my personal web site, http://doughennig.com/papers/default.html (look for “Rendering a Report Image in the Foreground”).

Friday, October 26, 2012

Fixing Yet Another Report Designer Issue

Last month I blogged about an issue in the VFP Report Designer that causes a “printer not ready” error if you don’t have a default printer set up, as is sometimes the case in a Terminal Server environment. Recently I discovered a related issue: the same error occurs when opening the Page Setup dialog. The cause is the SET(‘PRINTER’, 3) statement in the LoadDeviceInfo method of FRXDeviceHelper in _FRXCursor.VCX. The fix is to wrap that statement in a TRY structure.

Wednesday, September 26, 2012

Fixing Another Report Designer Issue

A couple of years ago, I blogged about a bug in FRXFormatUtil.ChooseFont. I ran into another issue in that method (not really a bug) recently. If you try to select a font for an object in the Report Designer and don’t have a default printer set up (as is sometimes the case in a Terminal Services environment), you get a “printer not ready” error. It turns out the reason is the use of the “P” switch in the GETFONT() function, which tells the Font dialog to only display fonts for the current printer. That errors out if you don’t have one.

I wrapped the GETFONT() calls in TRY blocks with a retry in the CATCH without the “P” flag. Here’s the updated code:

if THIS.FontCharSet = 0
*** DH 09/26/2012: wrap calls to GETFONT() with "P" switch in TRY and
*** if it fails, retry without that switch to avoid a "printer not ready"
*** error if there's no default printer
    try
        cFontString = getfont( ;
            THIS.FontFace, ;
            THIS.FontSize, ;
            THIS.StyleFlagsToChar( THIS.FontStyle)+"P", ;
            -1 )
    catch
        cFontString = getfont( ;
            THIS.FontFace, ;
            THIS.FontSize, ;
            THIS.StyleFlagsToChar( THIS.FontStyle), ;
            -1 )
    endtry
else
    try
        cFontString = getfont( ;
            THIS.FontFace, ;
            THIS.FontSize, ;
            THIS.StyleFlagsToChar( THIS.FontStyle)+"P", ;
            THIS.FontCharSet )
    catch
        cFontString = getfont( ;
            THIS.FontFace, ;
            THIS.FontSize, ;
            THIS.StyleFlagsToChar( THIS.FontStyle), ;
            THIS.FontCharSet )
    endtry
endif

(Yes, I realize this could be written more efficiently, but I wanted to make the minimum necessary changes to the code.)

Friday, June 25, 2010

Gotcha with Custom Report Memberdata Attribute Names in VFP 9 SP 2

(That must be the longest blog title in history!)

I’ve been (finally) implementing VFP 9 SP2 features in Stonefield Query (version 4.0 is due to be released next week) and, while doing some final testing, ran into an error when editing a certain object in the Report Designer. The error was 1712, “Field name is a duplicate or invalid”. In tracking the error down, it occurred in the XMLStrToCursor method of _FRXCursor, one of the report helper classes in the FFC. The statement causing the error was ALTER TABLE … ADD COLUMN … It’s not actually a table that columns are being added to but a cursor. The weird thing is that the columns being added didn’t exist in the cursor, so they certain weren’t duplicates.

I tweeted about this and Frank Perez Jr. responded “I think you also get that error if the field name is invalid (i.e. more than 10 characters in a free table).” Sure enough, that was the problem. Even though cursors can have field names longer than ten characters, ALTER TABLE can’t handle them, and given that there’s no ALTER CURSOR command, we’re stuck with it.

What lead to the error was that I added custom report memberdata to the specific object causing the problem. The custom memberdata had an attribute named “adjustwidth”, which is more than ten characters long. Changing it to “adjwidth” solved the problem. So, important safety rule (channelling Venkman here): don’t use names more than ten characters for your custom report memberdata attributes.

Thanks to Frank for pointing me in the right direction. Another reminder of the usefulness of Twitter.

Tuesday, March 30, 2010

Fixing a Report Designer Bug

A bug in the VFP Report Designer has always, ahem, bugged me: something seems to turn on the Printer Environment setting for a report. Unless you want a report to always use a certain printer, that setting should be off. Turning it on can make a report much slower to open (for example, if the printer saved with the report isn’t available on your system) and can cause other problems.

I don’t remember who discovered this (sorry) but the cause turned out to be clicking the font button in the Field or Label Properties dialogs, even if you then choose Cancel. But how would that affect the Printer Environment setting?

Tired of having to deal with this, I decided to track it down. Fortunately, the source code for the Report Designer dialogs is included with VFP (unzip HOME() + “TOOLS\XSource\XSource.ZIP” and look in the resulting VFPSource\ReportBuilder folder). After some digging, I found the culprit in the ChooseFont method of FRXFormatUtil (in FRXBuilder.VCX), which is called when you click the font button. That method checks whether the TAG memo field in the header record of the FRX is empty or not. If not, it uses SYS(1037, 3) to update TAG and TAG2 from the current printer environment (the code is actually in FRXCursor.PopPrintEnv, which ChooseFont calls). The idea is that if the report has a saved printer environment, that environment should be used since it may impact which fonts are available. However, here’s the bug: TAG may not be empty even when TAG2 (which contains the binary printer environment) is. I’ve seen a single CHR(8) in TAG when TAG2 is empty, which means there is no saved printer environment, but it passes the NOT EMPTY(TAG) check in ChooseFont so the printer environment of the report is overwritten.

The solution is simple: change the test in ChooseFont from IS NOT EMPTY(TAG) to IS NOT EMPTY(TAG2), then rebuild ReportBuilder.APP. I’ve already made this change in the upcoming Stonefield Query version 4.0.