GDI+ does not support rendering of Rich Text and this has always represented a challenge for developers. The approach described in this article offers a solution to such limitation by tapping into the power of API hooking.
Introduction
Like many others, I've surfed the web for countless hours to find a way to render Rich Text with GDI+ without having to roll out my own RTF parser and a (fast) word-break algorithm. I've never come across a satisfying solution so I'm posting this in the hope that it will help you save same time.
Background
The typical scenario for which this project is designed is a Windows application that features a layered image editor.
Users can create a composite image, made up of pictures and text, and the editor features an RTF control that enables you to enter and edit RichText
which is then rendered with antialias via GDI+ on a 32bpp transparent image, that will represent one of the layers in the composite image.
The Antialias Issue
As you may know, there's a quick way to render Rich Text from a RichEdit control to a DC by means of the EM_FORMATRANGE message sent to the control via SendMessage. This approach is great and it's the perfect solution when all you need is to render text on a static background.
But what if you want to render your Rich Text to a transparent background?
Then all hell breaks loose since the approach described above won't work.
Standard GDI methods that render text with antialias are designed to process alpha-blending based on a background color. If you want to render on a transparent image, antialias gets lost leaving you with terrible results.
Creating a Transparent RichEdit Control
The first step for our composite editor is to implement our own RichEdit
control by inheriting from the RichTextBox
class. This will serve as the GUI input element through which our users will enter and edit Rich Text, directly on the composite image we are editing:
Friend Class TransRtb
Inherits RichTextBox
There's a simple way to create a transparent RichEdit
control (RichTextBox
in .NET), as seen posted here. All you need to do is set the WS_EX_TRANSPARENT style upon window creation and set ControlStyle
flags accordingly:
Public Sub New()
Me.SetStyle(ControlStyles.AllPaintingInWmPaint, True)
Me.SetStyle(ControlStyles.Opaque, True)
Me.SetStyle(ControlStyles.SupportsTransparentBackColor, True)
Me.SetStyle(ControlStyles.EnableNotifyMessage, True)
MyBase.BackColor = Color.Transparent
Me.ScrollBars = RichTextBoxScrollBars.None
End Sub
Protected Overrides ReadOnly Property CreateParams() _
As System.Windows.Forms.CreateParams
Get
Dim cp As CreateParams = MyBase.CreateParams
cp.ExStyle = cp.ExStyle Or SafeNativeMethods.WS_EX_TRANSPARENT
Return cp
End Get
End Property
This alone is a great solution for anyone who simply needs to give the user the ability to input Rich Text on top of images.
It will render text with antialias and with a bit of work, you could roll out your own moving and resizing methods.
See my Tip/Trick Resize and Rotate Shapes in GDI+. Keep in mind that you wouldn't be able to rotate a Windows control though.
Define a Strategy for GDI+ Rendering
Now, if you need to actually have Rich Text rendered on a transparent bitmap with antialias, things get tricky.
You're basically left with a couple of options:
- Write your own RTF parser and word-wrap algorithm (good luck with that).
- Keep reading this article.
I've personally walked the path of option 1 on a project I've been working on for the past 15 years and I'm still fixing bugs.
The idea here is simple: we already have a control that does it all: it measures string
s and defines coordinates, positions and renders the text. Too bad we can't tap into the power of the existing control to get a hold of that information and use it to render the text ourselves via GDI+.
Or can we?
Spy, My Name is Spy++
In Windows, all flavors of a RichEdit
control are essentially wrappers of one of the RichEditxx.dlls. By firing up Spy++ and focusing on the gdi32.dll calls, it turns out that all text bits are rendered with three API calls:
- ExtTextOutW for text and glyph rendering
- ExtTextOutA for text background rendering
- PatBlt for underline and strikethrough
By intercepting these calls, one can gather all data necessary to render the text in any other way.
All we need now is a way to intercept these calls. It turns out there's a great open source library out there called EasyHook that will enable you to do just that, without having to write your own API hooking/hijacking methods, a rather complex and delicate task.
Let's Hook
You will need to include three DLLs to your project:
- EasyHook.dll
- EasyHook32.dll and
- EasyHook64.dll
Your project will then need to reference the first one, EasyHook.dll.
In our implementation of the RichTextBox
control, we will setup hooking of these three APIs by creating a delegate
for each one of the APIs we want to intercept:
Private m_DelMyTextOutW As DelExtTextOutW
Private m_DelMyTextOutA As DelExtTextOutA
Private m_DelMyPatBlt As DelPatBlt
Private m_HkMyTextOutW As LocalHook
Private m_HkMyTextOutA As LocalHook
Private m_HkMyPatBlt As LocalHook
Private Delegate Function DelExtTextOutW(ByVal hdc As IntPtr, ByVal x As Int32, _
ByVal y As Int32, ByVal wOptions As Int32, ByVal lpRect As IntPtr, _
ByVal lpString As IntPtr, ByVal nCount As Int32, ByVal lpDx As IntPtr) As Int32
Private Delegate Function DelExtTextOutA(ByVal hdc As IntPtr, ByVal x As Int32, _
ByVal y As Int32, ByVal wOptions As Int32, ByVal lpRect As IntPtr, _
ByVal lpString As IntPtr, ByVal nCount As Int32, ByVal lpDx As IntPtr) As Int32
Private Delegate Function DelPatBlt(ByVal hdc As IntPtr, ByVal x As Int32, _
ByVal y As Int32, ByVal nWidth As Int32, ByVal nHeight As Int32, _
ByVal dwRop As Int32) As Int32
We'll add an on/off method to start and stop hooking (please see the attached project for API declarations):
Private Sub HookGdi(State As Boolean)
Try
If State Then
m_DelMyTextOutW = AddressOf MyExtTextOutW
m_DelMyTextOutA = AddressOf MyExtTextOutA
m_DelMyPatBlt = AddressOf MyPatBlt
If m_HkMyTextOutW Is Nothing Then
Dim iPtrAddress As IntPtr = _
LocalHook.GetProcAddress("gdi32.dll", "ExtTextOutW")
m_HkMyTextOutW = LocalHook.Create(iPtrAddress, m_DelMyTextOutW, Me)
m_HkMyTextOutW.ThreadACL.SetExclusiveACL(New Int32() {1})
End If
If m_HkMyTextOutA Is Nothing Then
Dim iPtrAddress As IntPtr = _
LocalHook.GetProcAddress("gdi32.dll", "ExtTextOutA")
m_HkMyTextOutA = LocalHook.Create(iPtrAddress, m_DelMyTextOutA, Me)
m_HkMyTextOutA.ThreadACL.SetExclusiveACL(New Int32() {1})
End If
If m_HkMyPatBlt Is Nothing Then
Dim iPtrAddress As IntPtr = _
LocalHook.GetProcAddress("gdi32.dll", "PatBlt")
m_HkMyPatBlt = LocalHook.Create(iPtrAddress, m_DelMyPatBlt, Me)
m_HkMyPatBlt.ThreadACL.SetExclusiveACL(New Int32() {1})
End If
Else
If m_HkMyTextOutW IsNot Nothing Then
m_HkMyTextOutW.Dispose()
m_HkMyTextOutW = Nothing
End If
If m_HkMyTextOutA IsNot Nothing Then
m_HkMyTextOutA.Dispose()
m_HkMyTextOutA = Nothing
End If
If m_HkMyPatBlt IsNot Nothing Then
m_HkMyPatBlt.Dispose()
m_HkMyPatBlt = Nothing
End If
LocalHook.Release()
End If
Catch ex As Exception
End Try
End Sub
API hooking must start and end when our control is created and destroyed:
Private Sub TransRtb_HandleCreated(sender As Object, e As EventArgs) _
Handles Me.HandleCreated
HookGdi(True)
End Sub
Private Sub TransRtb_HandleDestroyed(sender As Object, e As EventArgs) _
Handles Me.HandleDestroyed
HookGdi(False)
End Sub
Now all calls to these three APIs will first go through our own methods. For example, for the ExtTextOutW
, the intercepting method will look like this:
Private Function MyExtTextOutW(ByVal hdc As IntPtr, _
ByVal x As Int32, _
ByVal y As Int32, _
ByVal wOptions As Int32, _
ByVal lpRect As IntPtr, _
ByVal lpString As IntPtr, _
ByVal nCount As Int32, _
ByVal lpDx As IntPtr) As Int32
Try
If m_hDc = hdc Then
pCatchText(hdc, x, y, wOptions, lpRect, lpString, nCount, True)
Return 0
Else
Return SafeNativeMethods.ExtTextOutW_
(hdc, x, y, wOptions, lpRect, lpString, nCount, lpDx)
End If
Catch ex As Exception
Return 0
End Try
End Function
The pCatchText
method is where all data is gathered. It's stored in a temporary array of structures that contains all the fields necessary to properly render the text:
Private Sub pCatchText(ByVal hdc As IntPtr, _
ByVal x As Int32, _
ByVal y As Int32, _
ByVal wOptions As Int32, _
ByVal lpRect As IntPtr, _
ByVal lpString As IntPtr, _
ByVal nCount As Int32, _
ByVal bUnicode As Boolean)
Dim sText As String = String.Empty
If (wOptions And SafeNativeMethods.ETO_GLYPH_INDEX) = False Then
If bUnicode Then
sText = String.Empty & Marshal.PtrToStringUni(lpString)
Else
sText = String.Empty & Marshal.PtrToStringAnsi(lpString)
End If
sText = sText.Substring(0, nCount)
If (sText = " ") AndAlso _
(sText <> m_sText.Substring(m_iLastStart, nCount)) Then
Return
End If
End If
Dim iNewSize As Integer = 0
If m_atLastTextOut Is Nothing Then
ReDim m_atLastTextOut(iNewSize)
Else
iNewSize = m_atLastTextOut.Length
ReDim Preserve m_atLastTextOut(iNewSize)
End If
With m_atLastTextOut(iNewSize)
.SourceX = x
Dim iBackCol As Int32 = SafeNativeMethods.GetBkColor(hdc)
If wOptions And SafeNativeMethods.ETO_OPAQUE Then
.BackColor = ColorTranslator.FromOle(iBackCol)
Else
.BackColor = Color.Transparent
End If
Dim iTextCol As Int32 = SafeNativeMethods.GetTextColor(hdc)
.TextColor = ColorTranslator.FromOle(iTextCol)
Dim hFnt As IntPtr = SafeNativeMethods.GetCurrentObject_
(hdc, SafeNativeMethods.OBJ_FONT)
.Font = System.Drawing.Font.FromHfont(hFnt)
.IsRTL = ((SafeNativeMethods.GetTextAlign(hdc) _
And SafeNativeMethods.TA_UPDATECP) = SafeNativeMethods.TA_UPDATECP)
If wOptions And SafeNativeMethods.ETO_GLYPH_INDEX Then
.Text = m_sText.Substring(m_iLastStart, nCount)
Else
.Text = sText
End If
m_iLastStart += nCount
.Location = New Point(x, y)
If lpRect.ToInt32 <> 0 Then
Dim tRc As SafeNativeMethods.RECT = _
Marshal.PtrToStructure(lpRect, New SafeNativeMethods.RECT().GetType)
.LineTop = tRc.Top
.LineBottom = tRc.Bottom
End If
End With
End Sub
We now have all the information needed to render the Rich Text contained in the control via GDI+.
Transparent Image Please
Our control will extend the RichTextBox
by adding an Image property which returns a 32bpp transparent bitmap (of the same size of the control), containing the text found in the control when the Image
method is called.
The concept is simple:
- Create a GDI+ Graphics, get the associated DC and call the
EM_FORMATRANGE
message. - While GDI processing takes place, we gather all the information we need to render the text ourselves, and by eating the calls, we'll prevent GDI rendering to actually take place.
- When
SendMessage
returns, GDI processing is done and we are now ready to render text with GDI+.
Public ReadOnly Property Image() As Image
Get
Dim oOut As Bitmap = Nothing
Dim oGfx As Graphics = Nothing
Try
oOut = New Bitmap(MyBase.ClientSize.Width, MyBase.ClientSize.Height)
oGfx = Graphics.FromImage(oOut)
oGfx.PageUnit = GraphicsUnit.Pixel
oGfx.SmoothingMode = SmoothingMode.None
oGfx.TextRenderingHint = m_eAntiAlias
oGfx.TextContrast = m_iContrast
Dim snInchX As Single = 1440 / oGfx.DpiX
Dim snInchY As Single = 1440 / oGfx.DpiY
Dim rectLayoutArea As SafeNativeMethods.RECT
rectLayoutArea.Right = CInt(oOut.Width * snInchX)
rectLayoutArea.Bottom = CInt(oOut.Height * _
snInchY * 2)
Dim fmtRange As SafeNativeMethods.FORMATRANGE
fmtRange.chrg.cpMax = -1
fmtRange.chrg.cpMin = 0
m_hDc = oGfx.GetHdc
fmtRange.hdc = m_hDc
fmtRange.hdcTarget = m_hDc
fmtRange.rc = rectLayoutArea
fmtRange.rcPage = rectLayoutArea
Dim wParam As New IntPtr(1)
Dim lParam As IntPtr = _
Marshal.AllocCoTaskMem(Marshal.SizeOf(fmtRange))
Marshal.StructureToPtr(fmtRange, lParam, False)
Erase m_atLastTextOut
m_sText = MyBase.Text.Replace(vbLf, String.Empty)
m_iLastStart = 0
SafeNativeMethods.SendMessage(MyBase.Handle, _
SafeNativeMethods.EM_FORMATRANGE, _
wParam, lParam)
oGfx.ReleaseHdc()
m_hDc = IntPtr.Zero
If m_atLastTextOut.Length = 0 Then Return oOut
For iItm As Integer = m_atLastTextOut.GetLowerBound(0) _
To m_atLastTextOut.GetUpperBound(0)
Dim tData As MAYATEXTOUT = m_atLastTextOut(iItm)
If (tData.Text IsNot Nothing) _
AndAlso (tData.Text.Length > 0) Then
Using oStrFormat As StringFormat = _
StringFormat.GenericTypographic
oStrFormat.FormatFlags = oStrFormat.FormatFlags Or _
StringFormatFlags.MeasureTrailingSpaces Or _
StringFormatFlags.NoWrap
If tData.IsRTL Then
oStrFormat.FormatFlags = oStrFormat.FormatFlags _
Or StringFormatFlags.DirectionRightToLeft
End If
If tData.Text.Length Then
Dim aoRng(0) As CharacterRange
aoRng(0).First = 0
aoRng(0).Length = tData.Text.Length
oStrFormat.SetMeasurableCharacterRanges(aoRng)
Dim oaRgn() As Region = oGfx.MeasureCharacterRanges_
(tData.Text, tData.Font, _
New RectangleF(0, 0, oOut.Width * 2, _
oOut.Height * 2), oStrFormat)
tData.Bounds = oaRgn(0).GetBounds(oGfx)
oaRgn(0).Dispose()
oaRgn(0) = Nothing
End If
tData.Destination = New RectangleF(tData.Location.X + _
tData.Bounds.Left, tData.Location.Y, _
tData.Bounds.Width, tData.Bounds.Height)
Dim tRcBack As New RectangleF(tData.Destination.Left + _
tData.Bounds.Left, tData.LineTop, _
tData.Destination.Width, _
tData.LineBottom - tData.LineTop)
If tData.BackColor.ToArgb <> Color.Transparent.ToArgb Then
Using oBr As New SolidBrush(tData.BackColor)
oGfx.FillRectangle(oBr, tRcBack)
End Using
End If
Using oBr As New SolidBrush(tData.TextColor)
oGfx.DrawString(tData.Text, tData.Font, oBr, _
New Point(tData.Destination.Left, _
tData.Destination.Top), oStrFormat)
If tData.Line1Height Then
oGfx.FillRectangle(oBr, New RectangleF_
(tRcBack.Left, tData.Line1Top, _
tRcBack.Width, tData.Line1Height))
End If
If tData.Line2Height Then
oGfx.FillRectangle(oBr, New RectangleF_
(tRcBack.Left, tData.Line2Top, _
tRcBack.Width, tData.Line2Height))
End If
End Using
End Using
End If
If tData.Font IsNot Nothing Then
tData.Font.Dispose()
tData.Font = Nothing
End If
Next iItm
Catch ex As Exception
Finally
If oGfx IsNot Nothing Then
oGfx.Dispose()
oGfx = Nothing
End If
End Try
Return oOut
End Get
End Property
GDI+ vs GDI
This is a battle as old as GDI+, which has a completely different text rendering engine. As you will see when you run the project, by double-clicking the text, the TransRtb
control becomes visible. By clicking outside its area, it disappears, displaying the text rendered with GDI+. And a few differences become evident: text length, antialias, character spacing, and so on.
You will have to figure out yourself the best way to tweak your GDI+ Graphics
object to obtain the solution that fits your needs.
Extending the Control
The TransRtb
control implements a few properties to get/set text style and effects on the current selection that seem to be missing from the original implementation.
It also exposes the Antialias
and Contrast
properties to get/set the way text is rendered in GDI+.
As this is only an example aimed at illustrating this technique, much more can be implemented. The RichEdit
control also supports images, superscript and subscript text effects which are not dealt with in this project.
Please drop a line if you add to the control, if you find any bugs or ways to make it better.
History
- 9th September, 2018: First release