[Previous] [Next]

The OLE Clipboard

The OLE clipboard is a modern-day version of the legacy clipboard. It is also backward-compatible. Thanks to some magic built into the OLE libraries, you can put a text string, a bitmap, or some other item on the OLE clipboard and an application that knows nothing about OLE can paste that item just as if it had come from the legacy clipboard. Conversely, an application can use the OLE clipboard to retrieve data from the legacy clipboard.

What's different about the OLE clipboard, and why is it superior to the old clipboard? There are two major differences between the two. First, the OLE clipboard is completely COM-based; all data is transferred by calling methods through pointers to COM interfaces. Second, the OLE clipboard supports storage media other than global memory. The legacy clipboard, in contrast, uses memory for all data transfers, which effectively limits the size of items transferred through the clipboard to the amount of memory available. Because of the legacy clipboard's inability to use media other than memory for data transfers, the compatibility between the legacy clipboard and the OLE clipboard is subject to the limitation that only items transferred through memory can be copied to one and retrieved from the other.

The first reason alone isn't enough to justify forsaking the legacy clipboard. COM is trendy and objects are cool, but without MFC, code that interacts with the OLE clipboard is much more complex than legacy clipboard code. But the second reason—the freedom to use alternative storage media—is just cause to use the OLE clipboard. Transferring a 4-GB bitmap through the legacy clipboard is impossible because current versions of Windows don't support memory objects that large. With the OLE clipboard, however, you can transfer anything that will fit on your hard disk. In fact, with a little ingenuity, you can transfer anything at all—even items too large to fit on a hard disk. Given the huge volumes of information that many modern applications are forced to deal with, the OLE clipboard can be a very handy tool indeed.

OLE Clipboard Basics

The first and most fundamental notion to understand about the OLE clipboard is that when you place an item of data on it, you don't actually place the data itself. Instead, you place a COM data object that encapsulates the data. A data object is a COM object that implements the IDataObject interface. IDataObject has two methods that play key roles in the operation of the OLE clipboard: SetData and GetData. Assuming that the data object is a generic data repository (as opposed to an object that is custom-fit to handle a particular set of data), a data provider stuffs data into the data object with IDataObject::SetData. It then places the object on the OLE clipboard with ::OleSetClipboard. A data consumer calls ::OleGetClipboard to get the clipboard data object's IDataObject pointer, and then it calls IDataObject::GetData to retrieve the data.

Figure 19-1 provides a conceptual look at OLE clipboard operations. This is a simplified view in that the IDataObject pointer returned by ::OleGetClipboard isn't really the IDataObject pointer that was passed to ::OleSetClipboard. Rather, it's a pointer to the IDataObject interface implemented by a system-provided clipboard data object that wraps the data object provided to ::OleSetClipboard and also allows consumers to access data on the legacy clipboard. Fortunately, this bit of indirection doesn't affect the code you write one iota. You simply use the IDataObject interface to interact with the data object. The system does the rest.

Click to view at full size.

Figure 19-1. Transferring data through the OLE clipboard.

Using the OLE clipboard sounds reasonably simple, but nothing is simple when COM and OLE are involved. The hard part is writing the code for a data object and implementing not only IDataObject::GetData and IDataObject::SetData but also the other IDataObject methods. But first things first. Assuming that you've already implemented a data object and that pdo holds a pointer to the object's IDataObject interface, here's one way to place a text string on the OLE clipboard:

// Copy the text string to a global memory block.
char szText[] = "Hello, world";
HANDLE hData = ::GlobalAlloc (GMEM_MOVEABLE, ::lstrlen (szText) + 1);
LPSTR pData = (LPSTR) ::GlobalLock (hData);
::lstrcpy (pData, szText);
::GlobalUnlock (hData);

// Initialize a FORMATETC structure and a STGMEDIUM structure that
// describe the data and the location at which it's stored.
FORMATETC fe;
fe.cfFormat = CF_TEXT;          // Clipboard format=CF_TEXT
fe.ptd = NULL;               // Target device=Screen
fe.dwAspect = DVASPECT_CONTENT;     // Level of detail=Full content
fe.lindex = -1;               // Index=Not applicable
fe.tymed = TYMED_HGLOBAL;     // Storage medium=Memory

STGMEDIUM stgm;
stgm.tymed = TYMED_HGLOBAL;     // Storage medium=Memory
stgm.hGlobal = hData;          // Handle to memory block
stgm.pUnkForRelease = NULL;     // Use ReleaseStgMedium

// Place the data object on the OLE clipboard.
pdo->SetData (&fe, &stgm, FALSE);
::OleSetClipboard (pdo);
pdo->Release ();

The Release call in the final statement assumes that the application that created the data object has no more use for it after handing it off to the OLE clipboard. Calling Release on the data object won't cause the object to self-delete because ::OleSetClipboard performs an AddRef on the IDataObject pointer passed to it.

Retrieving the text string is a little less work because we don't have to create a data object. But the process still isn't quite as straightforward as the one for retrieving a string from the legacy clipboard:

char szText[BUFLEN];
IDataObject* pdo;
STGMEDIUM stgm;

FORMATETC fe = {
    CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL
};

if (SUCCEEDED (::OleGetClipboard (&pdo))) {
    if (SUCCEEDED (pdo->GetData (&fe, &stgm) && stgm.hGlobal != NULL)) {
        LPCSTR pData = (LPCSTR) ::GlobalLock (stgm.hGlobal);
        if (::lstrlen (pData) < BUFLEN)
            ::lstrcpy (szText, pData);
        ::GlobalUnlock (stgm.hGlobal);
        ::ReleaseStgMedium (&stgm);
    }
    pdo->Release ();
}

If the data object can't provide the requested data, it returns an HRESULT signifying failure. The SUCCEEDED macro used in this example is the same one that we used to test HRESULTs in Chapter 18.

Two structures play key roles in the operation of SetData and GetData: FORMATETC and STGMEDIUM. FORMATETC describes the format of the data and identifies the type of storage medium (for example, global memory block or file) that holds the data. Here's how FORMATETC is defined in Objidl.h:

typedef struct  tagFORMATETC {
    CLIPFORMAT cfFormat;     // Clipboard format
    DVTARGETDEVICE *ptd;     // Target device
    DWORD dwAspect;     // Level of detail
    LONG lindex;          // Page number or other index
    DWORD tymed;          // Type of storage medium
} FORMATETC;

The two most important fields are cfFormat and tymed. cfFormat holds a clipboard format ID. The ID can be a standard clipboard format ID such as CF_TEXT or CF_BITMAP, or it can be a private clipboard format ID. tymed identifies the type of storage medium and can be any one of the values listed in the following table. Most OLE clipboard data transfers still use old-fashioned global memory blocks, but as you can plainly see, FORMATETC supports other media types as well.

IDataObject Storage Media Types

tymed Flag Storage Medium Type
TYMED_HGLOBAL Global memory block
TYMED_FILE File
TYMED_ISTREAM Stream object (implements interface IStream)
TYMED_ISTORAGE Storage object (implements interface IStorage)
TYMED_GDI GDI bitmap
TYMED_MFPICT Metafile picture
TYMED_ENHMF GDI enhanced metafile

FORMATETC identifies the storage medium type, but the STGMEDIUM structure identifies the storage medium itself. For example, if data is stored in a global memory block, the STGMEDIUM structure holds an HGLOBAL. If the data lives in a file instead, the STGMEDIUM holds a pointer to a character string that specifies the file name. STGMEDIUM holds other information as well. Here's how the structure is defined:

typedef struct  tagSTGMEDIUM {
    DWORD tymed;
    union {
        HBITMAP hBitmap;               // TYMED_GDI
        HMETAFILEPICT hMetaFilePict;     // TYMED_MFPICT
        HENHMETAFILE hEnhMetaFile;     // TYMED_ENHMF
        HGLOBAL hGlobal;               // TYMED_HGLOBAL
        LPOLESTR lpszFileName;          // TYMED_FILE
        IStream *pstm;               // TYMED_STREAM
        IStorage *pstg;               // TYMED_STORAGE
    };
    IUnknown *pUnkForRelease;
} STGMEDIUM;

Here tymed holds a TYMED value that identifies the storage medium type, just as FORMATETC's tymed field does. hBitmap, hMetaFilePict, and other members of the embedded union identify the actual storage. Finally, pUnkForRelease holds a pointer to the COM interface whose Release method releases the storage medium. When an application retrieves an item from the OLE clipboard with IDataObject::GetData, that application is responsible for releasing the storage medium when it's no longer needed. For a memory block, "release" means to free the block; for a file, it means to delete the file. COM provides an API function named ::ReleaseStgMedium that an application can call to release a storage medium. If you simply set pUnkForRelease to NULL when you initialize a STGMEDIUM, ::ReleaseStgMedium will free the storage medium using logic that is appropriate for the storage medium type.

There's much more that could be written about these data structures, but the description offered here should be enough to enable you to understand the examples in the previous section. The first example initialized a FORMATETC structure to describe an ANSI text string (cfFormat=CF_TEXT) stored in a global memory block (tymed=TYMED_HGLOBAL). It also wrapped the memory block with a STGMEDIUM (hGlobal=hData and tymed=TYMED_HGLOBAL). Both structures were passed by address to IDataObject::SetData.

In the second example, a FORMATETC structure was initialized with the same parameters and the STGMEDIUM structure was left uninitialized. Both were passed to IDataObject::GetData to retrieve the text string. In this case, the parameters in the FORMATETC structure told the data object what kind of data and what type of storage medium the caller wanted. On return from IDataObject::GetData, the STGMEDIUM structure held the HGLOBAL through which the data could be accessed.

By now, you're probably beginning to understand why programming the OLE clipboard is more involved than programming the legacy clipboard. You haven't seen the half of it yet, however, because I haven't shown the code for the data object. Remember, a data object is a COM object that implements the IDataObject interface. IDataObject is part of a COM-based data transfer protocol that Microsoft has christened Uniform Data Transfer, or UDT. I mentioned earlier that GetData and SetData are just two of the IDataObject methods you must wrestle with. The table below contains a complete list.

IDataObject Methods

Method Description
GetData Retrieves data from the data object (object provides the storage medium)
GetDataHere Retrieves data from the data object (caller provides the storage medium)
QueryGetData Determines whether data is available in a particular format
GetCanonicalFormatEtc Creates a different but logically equivalent FORMATETC
SetData Provides data to the data object
EnumFormatEtc Used to enumerate available data formats
DAdvise Establishes an advisory connection to the data object
DUnadvise Terminates an advisory connection
EnumDAdvise Enumerates existing advisory connections

You don't have to implement all these methods to perform a simple clipboard data transfer (some methods can simply return the special COM error code E_NOTIMPL), but implementing IDataObject is still a nontrivial task. Copying a simple text string to the legacy clipboard requires just a few lines of code. Copying the same text string to the OLE clipboard can require several hundred lines, primarily because of the added overhead of implementing a full-blown COM data object.

If having to write hundreds of lines of code to copy a string to the clipboard seems silly, take heart. MFC greatly simplifies matters by providing the data object for you and by wrapping it in friendly C++ classes that hide the FORMATETC structures and the STGMEDIUM structures and other low-level nuts and bolts of the IDataObject interface. Generally speaking, using the OLE clipboard in an MFC application is no more difficult than using the legacy clipboard, particularly when you use global memory as the storage medium. And you retain the option of using files and other storage media as alternatives to global memory. All things considered, MFC's abstraction of the OLE clipboard is a big win for programmers. Let's see if you agree.

MFC, Global Memory, and the OLE Clipboard

MFC's OLE clipboard support is concentrated in two classes. The first, COleDataSource, models the provider side of clipboard operations. The second, COleDataObject, models the consumer side. In other words, you use COleDataSource to place data on the OLE clipboard and COleDataObject to retrieve it. Not surprisingly, COleDataSource contains a generic implementation of COM's IDataObject interface. You can see this implementation for yourself in the MFC source code file Oledobj2.cpp. If you're not familiar with the manner in which MFC classes implement COM interfaces, you might want to review Chapter 18 before reading the source code.

Placing an item that's stored in global memory on the OLE clipboard is easy when you let COleDataSource do the dirty work. Here are the steps:

  1. Create a COleDataSource object on the heap (not on the stack).
  2. Call COleDataSource::CacheGlobalData to hand the HGLOBAL to the COleDataSource object.
  3. Place the object on the OLE clipboard by calling COleDataSource::SetClipboard.

The following example uses COleDataSource to make an ANSI text string available through the OLE clipboard:

char szText[] = "Hello, world"; // ANSI characters
HANDLE hData = ::GlobalAlloc (GMEM_MOVEABLE, ::lstrlen (szText) + 1);
LPSTR pData = (LPSTR) ::GlobalLock (hData);
::lstrcpy (pData, szText);
::GlobalUnlock (hData);

COleDataSource* pods = new COleDataSource;
pods->CacheGlobalData (CF_TEXT, hData);
pods->SetClipboard ();

Notice that the COleDataSource object is created on the heap, not on the stack. That fact is important because the object must remain in memory until a call to IUnknown::Release drops the data object's reference count to 0, at which time the object self-deletes. If you were to create the COleDataSource on the stack, the object would be deleted the moment it went out of scope.

MFC's COleDataObject provides a handy mechanism for retrieving items from the OLE clipboard. Here's the procedure for retrieving an item stored in global memory:

  1. Create a COleDataObject object.
  2. Call COleDataObject::AttachClipboard to connect the COleDataObject to the OLE clipboard.
  3. Use COleDataObject::GetGlobalData to retrieve the item.
  4. Free the global memory block returned by GetGlobalData.

And here's how the text string placed on the OLE clipboard in the previous example is retrieved using COleDataObject:

char szText[BUFLEN];
COleDataObject odo;
odo.AttachClipboard ();
HANDLE hData = odo.GetGlobalData (CF_TEXT);

if (hData != NULL) {
    LPCSTR pData = (LPCSTR) ::GlobalLock (hData);
    if (::lstrlen (pData) < BUFLEN)
        ::lstrcpy (szText, pData);
    ::GlobalUnlock (hData);
    ::GlobalFree (hData);
}

The AttachClipboard function creates a logical connection between a COleDataObject and the OLE clipboard. Once the connection is made, MFC transforms calls to GetGlobalData and other COleDataObject data retrieval functions into GetData calls through the IDataObject pointer returned by ::OleGetClipboard. Don't forget that it's your responsibility to free the global memory block returned by GetGlobalData. That requirement explains the call to ::GlobalFree in the preceding example.

Using Alternative Storage Media

All the examples presented so far in this chapter have used global memory as the transfer medium. But remember that the OLE clipboard supports other media types, too. COleDataSource::CacheGlobalData and COleDataObject::GetGlobalData are hardwired to use global memory blocks. You can use the more generic COleDataSource::CacheData and COleDataObject::GetData functions to transfer data in other types of storage media.

The next example demonstrates how to transfer a text string through the OLE clipboard using a file as the transfer medium. The string is first copied into a temporary file. Then FORMATETC and STGMEDIUM structures are initialized with information describing the file and the data that it contains. Finally, the information is passed to COleDataSource::CacheData, and the data object is placed on the OLE clipboard with COleDataSource::SetClipboard:

char szText[] = "Hello, world";
TCHAR szPath[MAX_PATH], szFileName[MAX_PATH];
::GetTempPath (sizeof (szPath) / sizeof (TCHAR), szPath);
::GetTempFileName (szPath, _T ("tmp"), 0, szFileName);

CFile file;
if (file.Open (szFileName, CFile::modeCreate | CFile::modeWrite)) {
    file.Write (szText, ::lstrlen (szText) + 1);
    file.Close ();

    LPWSTR pwszFileName =
        (LPWSTR) ::CoTaskMemAlloc (MAX_PATH * sizeof (WCHAR));

#ifdef UNICODE
    ::lstrcpy (pwszFileName, szFileName);
#else
    ::MultiByteToWideChar (CP_ACP, MB_PRECOMPOSED, szFileName, -1,
        pwszFileName, MAX_PATH);
#endif

    FORMATETC fe = {
        CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_FILE
    };

    STGMEDIUM stgm;
    stgm.tymed = TYMED_FILE;
    stgm.lpszFileName = pwszFileName;
    stgm.pUnkForRelease = NULL;

    COleDataSource* pods = new COleDataSource;
    pods->CacheData (CF_TEXT, &stgm, &fe);
    pods->SetClipboard ();
}

The file name whose address is copied to the STGMEDIUM structure prior to calling CacheData must be composed of Unicode characters. This is always true, even in Windows 98. You must also allocate the file name buffer using the COM function ::CoTaskMemAlloc. Among other things, this ensures that the buffer is properly freed when ::ReleaseStgMedium calls ::CoTaskMemFree on the buffer pointer.

On the consumer side, you can use COleDataObject::GetData to retrieve the string from the clipboard:

char szText[BUFLEN];
STGMEDIUM stgm;

FORMATETC fe = {
    CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_FILE
};

COleDataObject odo;
odo.AttachClipboard ();

if (odo.GetData (CF_TEXT, &stgm, &fe) && stgm.tymed == TYMED_FILE) {
    TCHAR szFileName[MAX_PATH];

#ifdef UNICODE
    ::lstrcpy (szFileName, stgm.lpszFileName);
#else
    ::WideCharToMultiByte (CP_ACP, 0, stgm.lpszFileName,
        -1, szFileName, sizeof (szFileName) / sizeof (TCHAR), NULL, NULL);
#endif

    CFile file;
    if (file.Open (szFileName, CFile::modeRead)) {
        DWORD dwSize = file.GetLength ();
        if (dwSize < BUFLEN)
            file.Read (szText, (UINT) dwSize);
        file.Close ();
    }
    ::ReleaseStgMedium (&stgm);
}

When you retrieve data with COleDataObject::GetData, you are responsible for freeing the storage medium, which is why ::ReleaseStgMedium is called in the final statement of this example.

Of course, transferring small text strings through files rather than global memory blocks doesn't make much sense. If the item being transferred is a large bitmap, however, such a transfer might make a lot of sense—especially if the bitmap is already stored on disk somewhere. I used text strings in this section's examples to make the code as simple and uncluttered as possible, but the principle represented here applies to data of all types.

Treating the OLE Clipboard as a CFile

MFC's COleDataObject::GetFileData function provides a handy abstraction of the OLE clipboard that enables data stored in any of the following storage media to be retrieved as if the clipboard were an ordinary CFile:

If successful, GetFileData returns a pointer to a CFile object that wraps the item retrieved from the clipboard. You can call CFile::Read through that pointer to read the data out.

The following example demonstrates how to use GetFileData to retrieve a string from the OLE clipboard:

char szText[BUFLEN];
COleDataObject odo;
odo.AttachClipboard ();

CFile* pFile = odo.GetFileData (CF_TEXT);

if (pFile != NULL) {
    DWORD dwSize = pFile->GetLength ();
    if (dwSize < BUFLEN)
        pFile->Read (szText, (UINT) dwSize);
    delete pFile; // Don't forget this!
}

Again, notice that you are responsible for deleting the CFile object whose address is returned by GetFileData. If you forget to delete it, you'll suffer memory leaks.

The code above is the functional equivalent of the GetData example presented in the previous section, but with two added benefits. One, it's simpler. Two, it works whether data is stored in a global memory block, a file, or a stream. In other words, one size fits all. To get the same results with GetData, you'd have to do something like this:

char szText[BUFLEN];
STGMEDIUM stgm;

COleDataObject odo;
odo.AttachClipboard ();

FORMATETC fe = {
    CF_TEXT, NULL, DVASPECT_CONTENT, -1,
    TYMED_FILE | TYMED_HGLOBAL | TYMED_ISTREAM
};

if (odo.GetData (CF_TEXT, &stgm, &fe)) {
    switch (stgm.tymed) {

    case TYMED_FILE:
        // Read the string from a file.
            .
            .
            .
        break;

    case TYMED_HGLOBAL:
        // Read the string from a global memory block.
            .
            .
            .
        break;

    case TYMED_ISTREAM:
        // Read the string from a stream object.
            .
            .
            .
        break;
    }
    ::ReleaseStgMedium (&stgm);
}

Notice the use of multiple TYMED flags in the FORMATETC structure passed to GetData. TYMED flags can be OR'd together in this manner to inform a data object that the caller will accept data in a variety of different storage media.

Multiple Formats and Multiple Storage Media

A data provider can call CacheData or CacheGlobalData as many times as necessary to make data available to data consumers in a variety of formats. The following code offers an item in two formats: a private format registered with ::RegisterClipboardFormat (nFormat) and a CF_TEXT format:

COleDataSource* pods = new COleDataSource;
pods->CacheGlobalData (nFormat, hPrivateData);
pods->CacheGlobalData (CF_TEXT, hTextData);
pods->SetClipboard ();

You can also make multiple data items available in the same format but in different storage media. Suppose you want to make CF_TEXT data available ineither a global memory block or a file. Assuming that pwszFileName has already been initialized to point to a file name (expressed in Unicode characters), here's how you go about it:

FORMATETC fe = {
    CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_FILE
};

STGMEDIUM stgm;
stgm.tymed = TYMED_FILE;
stgm.lpszFileName = pwszFileName;
stgm.pUnkForRelease = NULL;

COleDataSource* pods = new COleDataSource;
pods->CacheGlobalData (CF_TEXT, hTextData);     // TYMED_HGLOBAL
pods->CacheData (CF_TEXT, &stgm, &fe);          // TYMED_FILE
pods->SetClipboard ();

Calling CacheData and CacheGlobalData more than once and then placing the data object on the clipboard is analogous to calling ::SetClipboardData multiple times to place two or more formats on the legacy clipboard. However, the legacy clipboard won't accept two items that are of the same format. The OLE clipboard will—as long as each FORMATETC structure has a unique tymed value, which is another way of saying that the items are stored indifferent types of storage media.

Checking Data Availability

The API function ::IsClipboardFormatAvailable allows users of the legacy clipboard to find out whether data is available in a certain format. COleDataObject::IsDataAvailable lets OLE clipboard users do the same. The following code fragment checks to see whether CF_TEXT data is available in an HGLOBAL:

COleDataObject odo;
odo.AttachClipboard ();
if (odo.IsDataAvailable (CF_TEXT)) {
    // CF_TEXT is available in an HGLOBAL.
}
else {
    // CF_TEXT is not available in an HGLOBAL.
}

To check for storage media types other than global memory, you simply initialize a FORMATETC structure and pass its address to IsDataAvailable, as shown here:

COleDataObject odo;
odo.AttachClipboard ();

FORMATETC fe = {
    CF_TEXT, NULL, DVASPECT_CONTENT, -1, TYMED_ISTREAM
};

if (odo.IsDataAvailable (CF_TEXT, &fe)) {
    // CF_TEXT is available in a stream object.
}
else {
    // CF_TEXT is not available in a stream object.
}

If you want to, you can OR several TYMED flags into the tymed field of the FORMATETC structure passed to IsDataAvailable. The return value will be nonzero if data is available in any of the requested storage media.

NOTE
As a result of a bug in MFC 6.0, COleDataObject::IsDataAvailable sometimes returns a nonzero value if the requested data is available in any storage medium. In effect, the media type information passed to IsDataAvailable in a FORMATETC structure is ignored. Significantly, the bug manifests itself only if IsDataAvailable is called on a COleDataObject that's attached to the OLE clipboard, and it affects some data types (notably CF_TEXT data) more than others. IsDataAvailable works as advertised when COleDataObject is used to implement an OLE drop target.

Data consumers can use the COleDataObject functions BeginEnumFormats and GetNextFormat to enumerate the various formats available. The following code fragment enumerates all the formats available on the OLE clipboard:

COleDataObject odo;
odo.AttachClipboard ();

FORMATETC fe;
odo.BeginEnumFormats ();
while (odo.GetNextFormat (&fe)) {
    // FORMATETC structure describes the next available format.
}

If a particular data format is available in two or more types of storage media, GetNextFormat is supposed to either initialize the FORMATETC structure's tymed field with bit flags identifying each storage medium type or return a unique FORMATETC structure for each tymed. However, an interesting (and potentially aggravating) anomaly can occur. If the OLE clipboard contains two data items with identical cfFormats but different tymeds, GetNextFormat will return information for only one of them. This appears to be a bug in the system-supplied clipboard data objectwhose IDataObject pointer is returned by ::OleGetClipboard. If you need to know what media types a given clipboard format is available in, use IsDataAvailable to query for individual combinations of clipboard formats and storage media.

Delayed Rendering with COleDataSource

Does the OLE clipboard support delayed rendering? The short answer is yes, although in truth, MFC's implementation of COleDataSource, not the OLE clipboard, makes delayed rendering work. A glimpse under the hood of COleDataSource explains why.

A COleDataSource object is first and foremost a data cache. Internally, it maintains an array of FORMATETC and STGMEDIUM structures that describe the data that is currently available. When an application calls CacheData or CacheGlobalData, a STGMEDIUM structure with a tymed value that describes the storage medium type is added to the array. If an application calls DelayRenderData instead, a STGMEDIUM structure that contains a NULL tymed value is added to the array. When asked to retrieve that data, the COleDataSource sees the NULL tymed value and knows that the data was promised via delayed rendering. COleDataSource responds by calling a virtual function named OnRenderData. Your job is to override this function in a derived class so that you can provide the data on request.

Here's an example that demonstrates how to place a bitmap on the OLE clipboard using delayed rendering. The first step is to make a copy of the bitmap and store it in a file. (You could store it in memory, but that might defeat the purpose of using delayed rendering in the first place.) The second step is to call DelayRenderData:

FORMATETC fe = {
    CF_BITMAP, NULL, DVASPECT_CONTENT, -1, TYMED_GDI
};

CMyDataSource* pmds = new CMyDataSource;
pmds->DelayRenderData (CF_BITMAP, &fe);
pmds->SetClipboard ();

CMyDataSource is a COleDataSource derivative. Here's the OnRenderData function that renders the bitmap to a TYMED_GDI storage medium when the data source is asked to hand over the bitmap:

BOOL CMyDataSource::OnRenderData (LPFORMATETC lpFormatEtc,
    LPSTGMEDIUM lpStgMedium) 
{
    if (COleDataSource::OnRenderData (lpFormatEtc, lpStgMedium))
        return TRUE;

    if (lpFormatEtc->cfFormat == CF_BITMAP &&
        lpFormatEtc->tymed & TYMED_GDI) {

        // Re-create the bitmap from the file, and store the
        // handle in hBitmap. 	
            .
            .
            .
        lpFormatEtc->cfFormat = CF_BITMAP;
        lpFormatEtc->ptd = NULL;
        lpFormatEtc->dwAspect = DVASPECT_CONTENT;
        lpFormatEtc->lindex = -1;
        lpFormatEtc->tymed = TYMED_GDI;

        lpStgMedium->tymed = TYMED_GDI;
        lpStgMedium->hBitmap = hBitmap;
        lpStgMedium->pUnkForRelease = NULL;

        CacheData (CF_BITMAP, lpStgMedium, lpFormatEtc);
        return TRUE;
    }
    return FALSE;
}

Other than the fact that you have to derive a class and override OnRenderData, delayed rendering with a COleDataSource isn't much different from immediate rendering.

Other COleDataSource functions can sometimes simplify the delayed rendering code that you write. For example, if you intend to render data only to HGLOBAL storage media, you can override OnRenderGlobalData instead of OnRenderData. You can use a separate set of COleDataSource functions named DelayRenderFileData and OnRenderFileData functions to delay-render data using CFile output functions.

One detail to be aware of when you use, COleDataSource delayed rendering is that if the storage type is TYMED_HGLOBAL, TYMED_FILE, TYMED_ISTREAM, or TYMED_ISTORAGE, the storage medium might be allocated before OnRenderData is called. If the storage medium is preallocated, OnRenderData must render the data into the existing storage medium rather than create a new storage medium itself. The tymed value in the STGMEDIUM structure whose address is passed to OnRenderData tells the tale. If lpStgMedium->tymed is TYMED_NULL, OnRenderData is responsible for allocating the storage medium. If lpStgMedium->tymed holds any other value, the caller has supplied the storage medium and lpStgMedium->tymed identifies the storage type. The following code sample demonstrates proper handling of OnRenderData for media types that are subject to preallocation:

BOOL CMyDataSource::OnRenderData (LPFORMATETC lpFormatEtc,
    LPSTGMEDIUM lpStgMedium) 
{
    if (COleDataSource::OnRenderData (lpFormatEtc, lpStgMedium))
        return TRUE;

    if (lpStgMedium->tymed == TYMED_NULL) { // Medium is not preallocated.
        if (lpFormatEtc->tymed & TYMED_HGLOBAL) {
            // Allocate a global memory block, render the data
            // into it, and then copy the handle to lpStgMedium->hGlobal.
           }
    }
    else { // Medium is preallocated.
        if (lpStgMedium->tymed == TYMED_HGLOBAL) {
            // Render the data into the global memory block whose
            // handle is stored in lpStgMedium->hGlobal.
        }
    }
}

This example addresses only the case in which the storage medium is an HGLOBAL, but the principle should be clear nonetheless.

The most common reason for using COleDataSource's brand of delayed rendering is to provide data in a variety of storage media without having to allocate each and every storage medium up front. If you're willing to provide, say, CF_TEXT data in several different media, you can call DelayRenderData and pass in a FORMATETC structure whose tymed field contains bit flags representing each of the media types that you support. Then you can render the data in any medium that the data consumer requests by inspecting the tymed field of the FORMATETC structure passed to OnRenderData. If the consumer asks for the data in a medium that you don't support, you can simply fail the call to OnRenderData by returning FALSE.

COleDataSource and COleDataObject in Review

You now know how to use MFC's COleDataSource and COleDataObject classes to interact with the OLE clipboard. Just to put things in perspective (and to reinforce what you've already learned), the following tables provide a brief summary of the most useful COleDataSource and COleDataObject member functions. These classes have other functions as well, but those listed here are the ones that you're most likely to need.

Key COleDataSource Member Functions

Function Description
SetClipboard Places the COleDataSource on the OLE clipboard
CacheData Provides data to the COleDataSource
CacheGlobalData Provides data stored in global memory to the COleDataSource
DelayRenderData Offers data for delayed rendering
DelayRenderFileData Offers data for delayed rendering using CFile output functions
OnRenderData Called to render data to an arbitrary storage medium
OnRenderFileData Called to render data to a CFile
OnRenderGlobalData Called to render data to an HGLOBAL

Key COleDataObject Member Functions

Function Description
AttachClipboard Attaches the COleDataObject to the OLE clipboard
GetData Retrieves data from the data object to which the COleDataObject is attached
GetFileData Retrieves data using CFile functions
GetGlobalData Retrieves data in an HGLOBAL
IsDataAvailable Determines whether data is available in a particular format and storage medium
BeginEnumFormats Begins the process of enumerating available data formats
GetNextFormat Fills a FORMATETC structure with information describing the next available data format

Earlier, I said that the primary reason to use the OLE clipboard is to gain the ability to use storage media other than global memory. That's true, but there's another reason, too. Thanks to the abstractions offered by COleDataSource and COleDataObject, once you write MFC code to utilize the OLE clipboard, you only have to do a little more work to add support for an even more convenient form of data transfer: OLE drag-and-drop. OLE drag-and-drop lets the user transfer data by grabbing it with the mouse and dragging it. Writing OLE drag-and-drop code without a class library to help out isn't any fun, but MFC makes the process as hassle-free as possible.

The CHM file was converted to HTML by chm2web software.