Getting graphics for richtextbox

Bulldog

Member
Joined
Nov 13, 2006
Messages
10
Programming Experience
Beginner
I'm using the code in http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwinforms/html/wnf_RichTextBox.asp to print richtextboxes.

However, I have a form with multiple richtextboxes on it and I wrote a sub to convert the whole form to GDI. This just inspects each object, draws its tline, fills its color, add any text etc. Simple stuff and it works great... apart from richtextboxes, I can get the outline, backcolor no problem but the text formatting is a mare.

Is there any way I can get the formatted graphics returned from the code shown in the link above? I dont want to direct the result to a printer until later, I want to include richtextboxes in my sub so that I have a routine the draws the full graphical representation of the form.

My attempt so far is shown below, but this is junk since the FormatRange function is sending a message to a printer, not drawing/returning the graphics.

The part of my code which handles RTB's is;

VB.NET:
[SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE][SIZE=2] IsRichTextBoxEx [/SIZE][SIZE=2][COLOR=#0000ff]Then[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] RTB [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] RichTextBoxEx[/SIZE]
[SIZE=2]RTB = ctl[/SIZE]
[SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE][SIZE=2] RTB.Visible [/SIZE][SIZE=2][COLOR=#0000ff]Then[/COLOR][/SIZE]
[SIZE=2][COLOR=#008000]'Text[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] RTBRect [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Rectangle[/SIZE]
[SIZE=2]RTBRect = [/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Rectangle(YearPanel.PointToClient(ctl.Parent.PointToScreen(ctl.Location)).X, _[/SIZE]
[SIZE=2]YearPanel.PointToClient(ctl.Parent.PointToScreen(ctl.Location)).Y, ctl.Width, ctl.Height)[/SIZE]
[SIZE=2]RTB.FormatRange(g, RTBRect, 0, RTB.TextLength)[/SIZE]
[SIZE=2]RTB.FormatRangeDone()[/SIZE]
[SIZE=2][COLOR=#008000]'BackColor[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE][SIZE=2] ctl.BackColor <> PanelBackColor [/SIZE][SIZE=2][COLOR=#0000ff]Then[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] ctlBackBrush [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] SolidBrush(SetColor(ctl.BackColor))[/SIZE]
[SIZE=2]g.FillRectangle(ctlBackBrush, CtlRect)[/SIZE]
[SIZE=2][COLOR=#0000ff]End[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE]
[SIZE=2][COLOR=#008000]'Outline[/COLOR][/SIZE]
[SIZE=2]g.DrawRectangle(Pens.Black, CtlRect)[/SIZE]
[SIZE=2][COLOR=#0000ff]End[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]End[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE]

My RTBEx code is as per the link except for the function below where I changed the incoming parameters from printargs to graphics and the target rectangle;

VB.NET:
[SIZE=2][COLOR=#0000ff]Public[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Function[/COLOR][/SIZE][SIZE=2] FormatRange([/SIZE][SIZE=2][COLOR=#0000ff]ByVal[/COLOR][/SIZE][SIZE=2] e [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Graphics, _[/SIZE]
[SIZE=2][COLOR=#0000ff]ByVal[/COLOR][/SIZE][SIZE=2] r [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Rectangle, _[/SIZE]
[SIZE=2][COLOR=#0000ff]ByVal[/COLOR][/SIZE][SIZE=2] charFrom [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Integer[/COLOR][/SIZE][SIZE=2], _[/SIZE]
[SIZE=2][COLOR=#0000ff]ByVal[/COLOR][/SIZE][SIZE=2] charTo [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Integer[/COLOR][/SIZE][SIZE=2]) [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Integer[/COLOR][/SIZE]
[SIZE=2][COLOR=#008000]' Specify which characters to print[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] cr [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] STRUCT_CHARRANGE[/SIZE]
[SIZE=2]cr.cpMin = charFrom[/SIZE]
[SIZE=2]cr.cpMax = charTo[/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] rect [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Rectangle[/SIZE]
[SIZE=2]rect = [/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Rectangle(r.Location.X, r.Location.Y, r.Width, r.Height)[/SIZE]
[SIZE=2][COLOR=#008000]' Specify the area inside page margins[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] rc [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] STRUCT_RECT[/SIZE]
[SIZE=2]rc.top = HundredthInchToTwips(rect.Top)[/SIZE]
[SIZE=2]rc.bottom = HundredthInchToTwips(rect.Bottom)[/SIZE]
[SIZE=2]rc.left = HundredthInchToTwips(rect.Left)[/SIZE]
[SIZE=2]rc.right = HundredthInchToTwips(rect.Right)[/SIZE]
[SIZE=2][COLOR=#008000]' Specify the page area[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] rcPage [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] STRUCT_RECT[/SIZE]
[SIZE=2]rcPage.top = HundredthInchToTwips(rect.Top)[/SIZE]
[SIZE=2]rcPage.bottom = HundredthInchToTwips(rect.Bottom)[/SIZE]
[SIZE=2]rcPage.left = HundredthInchToTwips(rect.Left)[/SIZE]
[SIZE=2]rcPage.right = HundredthInchToTwips(rect.Right)[/SIZE]
[SIZE=2][COLOR=#008000]' Get device context of output device[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] hdc [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] IntPtr[/SIZE]
[SIZE=2]hdc = e.GetHdc()[/SIZE]
[SIZE=2][COLOR=#008000]' Fill in the FORMATRANGE structure[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] fr [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] STRUCT_FORMATRANGE[/SIZE]
[SIZE=2]fr.chrg = cr[/SIZE]
[SIZE=2]fr.hdc = hdc[/SIZE]
[SIZE=2]fr.hdcTarget = hdc[/SIZE]
[SIZE=2]fr.rc = rc[/SIZE]
[SIZE=2]fr.rcPage = rcPage[/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] lParam [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] IntPtr[/SIZE]
[SIZE=2]lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(fr))[/SIZE]
[SIZE=2]Marshal.StructureToPtr(fr, lParam, [/SIZE][SIZE=2][COLOR=#0000ff]False[/COLOR][/SIZE][SIZE=2])[/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] res [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Integer[/COLOR][/SIZE]
[SIZE=2]res = SendMessage(Handle, EM_FORMATRANGE, 1, lParam)[/SIZE]
[SIZE=2]Marshal.FreeCoTaskMem(lParam)[/SIZE]
[SIZE=2]e.ReleaseHdc(hdc)[/SIZE]
[SIZE=2][COLOR=#0000ff]Return[/COLOR][/SIZE][SIZE=2] res[/SIZE]
[SIZE=2][COLOR=#0000ff]End[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Function[/COLOR][/SIZE]
 
I think this is what you ask for; gets a snapshot of any control/container or full form as viewed on screen: http://www.vbdotnetforums.com/showthread.php?t=14626

What you attempted with FormatRange does work for me also, FR doesn't send a message to printer, it only works with the bounds and graphics provided by the parameters. Have you debugged the rectangle values compared to those relevant to your graphics object?
 
I can see that the code in the link you posted will work, but to be honest I was trying to avoid creating bitmap images since they never look quite right, especially when the zoom is high.

I did more work on this and I think I can see what is happening now (but I have no idea how to fix it!). I use standard printing methods, this includes some subs which scale and translate the canvas (the e.graphics) for fit to page, centering, margins etc. I then call my sub which draws the form onto the print surface. This all works fine, but... the formatted text of the RTB is offset.

I think what is happening is that the output from the FormatRange (the rendered RTB text) is not taking any notice of the scaling and translations I mentioned that have been applied to the print surface.

So if I print with no margins set, no centering, no fit to page, the RTB text aligns correctly. So I seem to be quite close, but Im well past the limits of my VB knowledge.

Can you see why this is happening? It seems as though the FormatRange is assuming it can print the RTB wherever it likes on the surface.
 
You are partly right, tranformations of the Graphics object does not apply to em_formatrange message, it only takes the handle to the device context (=virtual drawing 'surface') that was created by the Graphics object and draws to the rectangle specified. Transformations only apply when drawing through the methods of that Graphics instance. Formatrange will draw to the exact rectangle specified (rc), the full rcPage looks like it ignores.

Another thing when doing it through the print method is that you get white backcolor. If you do formatrange on a graphics object created from a bitmap you get the actual backcolor displayed in richtextbox. Here is a basic example doing that:
VB.NET:
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] bmp [/SIZE][SIZE=2][COLOR=#0000ff]As [/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Bitmap(400, 400)[/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] g [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Graphics = Graphics.FromImage(bmp)[/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] rctdraw [/SIZE][SIZE=2][COLOR=#0000ff]As [/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Rectangle(50, 50, 150, 150)[/SIZE]
[SIZE=2]RTBx.FormatRange(g, rctdraw, 0, RTBx.TextLength)[/SIZE]
[SIZE=2]RTBx.FormatRangeDone()[/SIZE]
[SIZE=2]bmp.Save(IO.Path.Combine(Application.StartupPath, [/SIZE][SIZE=2][COLOR=#800000]"PrintImage.jpg"[/COLOR][/SIZE][SIZE=2]), Imaging.ImageFormat.Jpeg)[/SIZE]
[SIZE=2]g.Dispose()[/SIZE]
If the text is less than the box you also have to fill the rectangle first with backcolor, because formatrange only concerns with drawing the text, not filling the rectangle necessarily. The bitmap produced by this can be drawn when printing with e.graphics.drawimage().
The formatrange signature used above is overloaded from the original example, here is the full method:
VB.NET:
Public Function FormatRange(ByVal g As Graphics, ByVal drawrect As Rectangle, _
                            ByVal charFrom As Integer, ByVal charTo As Integer) As Integer
    ' Specify which characters to print
    Dim cr As STRUCT_CHARRANGE
    cr.cpMin = charFrom
    cr.cpMax = charTo
    ' Specify the area inside page margins
    Dim rc As STRUCT_RECT
    rc.top = HundredthInchToTwips(drawrect.Top)
    rc.bottom = HundredthInchToTwips(drawrect.Bottom)
    rc.left = HundredthInchToTwips(drawrect.Left)
    rc.right = HundredthInchToTwips(drawrect.Right)
    ' Get device context of output device
    Dim hdc As IntPtr = g.GetHdc()
    ' Fill in the FORMATRANGE structure
    Dim fr As STRUCT_FORMATRANGE
    fr.chrg = cr
    fr.hdc = hdc
    fr.hdcTarget = hdc
    fr.rc = rc
    fr.rcPage = rc
    Dim lParam As IntPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(fr))
    Marshal.StructureToPtr(fr, lParam, False)
    Dim res As Integer = SendMessage(Handle, EM_FORMATRANGE, 1, lParam)
    Marshal.FreeCoTaskMem(lParam)
    g.ReleaseHdc(hdc)
    Return res
End Function

The getsnap() method I linked to in previous post is still much easier to handle, you can set the graphics quality also. And as said, individual controls drawn to images can also be drawn to the final print.
 
Thanks for looking into this. It seems that generating some kind of image is my only choice.

But for getsnap to work, dont the RTB's need to be fully visible? So I wouldnt be able to print an RTB that was partially off screen or one that was partly hidden because it was placed on a scrollable control?
 
That is correct. If this is an issue you should go for formatrange to bitmaps that you can use to DrawImage with the transformed graphics instance.
 
ok (with a ton of help from you) and an entire day of faffing about, I've finally cracked it!

I used the FormatRange as you showed it earlier and changed by 'DrawForm' sub to include this fragment;

VB.NET:
[SIZE=2][COLOR=#0000ff][SIZE=2][COLOR=#0000ff]For[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Each[/COLOR][/SIZE][SIZE=2] ctl [/SIZE][SIZE=2][COLOR=#0000ff]In[/COLOR][/SIZE][SIZE=2] Con.Controls[/SIZE]
 
Dim[/COLOR][/SIZE][SIZE=2] CtlRect [/SIZE][SIZE=2][COLOR=#0000ff]As [/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Rectangle[/SIZE]
[SIZE=2]CtlRect = [/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Rectangle( _[/SIZE]
[SIZE=2]Panel.PointToClient(ctl.Parent.PointToScreen(ctl.Location)).X, _[/SIZE]
[SIZE=2]Panel.PointToClient(ctl.Parent.PointToScreen(ctl.Location)).Y, _[/SIZE]
[SIZE=2]ctl.Width, ctl.Height)[/SIZE]
 
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] IsRichTextBoxEx [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]Boolean[/COLOR][/SIZE][SIZE=2] = ctl.GetType.ToString.ToLower = [/SIZE][SIZE=2][COLOR=#800000]"windowsapplication1.richtextboxex"[/COLOR][/SIZE]
 
[SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE][SIZE=2] IsRichTextBoxEx [/SIZE][SIZE=2][COLOR=#0000ff]And[/COLOR][/SIZE][SIZE=2] ctl.Visible [/SIZE][SIZE=2][COLOR=#0000ff]Then[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] RTB [/SIZE][SIZE=2][COLOR=#0000ff]As [/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] RichTextBoxEx[/SIZE]
[SIZE=2]RTB = ctl[/SIZE]
[SIZE=2][COLOR=#008000]'BackColor[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] ctlBackBrush [/SIZE][SIZE=2][COLOR=#0000ff]As [/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] SolidBrush(SetColor(RTB.BackColor))[/SIZE]
[SIZE=2]g.FillRectangle(ctlBackBrush, CtlRect)[/SIZE]
[SIZE=2][COLOR=#008000]'Text[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] bmp [/SIZE][SIZE=2][COLOR=#0000ff]As [/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Bitmap(RTB.ClientRectangle.Width, RTB.ClientRectangle.Height)[/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] k [/SIZE][SIZE=2][COLOR=#0000ff]As[/COLOR][/SIZE][SIZE=2] Graphics = Graphics.FromImage(bmp)[/SIZE]
[SIZE=2]k.FillRectangle(ctlBackBrush, RTB.ClientRectangle)[/SIZE]
[SIZE=2][COLOR=#008000]'Offset text X axis by 1. God knows why but it then looks correct[/COLOR][/SIZE]
[SIZE=2][COLOR=#0000ff]Dim[/COLOR][/SIZE][SIZE=2] TRect [/SIZE][SIZE=2][COLOR=#0000ff]As [/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]New[/COLOR][/SIZE][SIZE=2] Rectangle(RTB.ClientRectangle.Left + 1, RTB.ClientRectangle.Top, CtlRect.Width, CtlRect.Height)[/SIZE]
[SIZE=2]RTB.FormatRange(k, TRect, 0, RTB.TextLength)[/SIZE]
[SIZE=2]RTB.FormatRangeDone()[/SIZE]
[SIZE=2]g.DrawImage(bmp, CtlRect)[/SIZE]
[SIZE=2][COLOR=#008000]'Clean up[/COLOR][/SIZE]
[SIZE=2]k.Dispose()[/SIZE]
[SIZE=2]bmp.Dispose()[/SIZE]
[SIZE=2][COLOR=#008000]'Outline[/COLOR][/SIZE]
[SIZE=2]g.DrawRectangle(Pens.Black, CtlRect)[/SIZE]
[SIZE=2][COLOR=#0000ff]End[/COLOR][/SIZE][SIZE=2][COLOR=#0000ff]If[/COLOR][/SIZE]
 
Next

Which works great. The only odd thing is that I need to offset the text rectangle by 1, otherwise the first letter of the text is coincident with the outline. I guess the outline is 1 so maybe that makes sense anyway.

In googling on this subject, Ive seen a lot of people looking for a way of doing this, so I hope we have helped out a lot of others too.
 
Dim CtlRect As New Rectangle
CtlRect = New Rectangle( _
Panel.PointToClient(ctl.Parent.PointToScreen(ctl.Location)).X, _
Panel.PointToClient(ctl.Parent.PointToScreen(ctl.Location)).Y, _
ctl.Width, ctl.Height)



You can simply the above bit in the interests of decreased verbosity...


VB.NET:
Dim CtlRect As New Rectangle( _
Panel.PointToClient(ctl.Parent.PointToScreen(ctl.Location)), ctl.Size)
 
Back
Top