REPORT with 2 HEADER’s using CLONE of a DETAIL

Reports can have Details with very different data. Common is to have some “special” pages with a Recap, Summary or Totals. I want these “special” pages to have a 2nd different Header (or 3rd & 4th) that matches the different Detail data. For example Employee details with a lot of data:

At the end of the report are Grand Totals with just 5 columns that I want a different header:

Having a different HEADER can be done various ways. The way this Report was done was to use only DETAILS’s and manually manage page breaks. It can be tedious to get exactly right. Another way is to have a separate Report then merge Preview Queue’s.

My goal is to use the features built in the REPORT declaration and engine to do the pagination work for me. Everything I say here also works with FOOTER, FORM or any DETAIL.

Reports like above simply have 2 HEADER’s and matching DETAIL’s. It should not be that hard.

A Report can only have one Page HEADER band and the Report cannot be changed to use a different Band. What I realized I could do is “change” all the Header’s controls easily. HIDE() them all, then create new controls with a CLONE() of another DETAIL. Clone is what makes this easy.

Here’s an example (download below) that prints Employee Details. After are Grand Total details (colored Green to be obvious). I have designed the 2nd header for those as the TotalHeader2 DETAIL:

After the report prints all the Employee Details and does an ENDPAGE() it’s time to print the Grand Totals. I want the TotalHeader2 Detail as my Page Header above them.

Step 1 – HIDE the controls on the normal Page Header:

 !-------- Hide the Page Header Controls -------- 
 LOOP ChildNdx=1 TO 999
     ChildFEQ = ?PageHeader{PROP:Child,ChildNdx}       !Page Header Control FEQ
     IF ~ChildFEQ THEN BREAK.
     (ChildFEQ){'BandHide_Was'}=(ChildFEQ){PROP:Hide}  !Save what HIDE was to Undo
     (ChildFEQ){PROP:Hide}=TRUE
 END

Step 2 – CLONE() the controls from ?TotalHeader2 Detail onto the Page Header band ?PageHeader:

 !-------- Clone TotalHeader2 onto ?PageHeader --------
 LOOP ChildNdx=1 TO 999
     ChildFEQ=?TotalHeader2{PROP:Child,ChildNdx}     !Total Header Control FEQ
     IF ~ChildFEQ THEN BREAK.
     CloneFEQ=CLONE(0, ChildFEQ, ?PageHeader )       !Clone control of "From Band" to Page Header as Parent
     !--Set Clarion 10 User PROP's for Undo and know Clone FEQ--
     (CloneFEQ){'BandClone_FromBand'}=?TotalHeader2        !Clone set {'...From Band'} user property to Undo
     (ChildFEQ){'BandClone_IntoFEQ',?PageHeader}=CloneFEQ  !Original Child set {'...IntoFEQ'} with Clone's FEQ
 END

That’s it! When REPORT engine prints the next Page Header it will have the controls that are a Clone of the TotalHeader2 Detail. Below I’ll wrap this code in Procedures so its easy to use in your App.


Above you’ll notice some Clarion 10 User Defined Properties {'BandHide_Was'} and {'BandClone_FromBand'}. These were set to allow an UNDO of the Header changes.

Why Undo? E.g. if your “special” page has subtotals and you want to switch back to normal pages with the original page header, then do subtotals again. Or you have a 3rd or 4th different Header. In that case Undo the Clone of Header 2, then Clone Header 3, 4, etc.

To undo reverse the process and DESTROY() the Clones, then UNHIDE() the Page Header controls:

 !-------- UNDO --- Clone TotalHeader2 onto ?PageHeader --------
 LOOP ChildNdx=1 TO 999
      ChildFEQ = ?PageHeader{PROP:Child,ChildNdx}       !Page Header Control FEQ
      IF ~ChildFEQ THEN BREAK.
      IF (ChildFEQ){'BandClone_FromBand'}=?TotalHeader2 THEN  !This is a Clone() 
         DESTROY(ChildFEQ)
     END
 END
 !-------- UNDO --- Hide the Page Header Controls --------
 LOOP ChildNdx=1 TO 999
      ChildFEQ = ?PageHeader{PROP:Child,ChildNdx}         !Page Header Control FEQ
      IF ~ChildFEQ THEN BREAK.
      (ChildFEQ){PROP:Hide} = (ChildFEQ){'BandHide_Was'}  !Set to original hide
 END

One other User Property set in the Clone procedure is {'BandClone_IntoFEQ',DetailFEQ}:

(ChildFEQ){'BandClone_IntoFEQ',?PageHeader}=CloneFEQ  !Original Child set {'...IntoFEQ'} with Clone's FEQ

I created this to allow knowing the Clone control FEQ on the Header by using the FEQ from the original detail e.g. ?TotalHeader2. You could use the FEQ with {PROP:xxxx} to modify it. In the example the Total Page No text is changed to “Page#”:

  SETTARGET(REPORT)   !You can change a Cloned control's PROPs using ?OriginalFEQ{'BandClone_IntoFEQ',IntoBandFEQ}
  TotPageCloneFeq# = ?PagePromptTot{'BandClone_IntoFEQ',?PageHeader} 
  (TotPageCloneFeq#){PROP:Text} = 'Page#'    !Change 'PAGE:' to "Page#' on Page Header after Clone
  SETTARGET() 
2 Likes

Attached is project to demonstrate and with the code in functions

Rpt2HeaderClone_20251212_153854.ZIP (5.2 KB)


The example report prints pages 1 and 2 with Employee Details:


Then Hide that Page Header and Clone changes to Total Heading for Pages 3 and 4:



Undo Clone and Undo Hide changes Page 5 back to normal heading:

1 Like

Example Project code excerpts

The Hide, Clone and Undo procedurres. These 4 are what you need in your own code.

  MAP
RptBandHideControls  PROCEDURE(REPORT ReportRef, LONG BandToHideFEQ)     !Hide all controls on a BAND
RptBandHideUndo      PROCEDURE(REPORT ReportRef, LONG BandToUndoHideFEQ) !Undo Hide all controls on a BAND
RptBandCloneControls PROCEDURE(REPORT ReportRef, LONG IntoBandFEQ,     LONG CloneFromBandFEQ) !Clone all controls on a BAND
RptBandCloneUndo     PROCEDURE(REPORT ReportRef, LONG UndoIntoBandFEQ, LONG UndoFromBandFEQ)  !Undo Clone of controls on a BAND
  END

The Report Declaration and Data:

Test2Headers            PROCEDURE()
PreviewQ  QUEUE(PreviewQueue),PRE(PreQ)
    END
EmpCode   LONG
EmpName   STRING(40)
TotalCat  STRING(40)
TotalAmt  DECIMAL(9,2)
PageNoVar LONG          !See if using a Var will fix Clone PageNo

    !#3. >>>--Define your Report
Report REPORT,AT(302,1542,10500,6458),FONT('Arial',10),PRE(RPT),LANDSCAPE,PAPER(PAPER:LETTER),THOUS
        HEADER,AT(302,479,10500,1000),USE(?PageHeader)
            STRING('Report Test Page HEADER for Employee Details, i.e. not totals'),AT(240,240), |
                    USE(?STRING1),TRN,FONT(,,,FONT:bold)
            STRING(@N4),AT(6917,240),PAGENO,USE(PageNoVar),TRN,RIGHT,FONT(,10)
            STRING('PAGE:'),AT(6469,240),USE(?PagePrompt),TRN,FONT(,10)
            LINE,AT(62,500,13500,0),USE(?Line1:2),COLOR(COLOR:Black),LINEWIDTH(10)
            STRING('Code'),AT(83,660),USE(?HeadEmpCode),TRN
            STRING('Name of Employee'),AT(1083,656),USE(?HeadEmpName),TRN
            LINE,AT(73,875,13500,0),USE(?Line1),COLOR(COLOR:Black),LINEWIDTH(10)
        END
EmployeeDetail DETAIL,AT(0,0,10500,950),USE(?EmployeeDetail),WITHNEXT(1)
            STRING(@s5),AT(83,0,,156),USE(EmpCode)
            STRING(@s40),AT(1083,0,,156),USE(EmpName)
            STRING('Extra Line 1'),AT(2083,187,1813,156),USE(?Extra1)
            STRING('Extra Line 2'),AT(3083,354,1813,156),USE(?Extra2)
            STRING('Extra Line 3'),AT(4083,521,1813,156),USE(?Extra3)
            STRING('Extra Line 4'),AT(5094,698,1813,156),USE(?Extra4)
        END
Underline DETAIL,AT(0,0,,42),USE(?Underline),WITHPRIOR(1)
            LINE,AT(0,0,13500,0),USE(?LineE),COLOR(COLOR:Black),LINEWIDTH(5)
        END
TotalHeader2 DETAIL,AT(0,479,10500,1000),FONT(,,COLOR:Green),USE(?TotalHeader2)
            STRING('Total Page Header is DETAIL that will CLONE()'),AT(250,135),USE(?TitleTot)
            STRING('PAGE:'),AT(4146,135),USE(?PagePromptTot),TRN,FONT(,10)
            STRING(@N4),AT(4615,115,417),PAGENO,USE(PageNoVar,, ?ReportPageNoTot),TRN,RIGHT, |
                    FONT(,12,,FONT:bold)
            GROUP,AT(240,400,4781,377),USE(?Group4CloneTest),BOXED,TRN
                STRING('TOTAL CATEGORY'),AT(323,562),USE(?TotalHeadCat)
                STRING('TOTAL AMOUNT'),AT(3323,562),USE(?TotalHeadAmt)
            END
            LINE,AT(73,875,13500,0),USE(?TOTALSLine1),COLOR(COLOR:Green),LINEWIDTH(30)
        END
TotalDetail1 DETAIL,AT(0,0,10500,180),FONT(,,COLOR:Green),USE(?TotalDetail1)
            STRING(@s30),AT(323,0,1813,156),USE(TotalCat)
            STRING(@n11.2),AT(3281,0,1000,156),USE(TotalAmt),RIGHT
        END
    END

Report printing code with Hide and Clone calls to RptBandHideControls() and RptBandCloneControls()

    OPEN(Report)
    !--Print details using Page Header--------------------------
    LOOP EmpCode=1 TO 9
        EmpName=ALL(CHR(64+EmpCode))
        !How many Details will Print
        PRINT(RPT:EmployeeDetail)
        PRINT(RPT:Underline)
    END
    ENDPAGE(Report)

    !--Total Summary needs TotalHeader2-------------------------
  RptBandHideControls(REPORT, ?PageHeader)                        !Hide all the Page Header controls
  RptBandCloneControls(REPORT, ?PageHeader, ?TotalHeader2)        !Clone 2nd Header onto Page Header 
    LOOP T#=1 TO 9
         TotalCat='TOTAL '& ALL(T#) ; TotalAmt = RANDOM(11111,99999)
         PRINT(RPT:TotalDetail1)
    END    

    ENDPAGE(Report)         !Test page break with Total Header 2
        SETTARGET(REPORT)   !You can change a Cloned control's PROPs using ?OriginalFEQ{'BandClone_IntoFEQ',IntoBandFEQ}
        TotPageCloneFeq# = ?PagePromptTot{'BandClone_IntoFEQ',?PageHeader} 
        (TotPageCloneFeq#){PROP:Text} = 'Page#'    !Change 'PAGE:' to "Page#' on Page Header after Clone
        SETTARGET()     
    TotalCat='Sub Total on New Page ' ; TotalAmt = RANDOM(11111,99999) 
    PRINT(RPT:TotalDetail1)

    !--Totals page done, resume printing Employee Details by undoing Clone
    ENDPAGE(Report)
  RptBandCloneUndo(REPORT, ?PageHeader, ?TotalHeader2)   !Destroy Cloned 2nd Header
  RptBandHideUndo(REPORT, ?PageHeader)                   !Unhide original Page Header 
    EmpCode=91 ; EmpName='Return to Normal Page HEADER'   ; PRINT(RPT:EmployeeDetail)
    EmpCode=92 ; EmpName='After CloneUndo() & HideUndo()' ; PRINT(RPT:EmployeeDetail)
    ENDPAGE(Report)

    ENDPAGE(Report)
    DO ReportPreviewRtn
    CLOSE(Report)

Report Header HIDE and Undo Hide Functions:

RptBandHideControls PROCEDURE(REPORT ReportRef, LONG BandToHideFEQ) !Hide all controls on a BAND
ChildFEQ LONG,AUTO
ChildNdx LONG,AUTO
TargetWas &WINDOW,AUTO          !Preserve the caller SETTARGET()
    CODE
    TargetWas &= (SYSTEM{PROP:Target})
    SETTARGET(ReportRef)
    LOOP ChildNdx=1 TO 999
        ChildFEQ=(BandToHideFEQ){PROP:Child,ChildNdx}
        IF ~ChildFEQ THEN BREAK.
        (ChildFEQ){'BandHide_Was'}=(ChildFEQ){PROP:Hide}    !Save what HIDE was to Undo.
        (ChildFEQ){PROP:Hide}=TRUE
    END
    IF TargetWas &= NULL THEN SETTARGET() ELSE SETTARGET(TargetWas).
    RETURN
!--------------------------------------------------------------------------------------
RptBandHideUndo PROCEDURE(REPORT ReportRef, LONG BandToUndoHideFEQ) !Undo Hide all controls on a BAND
ChildFEQ LONG,AUTO
ChildNdx LONG,AUTO
TargetWas &WINDOW,AUTO          !Preserve the caller SETTARGET()
    CODE
    TargetWas &= (SYSTEM{PROP:Target})
    SETTARGET(ReportRef)
    LOOP ChildNdx=1 TO 999
        ChildFEQ=(BandToUndoHideFEQ){PROP:Child,ChildNdx}
        IF ~ChildFEQ THEN BREAK.
        (ChildFEQ){PROP:Hide} = (ChildFEQ){'BandHide_Was'}  !Set to value saved in Hide cALL
    END
    IF TargetWas &= NULL THEN SETTARGET() ELSE SETTARGET(TargetWas).
    RETURN

The Clone Detail code is wrapped in the RptBandCloneControls () procedure. It’s more complicated than my first example code because a Header can have a GROUP. That is the Parent of its Child controls so a recursive call to the local procedure CloneChildren() is needed.

RptBandCloneControls PROCEDURE(REPORT ReportRef, LONG IntoBandFEQ, LONG CloneFromBandFEQ) !Clone all controls on a BAND
!   E.g. RptBandCloneControls(REPORT, ?PageHeader, ?Header_2nd)
    MAP
CloneChildren   PROCEDURE(LONG IntoParentFEQ, LONG FromParentFEQ)
    END
TargetWas &WINDOW,AUTO   !Preserve the caller SETTARGET()
    CODE
    TargetWas &= (SYSTEM{PROP:Target})
    SETTARGET(ReportRef)
    CloneChildren(IntoBandFEQ, CloneFromBandFEQ)
    IF TargetWas &= NULL THEN SETTARGET() ELSE SETTARGET(TargetWas).
    RETURN

CloneChildren   PROCEDURE(LONG IntoParentFEQ, LONG FromParentFEQ)
ChildFEQ    LONG,AUTO
ChildNdx    LONG,AUTO
CloneFEQ    LONG,AUTO
PropNdx     LONG,AUTO
    CODE
    LOOP ChildNdx=1 TO 999
        ChildFEQ=(FromParentFEQ){PROP:Child,ChildNdx}
        IF ~ChildFEQ THEN BREAK.
        IF ~ChildFEQ{PROP:PageNo} THEN
            CloneFEQ=CLONE(0, ChildFEQ, IntoParentFEQ )  !Clone control of the "From Band" onto the Into
        ELSE                 
            DO PageNoCloneUsingCreateRtn
        END
        (CloneFEQ){'BandClone_FromBand'}=CloneFromBandFEQ       !Clone Control Save {'...From Band'} property so Undo can find and DESTROY clones of the From Band
        (ChildFEQ){'BandClone_IntoFEQ',IntoParentFEQ}=CloneFEQ  !Original Child Control save {'Property'} with Clone's FEQ for if I need to set PROP's 
        CASE ChildFEQ{PROP:Type}
        OF CREATE:Group
           CloneChildren(CloneFEQ, ChildFEQ)  !Into Clone Group from original Child Group with Parent as GROUP
        END
    END
    RETURN

PageNoCloneUsingCreateRtn ROUTINE  !PageNo does Not Clone() right so Create() and set all the PROP's.
    CloneFEQ=CREATE(0,ChildFEQ{PROP:Type},IntoParentFEQ)  !Create SString STRING(@N4),AT(,),USE(?ReportPageNo),PAGENO
    CloneFEQ{PROP:Use}      =ChildFEQ{Prop:Use}
    CloneFEQ{PROP:TRN}      =ChildFEQ{PROP:TRN}
    CloneFEQ{PROP:Text}     =ChildFEQ{PROP:Text}
    CloneFEQ{PROP:PageNo}   =ChildFEQ{PROP:PageNo}
    CloneFEQ{PROP:Color}    =ChildFEQ{PROP:Color}
    LOOP PropNdx=1 TO 4 ; CloneFEQ{PROP:AT  ,PropNdx}=ChildFEQ{PROP:AT  ,PropNdx} ; END
    LOOP PropNdx=1 TO 5 ; CloneFEQ{PROP:Font,PropNdx}=ChildFEQ{PROP:Font,PropNdx} ; END
    IF    ChildFEQ{PROP:Left}   THEN CloneFEQ{PROP:Left}='1'   ; CloneFEQ{PROP:LeftOffSet}   =ChildFEQ{PROP:LeftOffSet}
    ELSIF ChildFEQ{PROP:Right}  THEN CloneFEQ{PROP:Right}='1'  ; CloneFEQ{PROP:RightOffSet}  =ChildFEQ{PROP:RightOffSet}
    ELSIF ChildFEQ{PROP:Center} THEN CloneFEQ{PROP:Center}='1' ; CloneFEQ{PROP:CenterOffSet} =ChildFEQ{PROP:CenterOffSet}
    END
    CloneFEQ{PROP:Hide}    =ChildFEQ{PROP:Hide}     !Finally unhide it, but only if the original was UnHide
    EXIT   !??? do I have all the PROP's ??? 

A second complication you see above in the PageNoCloneUsingCreateRtn ROUTINE. The Page Number control (STRING(@n4),PAGENO) did not CLONE() correctly (showed 0) so I had to use CREATE(). Its possible other STRING attributes specific to Reports will have problems like CNT, SUM, AVE, MIN, MAX, RESET, TALLY, PAGE.


Finally … RptBandCloneUndo() has the code to Undo the Clone by doing a DESTROY() on all the cloned controls. The way it finds the controls is with the property {'BandClone_FromBand'} set when cloned. There are other possible ways like a Queue or putting the Clones in a Group.

RptBandCloneUndo PROCEDURE(REPORT ReportRef, LONG UndoIntoBandFEQ, LONG UndoFromBandFEQ) !Undo Clone of controls on a BAND
!   E.g. RptBandCloneUndo(REPORT, ?PageHeader, ?Header_2nd)
    MAP
DestroyCloneChildren   PROCEDURE(LONG IntoParentFEQ)
    END
DestroyQ QUEUE
FEQ         LONG
         END
DNdx     LONG
TargetWas &WINDOW,AUTO  !Preserve the caller SETTARGET()
    CODE
    TargetWas &= (SYSTEM{PROP:Target})
    SETTARGET(ReportRef)
    !The UndoIntoBandFEQ should have Children from prior Clone of Band UndoFromBandFEQ
    DestroyCloneChildren(UndoIntoBandFEQ)
    LOOP DNdx=1 TO RECORDS(DestroyQ)
         GET(DestroyQ,DNdx)
         DESTROY(DestroyQ.FEQ)    ! DB('          Destroy('& DestroyQ.FEQ & ')' )
    END
    IF TargetWas &= NULL THEN SETTARGET() ELSE SETTARGET(TargetWas).
    RETURN

DestroyCloneChildren   PROCEDURE(LONG IntoParentFEQ)
ChildFEQ    LONG,AUTO
ChildNdx    LONG,AUTO
    CODE
    LOOP ChildNdx=1 TO 999
        ChildFEQ=(IntoParentFEQ){PROP:Child,ChildNdx}   !Walk the Parent with the Clones
        IF ~ChildFEQ THEN BREAK.
        IF (ChildFEQ){'BandClone_FromBand'} <> UndoFromBandFEQ THEN   !In Clone Saved this User Prop to know what was cloned
            CYCLE
        END
        CASE ChildFEQ{PROP:Type}
        OF CREATE:Group                     !GROUP wraps Children ...
           DestroyCloneChildren(ChildFEQ)   !Help says Destroy(Group) Destroys Children, YES that worked in my test, but I decided to leave this
        END
    !!! DESTROY(ChildFEQ)                         !DESTROY changes Child Index, so after needs "ChildIdx -= 1" to work ...
        DestroyQ.FEQ = ChildFEQ ; ADD(DestroyQ)   !Seems safer to Queue them all to Destroy at the end
    END
    RETURN

The above also handles a GROUP with recursion using DestroyCloneChildren(). This is not actually required because DESTROY(Group) also destroys Children (documented in Help and verified). I choose to leave the recursive code in rather rely on that working in all versions.

1 Like

To use this in your own project you need these functions from the above example project attachment:

RptBandHideControls  PROCEDURE(REPORT ReportRef, LONG BandToHideFEQ)     !Hide all controls on a BAND
RptBandHideUndo      PROCEDURE(REPORT ReportRef, LONG BandToUndoHideFEQ) !Undo Hide all controls on a BAND
RptBandCloneControls PROCEDURE(REPORT ReportRef, LONG IntoBandFEQ,     LONG CloneFromBandFEQ) !Clone all controls on a BAND
RptBandCloneUndo     PROCEDURE(REPORT ReportRef, LONG UndoIntoBandFEQ, LONG UndoFromBandFEQ)  !Undo Clone of controls on a BAND

What I’ll probably do next is move this code into a simple CLASS or INC file to make it easier to include in a project, and allow adding extra functionality and debug.

The above assumes the Header’s are all the same AT() size. One feature I can see adding to a CLASS is code to make it easy to resize the Header AT() to match the Detail AT(), then adjust the Report AT() that specifies the body where the DETAIL’s print. Similar code for a FOOTER would work at the bottom of the page. Also code to make it easy to undo the size change.

2 Likes