Introduction
Windowless controls have been around since the dawn of ActiveX, and are very useful when you do not want (or cannot have) a separate HWND
for every control element in your user interface. This article presents the class CRichDrawText
, a simple wrapper around the windowless version of the RichEdit control, allowing formatted text to be sized and drawn on a device context.
Background
As part of an application I was working on (the Windows version of Inform 7), I had a need to draw sections of formatted text in a CScrollView
derived class. As the application was already making heavy use of the RichEdit control, that seemed the perfect control to use. Experiments with the FormatRange
and DisplayBand
functions in CRichEditCtrl
were not satisfactory: what I needed was a proper windowless control.
When I actually came to try to use the windowless RichEdit control, however, life became more interesting. The MSDN documentation in this area is very sparse. Most of the useful information I found buried away in an MSDN Knowledge Base article, Q270161, with the remainder determined by trial and error. Hopefully, if you need a windowless RichEdit control, you won't have to do as much searching as I did.
Using the code
To use the class in your code, include RichDrawText.h and declare an instance of the CRichDrawText
class somewhere.
To find out how much vertical space is needed for the contents of the CRichDrawText
, call SizeText
, passing in a device context and a rectangle whose width is the width you want the text to be formatted for. To actually draw the text, call DrawText
, passing in a device context and the bounding rectangle.
To actually set up the formatted text, CRichDrawText
provides two methods. SetText
replaces all the text in the windowless control with the given Unicode string, and Range
returns an ITextRange
COM interface pointer for a range of text in the windowless control. ITextRange
is part of the TOM (Text Object Model) which is an under-appreciated part of the RichEdit control, providing faster and more functional text manipulation than is available through the usual RichEdit EM_ messages. A description of the TOM is outside of the scope of this article, but the example program provided with this article, along with the MSDN documentation, should be enough to get you started.
The ITextHost implementation
In the CRichDrawText
constructor, we create a windowless RichEdit control by calling the CreateTextServices
function, to which we have to pass an implementation of the ITextHost
interface. The CRichDrawText
class provides a minimal implementation, which does the least possible to be able to size and draw the rich text. Which methods to implement was determined simply by seeing which were called using the debugger. Most of these methods are straightforward, however:
HRESULT CRichDrawText::XTextHost::TxNotify(DWORD iNotify, void *pv)
{
return S_OK;
}
The
TxNotify
implementation must return
S_OK
, even if it does nothing with the notification messages it is passed. If it returns an error code then the sizing and drawing calls will fail.
Another catch for the unwary is TxGetCharFormat
which must return a pointer to a CHARFORMATW
structure, not a CHARFORMAT
: the Unicode version of the structure must be used, even on Windows 95.
What CreateTextServices returns
Having successfully called CreateTextServices
we now have an IUnknown
COM pointer, which according to MSDN, we can now use QueryInterface
on to get an ITextServices
COM pointer. However, my initial attempts at this all failed: I always got E_NOINTERFACE
back from the QueryInterface
call.
The problem turns out to be that the IID_ITextServices
linked in from riched20.lib
is wrong. Thanks, Microsoft... The code linked from Knowledge Base article Q270161 contains the correct IID:
const IID IID_ITextServices = {
0x8d33f740, 0xcf58, 0x11ce, {0xa8, 0x9d, 0x00, 0xaa, 0x00, 0x6c, 0xad, 0xc5}
};
When we include this in the
CRichDrawText
source file (where it will be linked in preference to the one in
riched20.lib), we are able to get back an
ITextServices
COM pointer.
Calling ITextServices methods
Having implemented ITextHost
and obtained an ITextServices
interface, we are now ready to actually call the methods to size and draw text.
The CRichDrawText::SizeText
method calls ITextServices::TxGetNaturalSize
to calculate the required height. Trial and error has had to be used here to determine what all the arguments should be:
hicTargetDev
and ptd
are both NULL
, even though the documentation does not say that null values are allowed. For drawing onto a display device context, it is not easy to see what else could be passed.
psizelExtent
is a pointer to a SIZEL
structure with both member variables set to -1. MSDN claims that this argument is "currently unused" but this seems not to be the case: passing in NULL
leads to an access violation in riched20.dll. Passing in zeros (or small values) seems to affect the returned sizing rectangle. Given the lack of documentation, using {-1,-1}
seems a reasonable approach.
The CRichDrawText::DrawText
method calls ITextServices::TxDraw
to draw the rich text. The arguments to this method have proved slightly less problematic. However:
pvAspect
is NULL
, though the documentation does not say if this is allowed. Given that the argument's type is void*
and the documentation for it says "pointer for information for drawing optimizations", this might be worthy of some sort of award for unhelpful documentation.
hicTargetDev
and ptd
are again both NULL
.
lViewId
is zero. MSDN claims that this argument is not used, but in the Platform SDK TextServ.h defines enum TXTVIEW
as "useful values for TxDraw lViewId parameter".
Conclusion
With poorly documented interfaces, the hardest problem is usually getting any of it to work in the first place. I hope that this article will give anyone playing with the windowless RichEdit control enough of a start that they can make it do what they need. Anyway, if it was easy, what would be the fun in that?