Contents
Introduction
I needed a rich edit control that could display bitmaps. Searching around the Web, I could see that others have faced the same need. For me, I wanted a "light-weight" Help functionality that I could embed as an RTF resource and display from my application, so that I did not need a full-blown Help companion. And what good is a Help file without screenshots of your application -- hence the need for bitmaps in a rich edit control.
That same searching around the Web also convinced me that the solution was not easy to come by. By chance, I found the hint that made it all work, in a comment to an article (i.e., not the article itself) posted by Stephane Lesage here which he titled "Getting Images with a StreamIn/ClipBoard/Drag'n'Drop operation":
Quote from Mr. Lesage:
"If you want object insertion operations to work in your RichEdit control, you have to supply an IRichEditOleCallback
interface and implement the GetNewStorage
method."
OLE was the needed hint, and informed with code suggested by Mr. Lesage, I was able to write the COleRichEditCtrl
class, which is derived from MFC's CRichEditCtrl
class.
top
The COleRichEditCtrl Class
The code for the class is actually straightforward. Inside the header, I defined a nested class (actually, it's not really a class
; it's an interface
) derived from IRichEditOleCallback
, which is documented at MSDN. The most significant method implemented in this nested class is GetNewStorage
which provides storage for a new object pasted from the clipboard or read in from an RTF stream. Here's the header for class COleRichEditCtrl
, with most of the ClassWizard boilerplate deleted for simplification:
#include <richole.h>
class COleRichEditCtrl : public CRichEditCtrl
{
public:
COleRichEditCtrl();
virtual ~COleRichEditCtrl();
long StreamInFromResource(int iRes, LPCTSTR sType);
protected:
static DWORD CALLBACK readFunction(DWORD dwCookie,
LPBYTE lpBuf,
LONG nCount,
LONG* nRead);
interface IExRichEditOleCallback;
IExRichEditOleCallback* m_pIRichEditOleCallback;
BOOL m_bCallbackSet;
interface IExRichEditOleCallback : public IRichEditOleCallback
{
public:
IExRichEditOleCallback();
virtual ~IExRichEditOleCallback();
int m_iNumStorages;
IStorage* pStorage;
DWORD m_dwRef;
virtual HRESULT STDMETHODCALLTYPE GetNewStorage(LPSTORAGE* lplpstg);
virtual HRESULT STDMETHODCALLTYPE
QueryInterface(REFIID iid, void ** ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef();
virtual ULONG STDMETHODCALLTYPE Release();
virtual HRESULT STDMETHODCALLTYPE
GetInPlaceContext(LPOLEINPLACEFRAME FAR *lplpFrame,
LPOLEINPLACEUIWINDOW FAR *lplpDoc,
LPOLEINPLACEFRAMEINFO lpFrameInfo);
virtual HRESULT STDMETHODCALLTYPE ShowContainerUI(BOOL fShow);
virtual HRESULT STDMETHODCALLTYPE
QueryInsertObject(LPCLSID lpclsid, LPSTORAGE lpstg, LONG cp);
virtual HRESULT STDMETHODCALLTYPE DeleteObject(LPOLEOBJECT lpoleobj);
virtual HRESULT STDMETHODCALLTYPE
QueryAcceptData(LPDATAOBJECT lpdataobj, CLIPFORMAT FAR *lpcfFormat,
DWORD reco, BOOL fReally, HGLOBAL hMetaPict);
virtual HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode);
virtual HRESULT STDMETHODCALLTYPE
GetClipboardData(CHARRANGE FAR *lpchrg,
DWORD reco, LPDATAOBJECT FAR *lplpdataobj);
virtual HRESULT STDMETHODCALLTYPE
GetDragDropEffect(BOOL fDrag,
DWORD grfKeyState, LPDWORD pdwEffect);
virtual HRESULT STDMETHODCALLTYPE
GetContextMenu(WORD seltyp, LPOLEOBJECT lpoleobj,
CHARRANGE FAR *lpchrg, HMENU FAR *lphmenu);
};
public:
protected:
virtual void PreSubclassWindow();
public:
protected:
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
DECLARE_MESSAGE_MAP()
};
An IExRichEditOleCallback
object is allocated from the heap in the OnCreate
handler for the class (not precisely: see below where I describe a problem encountered during development). Implementation of its GetNewStorage
method followed a few examples I found elsewhere, and really is a textbook use of various APIs specifically designed for the task:
HRESULT STDMETHODCALLTYPE
COleRichEditCtrl::IExRichEditOleCallback::GetNewStorage(LPSTORAGE* lplpstg)
{
m_iNumStorages++;
WCHAR tName[50];
swprintf(tName, L"REOLEStorage%d", m_iNumStorages);
HRESULT hResult = pStorage->CreateStorage(tName,
STGM_TRANSACTED | STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE ,
0, 0, lplpstg );
if (hResult != S_OK )
{
::AfxThrowOleException( hResult );
}
return hResult;
}
Finally, since my purpose was to use the class by streaming in an RTF stream stored as a resource in the executable, I provided a StreamInFromResource
member function, together with a statically-scoped callback function used in the EDITSTREAM
structure:
long COleRichEditCtrl::StreamInFromResource(int iRes, LPCTSTR sType)
{
HINSTANCE hInst = AfxGetInstanceHandle();
HRSRC hRsrc = ::FindResource(hInst,
MAKEINTRESOURCE(iRes), sType);
DWORD len = SizeofResource(hInst, hRsrc);
BYTE* lpRsrc = (BYTE*)LoadResource(hInst, hRsrc);
ASSERT(lpRsrc);
CMemFile mfile;
mfile.Attach(lpRsrc, len);
EDITSTREAM es;
es.pfnCallback = readFunction;
es.dwError = 0;
es.dwCookie = (DWORD) &mfile;
return StreamIn( SF_RTF, es );
}
DWORD CALLBACK COleRichEditCtrl::readFunction(DWORD dwCookie,
LPBYTE lpBuf,
LONG nCount,
LONG* nRead)
{
CFile* fp = (CFile *)dwCookie;
*nRead = fp->Read(lpBuf,nCount);
return 0;
}
One amazing (to me) benefit from this architecture is that I got a tremendous "bang for the buck". When I set out, my only goal was to display a bitmap, yet I ended up with a control that could display any OLE object. Compound documents that contained completely arbitrary objects work just fine: bitmaps, video and audio clips, Office documents (Word, Excel, PowerPoint), and the like. Any other content could also be contained (like PDF files and HTML files), and the content would be launched by double-clicking on the content's icon, but there will not be an in-place display of those objects unless the OLE server application were written and configured for in-place OLE display.
top
The Demo Program
The demo program is a pathetic shell of an MFC SDI doc/view application, whose only purpose in life is to launch a dialog that contains a COleRichEditCtrl
. Under the assumption that the dialog contains Help-type information, a left-click will launch the dialog modelessly, so that the user can still interact with the main application. The code for the dialog uses a simple technique involving a BOOL
flag that's tested in OnPostNcDestroy
so as to delete the memory allocated for the dialog object; but since that's not the point of this article, you'll need to look at the code for the CRichEditHelpDialog
class if you're interested. A right-click will launch the dialog in the more-familiar modal mode, so that you can see the difference.
Run the program and click anywhere in the view to launch the Help dialog with its COleRichEditCtrl
. The contents of the control are streamed in from an RTF resource that's part of the executable; the contents include standard RTF text, a bitmap, a video clip, an Excel spreadsheet, and a PowerPoint presentation. If you can't see one or more of these objects, then you probably do not have the associated program installed.
top
A Problem During The Development of the COleRichEditCtrl Class
I thought I was finished with the development of the COleRichEditCtrl
class, and I was nearly finished writing this article, when I ran into a stumbling block that sent me back to the coding table. You can skip this part if you want, and proceed directly to using the class in your project, by clicking here.
The problem is rooted in the fact that the class wraps a control. Because it's a control, the class must expect that its Windows window will be created in either of two different ways: by an explicit call to ::CreateWindow
(or ::CreateWindowEx
, both of which are wrapped by the CWnd::Create
member of all CWnd
-derived classes), or automatically by Windows during a call to ::CreateDialogParam
which builds a dialog based on a template defined in the program's resources. The latter is more common (and in fact is the way that the next section describes the use of the class), but the former is also used often (and in fact is the method used in the demo project).
To implement OLE functionality, the control needs the OLE callback function very early on, before almost anything else was done. When I initially wrote the wrapper class, I therefore put the call to SetOLECallback
in the COleRichEditCtrl::OnCreate
handler, since this handler is called in response to one of the very-first messages sent by Windows to the control. That was a silly oversight, and I should have known better, since it only handles the first method of window creation (i.e., through ::CreateWindow
) and not the second method (i.e., from a dialog resource template).
The correct way to accommodate both methods of window creation is well-known: put this kind of early initialization in CWnd::PreSubclassWindow
. The PreSubclassWindow
function is a virtual function that's called by the MFC framework just before the Windows window is subclassed to the C++ CWnd
object, and it's called regardless of whether the window is created by the first or second method. Paul Dilascia discusses this in significantly more detail in his C++ Q&A column from the March 2002 issue of MSDN Magazine, see MSDN. So, I moved all the SetOLECallback
code into a new COleRichEditCtrl::PreSubclassWindow
handler.
And this is where I encountered the real problem. For although creation via dialog template now worked great, creation via CreateWindow
did not. The call to SetOLECallback
returned an error code signifying that the call failed, and indeed OLE capabilities were broken. I traced through the code but was unable to find the reason for the failure, and diligent searching of the web turned up nothing.
I figured that the problem was related to a too-soon call to SetOLECallback
, reasoning that when SetOLECallback
was called in PreSubclassWindow
after creation via a dialog template, the Windows window for the rich edit control was ready to accept messages, whereas after creation by CreateWindow
, it might not yet be ready. So I looked for ways to delay the call to SetOLECallback
. I looked for other functions called early by the MFC framework (and found none), and considered installing an OnNcCreate
handler for the WM_NCCREATE
message (which is actually sent before the WM_CREATE
message) or PostMessage'ing my own custom message. None of these really seemed right.
In the end, I added a BOOL
flag to the class to capture the result of the call to SetOLECallback
from PreSubclassWindow
. Then, I maintained the OnCreate
handler, and inside it, I tested the flag to see if SetOLECallback
had successfully been called already from inside PreSubclassWindow
. If not, I simply called it again. Here's the code:
int COleRichEditCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CRichEditCtrl::OnCreate(lpCreateStruct) == -1)
return -1;
ASSERT( m_pIRichEditOleCallback != NULL );
if ( !m_bCallbackSet )
{
SetOLECallback( m_pIRichEditOleCallback );
}
return 0;
}
void COleRichEditCtrl::PreSubclassWindow()
{
CRichEditCtrl::PreSubclassWindow();
m_pIRichEditOleCallback = NULL;
m_pIRichEditOleCallback = new IExRichEditOleCallback;
ASSERT( m_pIRichEditOleCallback != NULL );
m_bCallbackSet = SetOLECallback( m_pIRichEditOleCallback );
}
I'm not totally satisfied with the solution or the explanation of the problem, but it works. If anyone has seen this kind of behavior before, and knows what causes it or has another solution, please let us know.
top
How To Use The COleRichEditCtrl Control In Your Project
These instructions are for use with VC++ version 6.0, but it should be easy to use them with other versions like .NET.
To use the control in your project, download the source and header files (i.e., COleRichEditCtrl.cpp and COleRichEditCtrl.h) to a convenient folder and then include both of them in your project ("Project"->"Add To Project"->"Files...").
Create your dialog resource template and add a standard rich edit control using the control toolbar:
Open the "Properties" window for the just-added rich edit control, and under the "Styles" tab, select "Multi-line", "Vertical scroll" and "Want return", and de-select "Auto H-scroll". These are typical styles for most likely uses of the control, but you might want to play with them if you're not totally pleased with the result. For example, you might also want the "Read-only" style:
Now, we will add a member variable of type COleRichEditCtrl
to your dialog. (See footnote 1.) Open ClassWizard and select the class that corresponds to your dialog. Then, add a "control"-style variable of type CRichEditCtrl
, which is the base class for COleRichEditCtrl
. You should see something like this screenshot:
Click "OK" everywhere to exit out of ClassWizard, and then manually edit your dialog class to substitute the real target class, COleRichEditCtrl
. Here's how.
First, open the header file for your dialog class, and add #include "OleRichEditCtrl.h"
at the top. If you added the COleRichEditCtrl.cpp and COleRichEditCtrl.h files to a folder that's different from that of your main project, you'll need to give the folder name too, like so: #include "../components/OleRichEditCtrl.h"
. Then, to use the OLE rich edit control instead of the standard rich edit control, page down in the header file until you see a line like:
CRichEditCtrl m_ctlRichEdit;
and replace it with the following:
COleRichEditCtrl m_ctlRichEdit;
Before you build and run your program, you must be certain that it calls AfxInitRichEdit()
somewhere, usually in the CWinApp::InitInstance()
call. If you did not choose "Compound Document Support" when creating your application with the AppWizard, then you must edit your code manually to insert a call to AfxInitRichEdit()
. Do it now. Now build and run your program.
To include RTF text as part of your program's resources, use a "bare-bones" RTF editor like standard WordPad to create your document. (I recommend simple RTF editors like WordPad because more complex editors like Word often insert large amounts of needless and confusing tags into the RTF stream.) Save the document in RTF format and then move a copy of it into the /res folder of your project. I suggest a copy instead of a direct save, since Visual Studio has a nasty habit of erasing the contents of customized resources if you're not careful (which is very frustrating, believe me). Let's say that the name of the file is text.rtf. Go to the "Resources" tab of the ClassView, right-click the project, and select to "Import" a resource from the pop-up menu:
Select your text.rtf file and then define an alphabetic "type" for your custom resource. I tend to use something obvious, like "RTF_TEXT
", as shown in this screenshot:
Now, to stream in this resource when the dialog opens, simply use the COleRichEditCtrl::StreamInFromResource
function. In your dialog's OnInitDialog()
function, simply add the following line of code:
m_ctlRichEdit.StreamInFromResource( IDR_RTF_TEXT1, "RTF_TEXT" );
That's it: build and run your program. You might need to do a "Rebuild All" to get your custom resource built into your executable, since Visual Studio is not very good at detecting when a non-standard resource (like your "RTF_TEXT
" resource) has been added into a project.
top
Some Final Words
Here are a few practical suggestions if you run into trouble using the class.
- If your program worked fine before adding
COleRichEditCtrl
to it, but then doesn't seem to work at all afterwards, then you probably need to insert a call to AfxInitRichEdit()
. The best place for this is inside your application's CMyApp::InitInstance
function.
- If you added a customized "
RTF_TEXT
" resource like the fictitious test.rtf file mentioned above, but after building, you can't see the contents of the resource in the control, then you probably need to do a "Rebuild All". Visual Studio is not very good at detecting when a non-standard resource (like your "RTF_TEXT
" resource) has been added into a project.
- I noticed some odd behavior in the demo program, in which the rich edit control doesn't seem to erase and repaint itself properly if you give it a particular sequence of re-sizings and scrolls. In the demo, the dialog has a re-sizing border and can be re-sized in the conventional way by dragging on the border. The control itself also has a vertical scroll bar for vertical scrolling of the contents. If you launch the dialog and then re-size the dialog before touching the scroll bar, then you'll see the odd behavior, particularly if you re-size to a narrow width and then make the dialog wider again (this makes the control calculate new line-break positions). As soon as you touch the scroll bar at least once, then all is good from there on out. In other words, once you scroll once, then you can re-size all you want, and the control will erase and repaint itself perfectly. I don't know the reason for this behavior, and nothing turned up in my web searches. If anyone has a fix for this, then please let us know.
top
Version and Revision History
- February 4, 2005 - First release.
top
Bibliography
Here, in one place, is a list of all the articles and links mentioned in the article:
top
Footnotes
- At this point, you really should be able to use ClassWizard to add a variable of type
COleRichEditCtrl
directly, without the steps mentioned in the main text. However, I have found that the ClassWizard has problems enrolling the new class in its database. You can try the following procedure to force ClassWizard to re-build its database, but even though this has worked for me in the past with other controls, I just tried it and for some reason it didn't work. The procedure in the main text above always works, but in case this alternative procedure works for you, here it is: the class database is stored in a file whose extension is ".clw" in your project's folder. To force ClassWizard to re-build the class database, open your project's workspace with Explorer and find the file whose extension is ".clw". Delete it. (Trust me, but if you don't, rename it to an extension like ".clw~1".) Now, open ClassWizard, and you'll get a message saying that the ".clw" file doesn't exist, and asking if you would like to re-build it from your source files. Of course you should select "Yes". From the resulting dialog, select "Add All". In addition, make certain that you also add COleRichEditCtrl.cpp and COleRichEditCtrl.h from whatever folder you stored them to.