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
Hi Cesar, as always, excellent code, Calvin's and yours are my all time favorite blogs.
ReplyDeleteJust an idea, wouldn't it be better to use 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. All you would need is:
- Move tag to user data field (apparently designed for custom user tags)
- Increase "arecords" array to 13
- In the "EvaluateContents" function add:
This.aRecords[tnFRXRecno,13] = toObjProperties.User
- In the Render method, validate against aRecords[tnFRXRecno,13] for "<FJ>"
I'm no expert either, it's just a thought.
Regards
Victor
Hi Victor,
This sounds very nice, and I agree to move the tag to the "USER" tab. I'll just wait some more time, to see if some more suggestions arise.
Thanks very much !
Saludos
Cesar
Thank's Cesar, for your sample,
ReplyDeleteI was waited this sample since long time,
Thank's also to victor for your answer.
(but the link UT report preview Custom is broken)
I will making on in action your sample !
Thank's
Olivier
Hi Olivier,
The links seems to be working fine here... You can also enter UT directly: www.universalthread.com , go to the downloads section, and search for "Dorin Vasilescu".
HTH[
Cesar
Hi Cesar, I found a problem in my approach, maybe you can help. Somehow, the property "toObjProperties.User" in the "EvaluateContents" function does not pass along the values in the User field of the Report file. Thus, the Render procedure does not activate the Justification routine. I'm still working on a solution, your original code works perfect.
ReplyDeleteRegards
Victor
Hi Cesar, found a solution for the EvaluateContents problem, please disregard previous post and this one, I will post a corrected suggestion in a couple of minutes... for all to see :)
ReplyDeleteIt is a pleasure to contribute to your Blog ...
Thank you
Victor
Hi Cesar, here is my updated suggestion. My original idea was sound, but flawed, in the sense that the "toObjProperties" object in the "EvaluateContents" function does not contain a property with the value of the "user" field in the report file. Thus, the validation in the Render method would never find the <FJ> tag.
ReplyDeleteSo here is my updated suggestion; I have tried it and is working properly now.
- move the <FJ> tag to the "User" field (under "Other" tab of textbox properties)
(this avoids affecting output when using alternate listener)
- modify the BeforeReport method like so:
FUNCTION BEFOREREPORT
DODEFAULT()
WITH This
* Check if we already have the "System" object in "_Screen"
IF NOT PEMSTATUS(_Screen,"System",5)
_SCREEN.AddProperty("System", NEWOBJECT("xfcSystem", locfile("system.vcx")))
ENDIF
.oGDIGraphics = _SCREEN.SYSTEM.Drawing.Graphics.New()
.SetFRXDataSession()
DIMENSION .aRecords[reccount(), 13]
SCAN FOR "<FJ>" $ UPPER(User)
.aRecords[recno(), 13] = "FJ"
ENDSCAN
.ResetDataSession()
ENDWITH
ENDFUNC
- In the Render method validate like so :
IF VARTYPE(This.aRecords(tnFRXRecNo,13)) = "C" AND This.aRecords(tnFRXRecNo,13) == "FJ"
Note: there is no need anymore to trim <FJ> tag from lcText variable
Thank you for the chance to contribute to your excellent Blog....
Regards
Victor Espinoza
Miami, FL
Hi Victor,
Thanks very much for the suggestions. Unfortunately, I'm out of town, at this moment, and without my working machine, so I did not test any of the suggestions. But this last makes a lot of sense. Have you tested the performance of both aproaches ?
Regards
Cesar
Hi Victor,
ReplyDeleteThanks for the suggestions, it's working like a charm.
I've just updated the post, with the full code.
Thanks again
Cesar
Cool !
ReplyDeletethe FJ is functioning well. thank you.
ReplyDeletethe problem of splitting text between pages is not sorted out.
please look into and post at the arliest for community.
Hi, sorry for the late answer...
I don`t unsderstand what you mean, can you explain it better ? Maybe sending me some screenshots ?
Thanks in advance
Cesar
I have a program printing a daily report with about 20 lines that need to be full justified, the report is printing for 500 clients in one shot, vfp9 sp1. How much of a slowdown do you think it would be to include this listener to full justify the lettering.
ReplyDeleteHi, sorry for the late answer... :-(
For sure this report will run slower using a listener. The full justify function is really extremely fast. You can try it using the full justify samples from the GdiPlusX library.
I really don't know about how important the delay will be in a big report. I suggest you to make some tests, and tell us how it works for you. At this moment, for my own needs, it's working nice, and I didn`t notice a significant delay.
Hope to hear from your tests soon!
sir I saw yor reply
ReplyDeletesorry I could not explain properly.
The problem is the memo field having more lines of data running in to pages, while drawing the same on report from one page to next page. The data already rendered in the previous page is repeating in the next page and truncating/ limiting the balance to be rendered.
actually it has to render balance lines only on the subsequent pages till the data is complete.
it needs additional handling which has been explained by BO Durban in his clear method of rendering RTF in reports without full justification.
shortly i will send screen shots / movie for better understanding
kindly pardon me i fail to explain my views.
thanking you sir
--ravisobhan
Thank's Cesar, for your sample, Great Cesar
ReplyDeletebut ...
I found bug in gdiplusx when i try use, SET ANSI ON, SET EXACT ON, SET NEAR ON the report cannot fulljustify. Then When I try Use Clear dlls, Error in xfcbrush (vcx error)
O exemplo de "justificar texto" corresponde ao que preciso. No entanto, tenho alguma dificuldade de traduzir todas as indicações de Inglês/Português.
ReplyDeleteSerá que era possível disponibilizá-lo já traduzido para Português e enviadi para o seguinte Email : fernando.p.mesquita@iol.pt
Cómo podría integrarlo con xfrx para convertir mi report a pdf pero con texto justificado de un campo memo del fichero pdf ?.
ReplyDeleteEspero su ayuda. Gracias
Hola,
Desafortunadamentem nunca utilize XFRX. Pero convertir la grafica para una imagen es super simples ! Puedes salvar en un archiivo de imagen o entonces imprimir directamiente. Mire el codigo en el boton "Print" del formulario de ejemplo.
Saludos !
Thanks for the sample
ReplyDeleteI need so much this, plese if you can, post a full compiled project with this justifying option. Put it in an archive, please. because i tried to make this work, and i couldn't. i'm a beginner with FoxPro. Please help me.
ReplyDeletePlease just follow the steps I provided here. If you face any problems, please tell me exactly what is not going well.
I have a form, an editbox and a button. I just want to enter some text in that editbox and when i press the button, i want to preview the report with that text from the editbox, full justified. that's all. can you help me? please
ReplyDeleteHi,
I'm sorry, but this is not on the scope of this article. I suggest you to adapt the FRX and the PRG I used for this sample. You'll loose just some few minutes, and I'm sure you'll obtain the results you need.
For more detailed information, you may try the Foxite forum, and ask the users directly: www.foxite.com;forum - it's free ! and one of the best VFP resources we have!
Good luck !
Utilicé tu herramienta, me parece genial y ha funcionado sin problemas en los equipos clientes (HP, Compaq, Dell, Toshiba, Acer, Gateway), sin embargo se me presentó un problema en un equipo portatil HP Pavillion DV6420la AMD Turion 64 X2 Dual Core, con tarjeta gráfica NVIDIA GeForce Go 6150; al enviar la impresión no muestra la previsualización ni imprime. No sé que pueda estar pasando.
ReplyDeleteGracias por su ayuda.
Eduardo Laguna
Hola Eduardo,
Sinceramente, no tengo ninguna idea de como ayudarle... El problema solo acontece con esta clase o tambien quando imprimes desde el generador de reportes convencional?
Saludos
Cesar
Thank you..
ReplyDeleteGood afternoon. I find television very educating. Every time somebody turns on the set, I go into the other room and read a book.
ReplyDeleteI am from Luxembourg and also now am reading in English, please tell me right I wrote the following sentence: "Cheap air tickets available for new delhi, goa, bangalore, chennai, calcutta, air tickets available from bangalore hyderabad for rs."
Thanks for the help :p, Onur.
I'm sure this is really nice, but unfortunately I cannot get it to work. I copied the code, including Victor's modifications.
ReplyDelete1. I cannot locate "System.vcx"
2. If I use the "Function BeforeReport", the report runs bur the text is not modified. The vale in the expression field is a string, not a memo, but I don't think it should make a difference. I set the ReportBehaviot to 90 and Listenertype to 0.
Can I be missing somethin else.
Sorry. I menat to leave my email if someone needs it.
ReplyDeletemkatzla@juno.com
Sir,
ReplyDeleteIs there any development on RTF text with full justification in reports?
Yes, there is. Look for Bo Durban and For Mike Gagnon's articles regarding this on the web !
I found this article very good, but I have problem with my exe application. When I working in project everything is fine, but when I build exe, my report doesnt render. I include all files in project. Any suggestion?
ReplyDeleteThank you comment
ReplyDelete