Click here to Skip to main content
11,795,568 members (81,471 online)
Click here to Skip to main content

A Rich Edit Control That Displays Bitmaps and Other OLE Objects

, 9 Feb 2005 247.7K 15.2K 155
Rate this:
Please Sign up or sign in to vote.
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



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.


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
    virtual ~COleRichEditCtrl();

    long StreamInFromResource(int iRes, LPCTSTR sType);

    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
        virtual ~IExRichEditOleCallback();
        int m_iNumStorages;
        IStorage* pStorage;
        DWORD m_dwRef;

        virtual HRESULT STDMETHODCALLTYPE GetNewStorage(LPSTORAGE* lplpstg);
                QueryInterface(REFIID iid, void ** ppvObject);
        virtual ULONG STDMETHODCALLTYPE AddRef();
        virtual ULONG STDMETHODCALLTYPE Release();
                GetInPlaceContext(LPOLEINPLACEFRAME FAR *lplpFrame, 
                LPOLEINPLACEUIWINDOW FAR *lplpDoc, 
                LPOLEINPLACEFRAMEINFO lpFrameInfo);
         virtual HRESULT STDMETHODCALLTYPE ShowContainerUI(BOOL fShow);
                 QueryInsertObject(LPCLSID lpclsid, LPSTORAGE lpstg, LONG cp);
         virtual HRESULT STDMETHODCALLTYPE DeleteObject(LPOLEOBJECT lpoleobj);
                 QueryAcceptData(LPDATAOBJECT lpdataobj, CLIPFORMAT FAR *lpcfFormat, 
                 DWORD reco, BOOL fReally, HGLOBAL hMetaPict);
         virtual HRESULT STDMETHODCALLTYPE ContextSensitiveHelp(BOOL fEnterMode);
                 GetClipboardData(CHARRANGE FAR *lpchrg, 
                 DWORD reco, LPDATAOBJECT FAR *lplpdataobj);
                 GetDragDropEffect(BOOL fDrag, 
                 DWORD grfKeyState, LPDWORD pdwEffect);
                 GetContextMenu(WORD seltyp, LPOLEOBJECT lpoleobj, 
                 CHARRANGE FAR *lpchrg, HMENU FAR *lphmenu);


// Overrides
    // ClassWizard generated virtual function overrides
    virtual void PreSubclassWindow();

// Implementation

    // Generated message map functions
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);


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:

COleRichEditCtrl::IExRichEditOleCallback::GetNewStorage(LPSTORAGE* lplpstg)
    WCHAR tName[50];
    swprintf(tName, L"REOLEStorage%d", m_iNumStorages);

    HRESULT hResult = pStorage->CreateStorage(tName, 
        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); 
    CMemFile mfile;
    mfile.Attach(lpRsrc, len); 

    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.


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.


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

    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.


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.


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.


Version and Revision History

  • February 4, 2005 - First release.



Here, in one place, is a list of all the articles and links mentioned in the article:



  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.


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


About the Author

Mike O'Neill
United States United States
Mike O'Neill is a patent attorney in Southern California, where he specializes in computer and software-related patents. He programs as a hobby, and in a vain attempt to keep up with and understand the technology of his clients.

You may also be interested in...

Comments and Discussions

QuestionHow to add RICHEDIT50W supported? Pin
lonssoft21-Jan-15 6:46
memberlonssoft21-Jan-15 6:46 
GeneralMy vote of 5 Pin
s_gerova14-Nov-14 6:09
members_gerova14-Nov-14 6:09 
QuestionHow to load any rft in my pc into the richedit control? Pin
Member 97323845-Jan-13 18:37
memberMember 97323845-Jan-13 18:37 
GeneralHow do i handle NM_CLICK Event Pin
vss11128-Apr-11 2:22
membervss11128-Apr-11 2:22 
GeneralFinal Word 3 - Not Erasing and Repainting within bounds of CTRL - Solution Pin
Phil Outram26-May-10 22:53
memberPhil Outram26-May-10 22:53 
QuestionLicense terms? Pin
mark gooding8-Nov-09 23:35
membermark gooding8-Nov-09 23:35 
GeneralBUG: Resizing help dialog doesn't redraw rich edit control Pin
ols60003-Jul-09 8:19
memberols60003-Jul-09 8:19 
GeneralUnable to estimate print size Pin
mhorowit29-Apr-09 15:00
membermhorowit29-Apr-09 15:00 
Questionobjects copy?? [modified] Pin
LeeWonJong8-Feb-09 19:04
memberLeeWonJong8-Feb-09 19:04 
QuestionScrollbar down, Add new RTF - no visible Pin
pajero_pn8-Feb-09 1:04
memberpajero_pn8-Feb-09 1:04 
GeneralFree Memory problem Pin
Member 398388822-Dec-08 3:50
memberMember 398388822-Dec-08 3:50 
Generalload rtf Memory have a problem Pin
jackjons18-Dec-08 20:44
memberjackjons18-Dec-08 20:44 
Generalload rtf Memory have a problem Pin
jackjons18-Dec-08 20:43
memberjackjons18-Dec-08 20:43 
QuestionNice work! How to get rtf files? Pin
xunneinei6-Jul-08 15:12
memberxunneinei6-Jul-08 15:12 
GeneralCan't copy Pin
Staffan V3-Apr-08 2:43
memberStaffan V3-Apr-08 2:43 
AnswerRe: Can't copy Pin
Staffan V10-Apr-08 3:06
memberStaffan V10-Apr-08 3:06 
GeneralRe: Can't copy Pin
cayman1379-Sep-13 18:53
membercayman1379-Sep-13 18:53 
GeneralVery nice, but ignores ES_READONLY Pin
Staffan V2-Apr-08 22:56
memberStaffan V2-Apr-08 22:56 
Questionwmv problem Pin
ahyqs16-Mar-08 17:15
memberahyqs16-Mar-08 17:15 
Questionbitmap problem Pin
ahyqs16-Mar-08 16:49
memberahyqs16-Mar-08 16:49 
GeneralThis Article is very good! She gives a great of help! Pin
NickZhang14-Mar-08 0:36
memberNickZhang14-Mar-08 0:36 
Questionblank rect ?? Pin
iceyaobing29-Feb-08 3:46
membericeyaobing29-Feb-08 3:46 
Generalgood Pin
tdp_tdp23-Jan-08 7:37
membertdp_tdp23-Jan-08 7:37 
GeneralCtrl+C, Ctrl+X Pin
Hryuckinnen10-Oct-07 5:54
memberHryuckinnen10-Oct-07 5:54 
GeneralRe: Ctrl+C, Ctrl+X Pin
Member 31748436-Nov-08 15:36
memberMember 31748436-Nov-08 15:36 
QuestionHow can open rich text file ? Pin
KienNT7827-May-07 23:37
memberKienNT7827-May-07 23:37 
AnswerRe: How can open rich text file ? Pin
Jean-Noel5-Sep-07 3:51
memberJean-Noel5-Sep-07 3:51 
QuestionHow to disable select image or table objects? Pin
wdgcims4-Dec-06 2:49
memberwdgcims4-Dec-06 2:49 
QuestionHow can I display metafile ? Pin
Jagdish Vasani8-Nov-06 21:04
memberJagdish Vasani8-Nov-06 21:04 
QuestionExcel Transform Pin
litaoo28-Aug-06 18:04
memberlitaoo28-Aug-06 18:04 
Generali wanna to insert text parallel to inserted bitmap Pin
sheladiya chetan13-Jul-06 18:21
membersheladiya chetan13-Jul-06 18:21 
Questionhow to read an RTF file containing a bitmap and text Pin
sheladiya chetan3-Jul-06 23:41
membersheladiya chetan3-Jul-06 23:41 
GeneralCopy /paste other problem Pin
catialin5-Jun-06 22:37
membercatialin5-Jun-06 22:37 
GeneralCrash on Win 98 / ME Pin
CPP-XXL8-May-06 1:12
memberCPP-XXL8-May-06 1:12 
QuestionHow to show powerpoint in the rich edit control? Pin
tyut12345627-Mar-06 22:47
membertyut12345627-Mar-06 22:47 
GeneralProblem with Document file Pin
Dime_198122-Mar-06 21:39
memberDime_198122-Mar-06 21:39 
GeneralRe: Problem with Document file Pin
Dime_19815-Apr-06 21:17
memberDime_19815-Apr-06 21:17 
QuestionGood work, and Heeeelp Pin
objManager2-Mar-06 11:36
memberobjManager2-Mar-06 11:36 
GeneralPoblems with bitmap Pin
DianaBar-Or14-Jan-06 21:44
memberDianaBar-Or14-Jan-06 21:44 
GeneralRe: Poblems with bitmap Pin
summer036022-Mar-06 21:23
membersummer036022-Mar-06 21:23 
QuestionMemory leak with GetNewStorage() Pin
Peter JC20-Dec-05 16:12
memberPeter JC20-Dec-05 16:12 
AnswerRe: Memory leak with GetNewStorage() Pin
elks22-Dec-05 12:36
memberelks22-Dec-05 12:36 
QuestionHow to make bitmap not resizable Pin
DianaBar-Or21-Nov-05 5:07
memberDianaBar-Or21-Nov-05 5:07 
QuestionHow to load content from memory? Pin
Fzz29-Sep-05 22:40
memberFzz29-Sep-05 22:40 
AnswerRe: How to load content from memory? Pin
HuB_Offline29-Sep-05 23:23
memberHuB_Offline29-Sep-05 23:23 
GeneralRe: How to load content from memory? Pin
Anonymous29-Sep-05 23:29
sussAnonymous29-Sep-05 23:29 
GeneralRe: How to load content from memory? Pin
HuB_Offline29-Sep-05 23:37
memberHuB_Offline29-Sep-05 23:37 
GeneralGreat stuff Pin
piotrmb14-Jul-05 23:28
memberpiotrmb14-Jul-05 23:28 
GeneralPrinting Pin
Chirurg28-May-05 0:25
memberChirurg28-May-05 0:25 
GeneralThanks !!!! Pin
Gautam Jain22-Mar-05 18:48
memberGautam Jain22-Mar-05 18:48 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.151002.1 | Last Updated 10 Feb 2005
Article Copyright 2005 by Mike O'Neill
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid