2007-04-05

Full Justified Texts in your reports with GdiPlus X


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 !

hey report gurus ! colin, lisa, doug, cathy, dorin, garret, craig, bo, and others... i'd be very grateful if you could have a look at the code below, and send some feedback... and suggestions !


anyway, it's working nice.


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 - read this !


all samples below require that you have the latest planned release, 0.08a. it's a little bit hidden at codeplex, so i'm putting below the link for direct downloading:

http://www.codeplex.com/vfpx/release/projectreleases.aspx?releaseid=1711


the main gdi+ part of the custom report listener is located at the procedure "render" of the subclass.

please copy and 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(nbandobjcode, nfrxrecno)
endfunc
 
procedure render
(tnfrxrecno,;
      tnleft,tntop,tnwidth,tnheight,;
      nobjectcontinuationtype, ;
      ccontentstoberendered, gdiplusimage)
   local lctext
   lctext =
this.arecords(tnfrxrecno,1)


   if vartype(lctext) = "c" and lctext = "<fj>"
      lctext =
substr(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
      lorectf = .rectanglef.new(tnleft, tntop, tnwidth, tnheight)


      * create a font object based on the report original settings
      local lofont as xfcfont
      lofont = .
font.new(this.arecords(tnfrxrecno,2) ;
         ,
this.arecords(tnfrxrecno,4), this.arecords(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[tnfrxrecno,8] <> 0 && alpha


         * retrieve colors for the background
         local lnred, lngreen, lnblue, lnalpha
         lnred =
this.arecords[tnfrxrecno,5]
         lngreen =
this.arecords[tnfrxrecno,6]
         lnblue =
this.arecords[tnfrxrecno,7]
         lnalpha =
this.arecords[tnfrxrecno,8]


         * create a solid brush that will be used to draw the background
         local lobackbrush as xfcsolidbrush
         lobackbrush = .solidbrush.new(;
            .
color.fromargb(lnalpha,lnred, lngreen, lnblue))


         * draw the background rectangle
         this.ogdigraphics.fillrectangle(lobackbrush, tnleft, tntop, tnwidth, tnheight)
      endif


      * retieve colors for the text
      lnred = this.arecords[tnfrxrecno,9]
      lngreen =
this.arecords[tnfrxrecno,10]
      lnblue =
this.arecords[tnfrxrecno,11]
      lnalpha =
this.arecords[tnfrxrecno,12]


      * create a solid brush that will be used to draw the text
      local lotextbrush as xfcsolidbrush
      lotextbrush = .solidbrush.new(;
         .
color.fromargb(lnalpha,lnred, lngreen, lnblue))


      * finally, draw the text in fulljustified mode.
      this.ogdigraphics.drawstringjustified(lctext, lofont, lotextbrush, lorectf)
      endwith
   else


      * if we're not drawing a full justified string,
      * let vfp draw the text as usual.
      dodefault(tnfrxrecno, tnleft, tntop, tnwidth, tnheight, ;
      nobjectcontinuationtype, ccontentstoberendered, 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[tnfrxrecno,1] = toobjproperties.text
   this
.arecords[tnfrxrecno,2] = toobjproperties.fontname
   this
.arecords[tnfrxrecno,3] = toobjproperties.fontstyle
   this.arecords[tnfrxrecno,4] = toobjproperties.fontsize
   this
.arecords[tnfrxrecno,5] = toobjproperties.fillred
   this.arecords[tnfrxrecno,6] = toobjproperties.fillgreen
   this.arecords[tnfrxrecno,7] = toobjproperties.fillblue
   this.arecords[tnfrxrecno,8] = toobjproperties.fillalpha
   this.arecords[tnfrxrecno,9] = toobjproperties.penred
   this.arecords[tnfrxrecno,10] = toobjproperties.pengreen
   this.arecords[tnfrxrecno,11] = toobjproperties.penblue
   this.arecords[tnfrxrecno,12] = 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
loreportlistener =
newobject("fulljustifylistener",locfile("fjlistener.prg"))
* loreportlistener = createobject("fulljustifylistener")
loreportlistener.listenertype = 1
report form myreport object 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 chalom 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(nbandobjcode, nfrxrecno)
endfunc


procedure render(tnfrxrecno,;
      tnleft,tntop,tnwidth,tnheight,;
      nobjectcontinuationtype, ;
      ccontentstoberendered, gdiplusimage)
   if vartype(this.arecords(tnfrxrecno,13)) = "c" and ;
      this.arecords(tnfrxrecno,13) == "fj"
      local lctext
      lctext =
this.arecords(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
         lorectf = .rectanglef.new(tnleft, tntop, tnwidth, tnheight)


         * create a font object based on the report original settings
         local lofont as xfcfont
         lofont = .
font.new(this.arecords(tnfrxrecno,2) ;
            ,
this.arecords(tnfrxrecno,4), this.arecords(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[tnfrxrecno,8] <> 0 && alpha
            * retrieve colors for the background
            local lnred, lngreen, lnblue, lnalpha
            lnred =
this.arecords[tnfrxrecno,5]
            lngreen =
this.arecords[tnfrxrecno,6]
            lnblue =
this.arecords[tnfrxrecno,7]
            lnalpha =
this.arecords[tnfrxrecno,8]


            * create a solid brush that will be used to draw the background
            local lobackbrush as xfcsolidbrush
            lobackbrush = .solidbrush.new(;
               .
color.fromargb(lnalpha,lnred, lngreen, lnblue))


            * draw the background rectangle
            this.ogdigraphics.fillrectangle(lobackbrush, tnleft, tntop, tnwidth, tnheight)
         endif


         * retieve colors for the text
         lnred = this.arecords[tnfrxrecno,9]
         lngreen =
this.arecords[tnfrxrecno,10]
         lnblue =
this.arecords[tnfrxrecno,11]
         lnalpha =
this.arecords[tnfrxrecno,12]


         * create a solid brush that will be used to draw the text
         local lotextbrush as xfcsolidbrush
         lotextbrush = .solidbrush.new(;
            .
color.fromargb(lnalpha,lnred, lngreen, lnblue))


         * finally, draw the text in fulljustified mode.
         this.ogdigraphics.drawstringjustified(lctext, lofont, lotextbrush, lorectf)
      endwith
   else
      * if we're not drawing a full justified string,
      * let vfp draw the text as usual.
      dodefault(tnfrxrecno, tnleft, tntop, tnwidth, tnheight, ;
         nobjectcontinuationtype, ccontentstoberendered, 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[tnfrxrecno,1] = toobjproperties.text
   this
.arecords[tnfrxrecno,2] = toobjproperties.fontname
   this
.arecords[tnfrxrecno,3] = toobjproperties.fontstyle
   this.arecords[tnfrxrecno,4] = toobjproperties.fontsize
   this
.arecords[tnfrxrecno,5] = toobjproperties.fillred
   this.arecords[tnfrxrecno,6] = toobjproperties.fillgreen
   this.arecords[tnfrxrecno,7] = toobjproperties.fillblue
   this.arecords[tnfrxrecno,8] = toobjproperties.fillalpha
   this.arecords[tnfrxrecno,9] = toobjproperties.penred
   this.arecords[tnfrxrecno,10] = toobjproperties.pengreen
   this.arecords[tnfrxrecno,11] = toobjproperties.penblue
   this.arecords[tnfrxrecno,12] = 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

24 comments:

  1. Hi Cesar, as always, excellent code, Calvin's and yours are my all time favorite blogs.

    Just 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

    ReplyDelete
  2. Thank's Cesar, for your sample,

    I 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

    ReplyDelete
  3. 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.


    Regards


    Victor

    ReplyDelete
  4. 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  :)


    It is a pleasure to contribute to your Blog ...


    Thank you


    Victor


    ReplyDelete
  5. 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.
    So 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 

    ReplyDelete
  6. Hi Victor,

    Thanks for the suggestions, it's working like a charm.

    I've just updated the post, with the full code.

    Thanks again

    Cesar

    ReplyDelete
  7. the FJ is functioning well. thank you.
    the 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

    ReplyDelete
  8. 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.
    Hi, 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!

    ReplyDelete
  9. sir I saw yor reply

    sorry 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

    ReplyDelete
  10. Thank's Cesar, for your sample, Great Cesar

    but ...

    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)

    ReplyDelete
  11. fernando mesquitaMarch 15, 2008 at 9:26 PM

    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.

    Será que era possível disponibilizá-lo já traduzido para Português e enviadi para o seguinte Email : fernando.p.mesquita@iol.pt

    ReplyDelete
  12. Alejandro FernándezMay 27, 2008 at 9:00 PM

    Cómo podría integrarlo con xfrx para convertir mi report a pdf pero con texto justificado de un campo memo del fichero pdf ?.

    Espero 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 !

    ReplyDelete
  13. I 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.
    Please just follow the steps I provided here. If you face any problems, please tell me exactly what is not going well.

    ReplyDelete
  14. 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
    Hi,
    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 !

    ReplyDelete
  15. 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.

    Gracias 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

    ReplyDelete
  16. Good afternoon. I find television very educating. Every time somebody turns on the set, I go into the other room and read a book.

    I 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.

    ReplyDelete
  17. I'm sure this is really nice, but unfortunately I cannot get it to work. I copied the code, including Victor's modifications.


    1.  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.

    ReplyDelete
  18. Sorry. I menat to leave my email if someone needs it.

    mkatzla@juno.com

    ReplyDelete
  19. Sir,

    Is 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 !

    ReplyDelete
  20. 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?

    ReplyDelete