This is a continuation of my previous post, Full-Justified Text with GdiPlusX . Now I'll show how we can use the new method DRAWSTRINGJUSTIFIED from GdiPlusX library to have full Justified texts in our reports.
There are 2 ways of using this feature in reports.
The most obvious is to use the picture saved in the samples from the previous post, and use it directly in the report.
But VFP9 brings so many other cool possibilities, that I couldn't leave them without a try, so I decided to create a subclass of the Report Listener that will transform some of the texts from my reports to Full-Justified. My idea was that the user would just need to add a "<FJ>" tag in the beginning of any string from a textbox in a report, to tell my ReportListener that it will draw the text using the DRAWSTRINGJUSTIFIED method from GdiPlusX.
Please note that I'm NO expert in the new Object Assisted Reports from VFP9. My experience using Report Listeners is really very limited. I'm sure that the report listener below can be improved, so, if you have any suggestion that can improve the performance or ease the process, please tell me !
My "FullJustifyListener" performs the following actions:
- Initializes GdiPlusX
- Creates a GDI+ Graphics object that will be used to draw in the report
- Stores in an array the required information needed to draw the string(Font, Size, Style and Color)
- Before Rendering the string, checks if the "<FJ>" tag is at the beginning of text - if yes, draws the string using the new method.
VERY IMPORTANT
The main GDI+ part of the custom report listener is located at the procedure "Render" of the subclass.
Paste the code below, and save it as FJLISTENER.PRG in the samples folder of GdiPlusX
DEFINE CLASS FullJustifyListener AS _ReportListener OF ;
ADDBS(HOME(1)) + "FFC\_ReportListener.VCX"
oGDIGraphics = NULL
DIMENSION aRecords[1]
* Before we run the report, we need to Make sure that GdiPlusX
* was initialized, and create a new Graphics object
FUNCTION BEFOREREPORT
DODEFAULT()
WITH THIS
* Check if we already have the "System" object in "_Screen"
IF NOT PEMSTATUS(_SCREEN, "System", 5)
DO LOCFILE("System.App")
ENDIF
.oGDIGraphics = _SCREEN.SYSTEM.Drawing.Graphics.New()
.SetFRXDataSession()
DIMENSION .aRecords[reccount(), 12]
.ResetDataSession()
ENDWITH
ENDFUNC
FUNCTION BEFOREBAND(nBandObjCode, nFRXRecNo)
THIS.SharedGDIPlusGraphics = THIS.GDIPLUSGRAPHICS
THIS.oGDIGraphics.Handle = THIS.SharedGDIPlusGraphics
DODEFAULT(m.nBandObjCode, m.nFRXRecNo)
ENDFUNC
PROCEDURE RENDER(tnFRXRecNo, ;
tnLeft, tnTop, tnWidth, tnHeight, ;
nObjectContinuationType, ;
cContentsToBeRendered, GDIPlusImage)
LOCAL lcText
m.lcText = THIS.aRecords(m.tnFRXRecNo, 1)
IF VARTYPE(m.lcText) = "C" AND m.lcText = "<FJ>"
m.lcText = SUBSTR(m.lcText, 5) && Remove the <FJ> tag from string
THIS.oGDIGraphics.Handle = THIS.GDIPLUSGRAPHICS
WITH _SCREEN.SYSTEM.Drawing
* Create a GDI+ Rectangle which specifies where on the
* surface we're drawing the text.
LOCAL loRectF AS xfcRectangleF
m.loRectF = .RectangleF.New(m.tnLeft, m.tnTop, m.tnWidth, m.tnHeight)
* Create a Font Object based on the report original settings
LOCAL loFont AS xfcFont
m.loFont = .FONT.New(THIS.aRecords(m.tnFRXRecNo, 2) ;
, THIS.aRecords(m.tnFRXRecNo, 4), THIS.aRecords(m.tnFRXRecNo, 3) ;
, .GraphicsUnit.POINT)
* If we have an opaque background set for the text, then draw a rectangle
* using the background chosen background color
IF THIS.aRecords[m.tnFRXRecno, 8] <> 0 && Alpha
* Retrieve colors for the background
LOCAL lnRed, lnGreen, lnBlue, lnAlpha
m.lnRed = THIS.aRecords[m.tnFRXRecno, 5]
m.lnGreen = THIS.aRecords[m.tnFRXRecno, 6]
m.lnBlue = THIS.aRecords[m.tnFRXRecno, 7]
m.lnAlpha = THIS.aRecords[m.tnFRXRecno, 8]
* Create a Solid Brush that will be used to draw the Background
LOCAL loBackBrush AS xfcSolidBrush
m.loBackBrush = .SolidBrush.New(;
.COLOR.FromArgb(m.lnAlpha, m.lnRed, m.lnGreen, m.lnBlue))
* Draw the background rectangle
THIS.oGDIGraphics.FillRectangle(m.loBackBrush, m.tnLeft, m.tnTop, m.tnWidth, m.tnHeight)
ENDIF
* Retieve colors for the Text
m.lnRed = THIS.aRecords[m.tnFRXRecno, 9]
m.lnGreen = THIS.aRecords[m.tnFRXRecno, 10]
m.lnBlue = THIS.aRecords[m.tnFRXRecno, 11]
m.lnAlpha = THIS.aRecords[m.tnFRXRecno, 12]
* Create a Solid Brush that will be used to draw the text
LOCAL loTextBrush AS xfcSolidBrush
m.loTextBrush = .SolidBrush.New(;
.COLOR.FromArgb(m.lnAlpha, m.lnRed, m.lnGreen, m.lnBlue))
* Finally, draw the text in FullJustified mode.
THIS.oGDIGraphics.DrawStringJustified(m.lcText, m.loFont, m.loTextBrush, m.loRectF)
ENDWITH
ELSE
* If we're not drawing a full justified string,
* let VFP draw the text as usual.
DODEFAULT(m.tnFRXRecNo, m.tnLeft, m.tnTop, m.tnWidth, m.tnHeight, ;
m.nObjectContinuationType, m.cContentsToBeRendered, m.GDIPlusImage)
ENDIF
* Since we already drew the text, we don't want the default
* behavior to occur.
NODEFAULT
ENDPROC
FUNCTION EVALUATECONTENTS(tnFRXRecNo, toObjProperties)
* Get the FRX data
THIS.aRecords[m.tnFRXRecno, 1] = m.toObjProperties.TEXT
THIS.aRecords[m.tnFRXRecno, 2] = m.toObjProperties.FONTNAME
THIS.aRecords[m.tnFRXRecno, 3] = m.toObjProperties.FontStyle
THIS.aRecords[m.tnFRXRecno, 4] = m.toObjProperties.FONTSIZE
THIS.aRecords[m.tnFRXRecno, 5] = m.toObjProperties.FillRed
THIS.aRecords[m.tnFRXRecno, 6] = m.toObjProperties.FillGreen
THIS.aRecords[m.tnFRXRecno, 7] = m.toObjProperties.FillBlue
THIS.aRecords[m.tnFRXRecno, 8] = m.toObjProperties.FillAlpha
THIS.aRecords[m.tnFRXRecno, 9] = m.toObjProperties.PenRed
THIS.aRecords[m.tnFRXRecno, 10] = m.toObjProperties.PenGreen
THIS.aRecords[m.tnFRXRecno, 11] = m.toObjProperties.PenBlue
THIS.aRecords[m.tnFRXRecno, 12] = m.toObjProperties.PenAlpha
ENDFUNC
ENDDEFINE
To use it is very simple:
Open any report, and select a textbox that has a memo field associated to it. In the field properties, tab General, on expression, just add a "<FJ>" string before the original field. Supposing you had the expression "MyTable.MyMemoField", to have it justified, you'll change it to: "<FJ>" + MyTable.MyMemoField.
The next step is to make sure that VFP will use our new listener to render the report.
* Tell VFP that we'll be using the new report features
SET REPORTBEHAVIOR 90
LOCAL loReportListener
m.loReportListener = NEWOBJECT("FullJustifyListener", LOCFILE("FJListener.prg"))
* loReportListener = CREATEOBJECT("FullJustifyListener")
m.loReportListener.LISTENERTYPE = 1
REPORT FORM MyReport OBJECT m.loReportListener
UPDATE 04/07/2007
After some good suggestions from Victor Espinoza, below is a modified version of the "FJListener" class, that will receivethe <FJ> tag at the user data field for the textbox (under the "Other" tab), this way, the "<FJ>" tag wouldn't affect the reports if it was latter decided to use an alternative report listener.
* Program : FJLISTENER.PRG
* Purpose : Provides a Report Listener that allows rendering text in
* Full Justify alignment.
* Author : Cesar Ch and Victor Espinoza
* Class based on article "Listening to a report" by Doug Hennig
* http://msdn2.microsoft.com/en-us/library/ms947682.aspx
DEFINE CLASS FullJustifyListener AS _ReportListener OF ;
ADDBS(HOME(1)) + "FFC\_ReportListener.VCX"
oGDIGraphics = NULL
DIMENSION aRecords[1]
* Doug Hennig - If ListenerType hasn't already been set, set it based on whether the report
* is being printed or previewed.
FUNCTION LOADREPORT
WITH THIS
DO CASE
CASE .LISTENERTYPE <> -1
CASE .COMMANDCLAUSES.PREVIEW
.LISTENERTYPE = 1
CASE .COMMANDCLAUSES.OutputTo = 1
.LISTENERTYPE = 0
ENDCASE
ENDWITH
DODEFAULT()
ENDFUNC
* Before we run the report, we need to Make sure that GdiPlusX
* was initialized, and create a new Graphics object
FUNCTION BEFOREREPORT
DODEFAULT()
WITH THIS
* Check if we already have the "System" object in "_Screen"
IF NOT PEMSTATUS(_SCREEN, "System", 5)
DO LOCFILE("System.App")
ENDIF
.oGDIGraphics = _SCREEN.SYSTEM.Drawing.Graphics.New()
.SetFRXDataSession() && switches to the FRX cursor's datasession
DIMENSION .aRecords(RECCOUNT(), 13)
SCAN FOR "<FJ>" $ UPPER(USER)
.aRecords(RECNO(), 13) = "FJ"
ENDSCAN
.ResetDataSession() && restores the datasession ID to the one the listener is in
ENDWITH
ENDFUNC
FUNCTION BEFOREBAND(nBandObjCode, nFRXRecNo)
THIS.SharedGDIPlusGraphics = THIS.GDIPLUSGRAPHICS
THIS.oGDIGraphics.Handle = THIS.SharedGDIPlusGraphics
DODEFAULT(m.nBandObjCode, m.nFRXRecNo)
ENDFUNC
PROCEDURE RENDER(tnFRXRecNo, ;
tnLeft, tnTop, tnWidth, tnHeight, ;
nObjectContinuationType, ;
cContentsToBeRendered, GDIPlusImage)
IF VARTYPE(THIS.aRecords(m.tnFRXRecNo, 13)) = "C" AND ;
THIS.aRecords(m.tnFRXRecNo, 13) == "FJ"
LOCAL lcText
m.lcText = THIS.aRecords(m.tnFRXRecNo, 1)
THIS.oGDIGraphics.Handle = THIS.GDIPLUSGRAPHICS
WITH _SCREEN.SYSTEM.Drawing
* Create a GDI+ Rectangle which specifies where on the
* surface we're drawing the text.
LOCAL loRectF AS xfcRectangleF
m.loRectF = .RectangleF.New(m.tnLeft, m.tnTop, m.tnWidth, m.tnHeight)
* Create a Font Object based on the report original settings
LOCAL loFont AS xfcFont
m.loFont = .FONT.New(THIS.aRecords(m.tnFRXRecNo, 2) ;
, THIS.aRecords(m.tnFRXRecNo, 4), THIS.aRecords(m.tnFRXRecNo, 3) ;
, .GraphicsUnit.POINT)
* If we have an opaque background set for the text, then draw a rectangle
* using the background chosen background color
IF THIS.aRecords[m.tnFRXRecno, 8] <> 0 && Alpha
* Retrieve colors for the background
LOCAL lnRed, lnGreen, lnBlue, lnAlpha
m.lnRed = THIS.aRecords[m.tnFRXRecno, 5]
m.lnGreen = THIS.aRecords[m.tnFRXRecno, 6]
m.lnBlue = THIS.aRecords[m.tnFRXRecno, 7]
m.lnAlpha = THIS.aRecords[m.tnFRXRecno, 8]
* Create a Solid Brush that will be used to draw the Background
LOCAL loBackBrush AS xfcSolidBrush
m.loBackBrush = .SolidBrush.New(;
.COLOR.FromArgb(m.lnAlpha, m.lnRed, m.lnGreen, m.lnBlue))
* Draw the background rectangle
THIS.oGDIGraphics.FillRectangle(m.loBackBrush, m.tnLeft, m.tnTop, m.tnWidth, m.tnHeight)
ENDIF
* Retieve colors for the Text
m.lnRed = THIS.aRecords[m.tnFRXRecno, 9]
m.lnGreen = THIS.aRecords[m.tnFRXRecno, 10]
m.lnBlue = THIS.aRecords[m.tnFRXRecno, 11]
m.lnAlpha = THIS.aRecords[m.tnFRXRecno, 12]
* Create a Solid Brush that will be used to draw the text
LOCAL loTextBrush AS xfcSolidBrush
m.loTextBrush = .SolidBrush.New(;
.COLOR.FromArgb(m.lnAlpha, m.lnRed, m.lnGreen, m.lnBlue))
* Finally, draw the text in FullJustified mode.
THIS.oGDIGraphics.DrawStringJustified(m.lcText, m.loFont, m.loTextBrush, m.loRectF)
ENDWITH
ELSE
* If we're not drawing a full justified string,
* let VFP draw the text as usual.
DODEFAULT(m.tnFRXRecNo, m.tnLeft, m.tnTop, m.tnWidth, m.tnHeight, ;
m.nObjectContinuationType, m.cContentsToBeRendered, m.GDIPlusImage)
ENDIF
* Since we already drew the text, we don't want the default
* behavior to occur.
NODEFAULT
ENDPROC
FUNCTION EVALUATECONTENTS(tnFRXRecNo, toObjProperties)
* Get the FRX data
THIS.aRecords[m.tnFRXRecno, 1] = m.toObjProperties.TEXT
THIS.aRecords[m.tnFRXRecno, 2] = m.toObjProperties.FONTNAME
THIS.aRecords[m.tnFRXRecno, 3] = m.toObjProperties.FontStyle
THIS.aRecords[m.tnFRXRecno, 4] = m.toObjProperties.FONTSIZE
THIS.aRecords[m.tnFRXRecno, 5] = m.toObjProperties.FillRed
THIS.aRecords[m.tnFRXRecno, 6] = m.toObjProperties.FillGreen
THIS.aRecords[m.tnFRXRecno, 7] = m.toObjProperties.FillBlue
THIS.aRecords[m.tnFRXRecno, 8] = m.toObjProperties.FillAlpha
THIS.aRecords[m.tnFRXRecno, 9] = m.toObjProperties.PenRed
THIS.aRecords[m.tnFRXRecno, 10] = m.toObjProperties.PenGreen
THIS.aRecords[m.tnFRXRecno, 11] = m.toObjProperties.PenBlue
THIS.aRecords[m.tnFRXRecno, 12] = m.toObjProperties.PenAlpha
ENDFUNC
ENDDEFINE
For now this seems good to me. But there's still some more to improve here. For the next version this can become a "Directive" to be used together with other custom report listeners, as Doug Hennig did in his great article "Listening to a Report"
Related links that helped me to understand how the Report Listener works:
Doug Hennig - Listening to a Report
Doug Hennig - Hyperlink your reports
Doug Hennig - Extending the Visual FoxPro 9 Reporting System
Cathy Pountney - VFP 9.0 Report Writer In Action
Cathy Pountney - What's New in the VFP 9 Report Writer
Garret Fitzgerald - Using the GDI+ FFCs to work with output