Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Rich Edit Control That Displays Bitmaps and Other OLE Objects

0.00/5 (No votes)
9 Feb 2005 4  
COleRichEditCtrl will display RTF text as well as bitmaps, video clips, Word, Excel and PowerPoint documents, and any other kind of OLE objects.

Bitmaps and other OLE objects displayed in a rich edit control

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>


/////////////////////////////////////////////////////////////////////////////

// COleRichEditCtrl window


class COleRichEditCtrl : public CRichEditCtrl
{
// Construction

public:
    COleRichEditCtrl();
    virtual ~COleRichEditCtrl();

    long StreamInFromResource(int iRes, LPCTSTR sType);

protected:
    static DWORD CALLBACK readFunction(DWORD dwCookie,
         LPBYTE lpBuf,           // the buffer to fill

         LONG nCount,            // number of bytes to read

         LONG* nRead);           // number of bytes actually read


    interface IExRichEditOleCallback;
    // forward declaration (see below in this header file)


    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:

// Overrides

    // ClassWizard generated virtual function overrides

    //{{AFX_VIRTUAL(COleRichEditCtrl)

    protected:
    virtual void PreSubclassWindow();
    //}}AFX_VIRTUAL


// Implementation

public:

    // Generated message map functions

protected:
    //{{AFX_MSG(COleRichEditCtrl)

    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
    //}}AFX_MSG


    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 );
}

/* static */
DWORD CALLBACK COleRichEditCtrl::readFunction(DWORD dwCookie,
         LPBYTE lpBuf,            // the buffer to fill

         LONG nCount,            // number of bytes to read

         LONG* nRead)            // number of bytes actually read

{
    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;
     
    // m_pIRichEditOleCallback should have been created in PreSubclassWindow


     ASSERT( m_pIRichEditOleCallback != NULL );    

    // set the IExRichEditOleCallback pointer if it wasn't set 

    // successfully in PreSubclassWindow


    if ( !m_bCallbackSet )
    {
        SetOLECallback( m_pIRichEditOleCallback );
    }
     
     return 0;
}

void COleRichEditCtrl::PreSubclassWindow() 
{
    // base class first

    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:

Using the control toolbar to add a rich edit control

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:

Setting styles of the rich edit

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:

Screenshot of ClassWizard, showing how to add a member variable of type CRichEditCtrl

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:

Importing a custom resource

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:

Defining the alphabetic type of the resource

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.

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

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here