Implement IOleObject

In some ways, IOleObject looks like the interface from hell, a dumping ground for every function that didn't seem to have any better home. Intimidating? You bet! Is it a problem? Not really. For the most part, the member functions in this interface have either trivial or optional implementations. Only about 15 of the 21 member functions, excluding those in IUnknown, require some implementation for embedded objects. Twelve of those 15 either are trivial to implement or use defaults from the registry. The remaining 6 (of the 21) functions either are optional or are not used for embedded objects themselves. (They will become more important later on.) The following table lists the implementation requirements for IOleObject members:

Group

Members

Require real programming

SetHostNames, Close, DoVerb

Trivial implementations

SetClientSite, GetClientSite, Update, IsUpToDate, GetExtent, Advise, Unadvise, EnumAdvise, GetUserClassID

Implemented using registry

EnumVerbs, GetUserType, GetMiscStatus

Optional

SetExtent, InitFromData, GetClipboardData, SetColorScheme

Used for linking

SetMoniker, GetMoniker


Members in the real programming group will be the focus of our discussion here. Those with trivial implementations will generally require only a few lines of code, and standard code at that. Those using the registry can simply return OLE_S_USEREG, as we did with IDataObject::EnumFormatEtc. OLE implements these using OleRegEnumVerbs, OleRegGetUserType, and OleRegGetMiscStatus, respectively. For the others, you can simply return E_NOTIMPL if you don't need them. Otherwise, functions such as InitFromData and SetColorScheme can easily require nontrivial code.

Cosmo implements IOleObject through the class CImpIOleObject, found in IOLEOBJ.CPP. So that you can see the varying complexity of these members, I've included the entire file in Listing 18-1 beginning on the following page. The next three sections cover the trivial, required, and optional members in more detail.

IOLEOBJ.CPP


/*
* IOLEOBJ.CPP
* Copyright (c)1993-1995 Microsoft Corporation, All Rights Reserved
*/

#include "cosmo.h"

[Constructor, destructor, IUnknown members omitted]

STDMETHODIMP CImpIOleObject::SetClientSite
(LPOLECLIENTSITE pIOleClientSite)
{
if (NULL!=m_pObj->m_pIOleClientSite)
m_pObj->m_pIOleClientSite->Release();

m_pObj->m_pIOleClientSite=pIOleClientSite;
m_pObj->m_pIOleClientSite->AddRef();
return NOERROR;
}


STDMETHODIMP CImpIOleObject::GetClientSite(LPOLECLIENTSITE
*ppSite)
{
//Be sure to call AddRef on new pointer you are giving away.
*ppSite=m_pObj->m_pIOleClientSite;
m_pObj->m_pIOleClientSite->AddRef();

return NOERROR;
}


STDMETHODIMP CImpIOleObject::SetHostNames(LPCOLESTR pszApp
, LPCOLESTR pszObj)
{
m_pObj->m_fEmbedded=TRUE;
m_pObj->m_pFR->UpdateEmbeddingUI(TRUE, m_pObj->m_pDoc
, pszApp, pszObj);
return NOERROR;
} STDMETHODIMP CImpIOleObject::Close(DWORD dwSaveOption)
{
HWND hWnd;
BOOL fSave=FALSE;

hWnd=m_pObj->m_pDoc->Window();

//If object is dirty and we're asked to save, save it and close.
if (OLECLOSE_SAVEIFDIRTY==dwSaveOption && m_pObj->FIsDirty())
fSave=TRUE;

/*
* If asked to prompt, do so only if dirty; if we get a
* YES, save as usual and close. On NO, just close. On
* CANCEL, return OLE_E_PROMPTSAVECANCELLED.
*/
if (OLECLOSE_PROMPTSAVE==dwSaveOption && m_pObj->FIsDirty())
{
UINT uRet;

uRet=MessageBox(hWnd, (*m_pObj->m_pST)[IDS_CLOSECAPTION]
, (*m_pObj->m_pST)[IDS_CLOSEPROMPT], MB_YESNOCANCEL);

if (IDCANCEL==uRet)
return ResultFromScode(OLE_E_PROMPTSAVECANCELLED);

if (IDYES==uRet)
fSave=TRUE;
}

if (fSave)
{
m_pObj->SendAdvise(OBJECTCODE_SAVEOBJECT);
m_pObj->SendAdvise(OBJECTCODE_SAVED);
}

//We get here directly on OLECLOSE_NOSAVE.
PostMessage(hWnd, WM_CLOSE, 0, 0L);
return NOERROR;
} STDMETHODIMP CImpIOleObject::SetMoniker(DWORD dwWhich
, LPMONIKER pmk)
{
return ResultFromScode(E_NOTIMPL);
}


STDMETHODIMP CImpIOleObject::GetMoniker(DWORD dwAssign
, DWORD dwWhich, LPMONIKER *ppmk)
{
return ResultFromScode(E_NOTIMPL);
}


STDMETHODIMP CImpIOleObject::InitFromData(LPDATAOBJECT pIDataObject
, BOOL fCreation, DWORD dwReserved)
{
BOOL fRet;

fRet=m_pObj->m_pDoc->PasteFromData(pIDataObject);
return fRet ? NOERROR : ResultFromScode(E_FAIL);
}


STDMETHODIMP CImpIOleObject::GetClipboardData(DWORD dwReserved
, LPDATAOBJECT *ppIDataObj)
{
*ppIDataObj=m_pObj->m_pDoc->TransferObjectCreate(FALSE);
return (NULL!=*ppIDataObj) ? NOERROR : ResultFromScode(E_FAIL);
}


STDMETHODIMP CImpIOleObject::DoVerb(LONG iVerb, LPMSG pMSG
, LPOLECLIENTSITE pActiveSite, LONG lIndex, HWND hWndParent
, LPCRECT pRectPos)
{
HWND hWnd, hWndT;

//Find the uppermost window.
hWndT=GetParent(m_pObj->m_pDoc->Window());

while (NULL!=hWndT)
{
hWnd=hWndT;
hWndT=GetParent(hWndT);
} switch (iVerb)
{
case OLEIVERB_HIDE:
ShowWindow(hWnd, SW_HIDE);
m_pObj->SendAdvise(OBJECTCODE_HIDEWINDOW);
break;

case OLEIVERB_PRIMARY:
case OLEIVERB_OPEN:
case OLEIVERB_SHOW:
ShowWindow(hWnd, SW_SHOW);
SetForegroundWindow(hWnd);
SetFocus(hWnd);

m_pObj->SendAdvise(OBJECTCODE_SHOWOBJECT);
m_pObj->SendAdvise(OBJECTCODE_SHOWWINDOW);
break;

default:
return ResultFromScode(OLEOBJ_S_INVALIDVERB);
}

return NOERROR;
}


STDMETHODIMP CImpIOleObject::EnumVerbs(LPENUMOLEVERB *ppEnum)
{
return ResultFromScode(OLE_S_USEREG);
}

STDMETHODIMP CImpIOleObject::Update(void)
{
//We're always updated since we don't contain.
return NOERROR;
}

STDMETHODIMP CImpIOleObject::IsUpToDate(void)
{
//We're always updated since we don't contain.
return NOERROR;
}

STDMETHODIMP CImpIOleObject::GetUserClassID(LPCLSID pClsID)
{
*pClsID=m_pObj->m_clsID;
return NOERROR;
} STDMETHODIMP CImpIOleObject::GetUserType(DWORD dwForm
, LPOLESTR *ppszType)
{
return ResultFromScode(OLE_S_USEREG);
}


STDMETHODIMP CImpIOleObject::SetExtent(DWORD dwAspect, LPSIZEL pszl)
{
RECT rc;
SIZEL szl;

if (!(DVASPECT_CONTENT & dwAspect))
return ResultFromScode(E_FAIL);

XformSizeInHimetricToPixels(NULL, pszl, &szl);

//This resizes the window to match the container's size.
SetRect(&rc, 0, 0, (int)szl.cx, (int)szl.cy);
m_pObj->m_pPL->SizeSet(&rc, TRUE);

return NOERROR;
}

STDMETHODIMP CImpIOleObject::GetExtent(DWORD dwAspect, LPSIZEL pszl)
{
RECT rc;
SIZEL szl;

if (!(DVASPECT_CONTENT & dwAspect))
return ResultFromScode(E_FAIL);

m_pObj->m_pPL->RectGet(&rc);
szl.cx=rc.right-rc.left;
szl.cy=rc.bottom-rc.top;

XformSizeInPixelsToHimetric(NULL, &szl, pszl);
return NOERROR;
} STDMETHODIMP CImpIOleObject::Advise(LPADVISESINK pIAdviseSink
, LPDWORD pdwConn)
{
if (NULL==m_pObj->m_pIOleAdviseHolder)
{
HRESULT hr;

hr=CreateOleAdviseHolder(&m_pObj->m_pIOleAdviseHolder);

if (FAILED(hr))
return hr;
}

return m_pObj->m_pIOleAdviseHolder->Advise(pIAdviseSink
, pdwConn);
}

STDMETHODIMP CImpIOleObject::Unadvise(DWORD dwConn)
{
if (NULL!=m_pObj->m_pIOleAdviseHolder)
return m_pObj->m_pIOleAdviseHolder->Unadvise(dwConn);

return ResultFromScode(E_FAIL);
}

STDMETHODIMP CImpIOleObject::EnumAdvise(LPENUMSTATDATA *ppEnum)
{
if (NULL!=m_pObj->m_pIOleAdviseHolder)
return m_pObj->m_pIOleAdviseHolder->EnumAdvise(ppEnum);

return ResultFromScode(E_FAIL);
}


STDMETHODIMP CImpIOleObject::GetMiscStatus(DWORD dwAspect
, LPDWORD pdwStatus)
{
return ResultFromScode(OLE_S_USEREG);
}

STDMETHODIMP CImpIOleObject::SetColorScheme(LPLOGPALETTE pLP)
{
return ResultFromScode(E_NOTIMPL);
}

Listing 18-1

Cosmo's implementation of IOleObject.

Trivial Functions

Let's look at the simple implementations first because we'll need some of the information from these functions to implement the more complex ones. The functions in this set are SetClientSite, GetClientSite, Update, IsUpToDate, GetExtent, GetUserClassID, and the triumvirate Advise, Unadvise, and EnumAdvise.

SetClientSite is the only way through which the embedded object gets an IOleClientSite pointer to the container's site object. You must hold on to this pointer for the lifetime of your object, saving it in a variable and calling AddRef on it, of course. In the rare case that your object receives multiple calls to SetClientSite, release whatever pointer you are currently holding before overwriting it with the new one.

GetClientSite is the direct sibling of SetClientSite. It simply needs to copy the last IOleClientSite pointer seen in SetClientSite to the out-parameter **ppSite. GetClientSite is a function that returns a new copy of a pointer, so be sure to call AddRef on the IOleClientSite pointer again.

Update and IsUpToDate are a pair of functions that a container can use to be sure that the presentation in its cache matches the current state of the object. IsUpToDate asks, "Are you current?" whereas Update tells your application, "Make yourself current." I mentioned before that embedded objects are always up-to-date unless they actually contain other objects themselves (especially links). This is because singular embedded objects always call IAdviseSink::OnViewChange when a change occurs, so the cache and the container are visibly updated. If the object is a container itself, however, it has to recursively call Update or IsUpToDate on its contained objects to implement these members.

GetExtent asks the object, "How big is this aspect?" by asking the object to fill a SIZEL structure with the horizontal and vertical dimensions of the object in HIMETRIC units that are sensitive to the requested aspect. These extents are in absolute units—that is, the vertical value is not negative, as it would be if you were dealing in the MM_HIMETRIC mapping mode. Because no hDC is anywhere in sight, there is no conception of a mapping mode in this function. Cosmo implements a mapping mode by retrieving the rectangle of the current Polyline window (in pixels) and using the helper function XformSizeInPixelsToHimetric (which is in INOLE.DLL, file INOLE\XFORM.CPP) to convert the values before returning.

GetUserClassID has to return the emulated CLSID—just as IPersistStorage::GetClassID does—if the present object is being used in this capacity. In Cosmo, IPersistStorage::Load stores the emulated CLSID in CFigure::m_clsID, so that's what we return here. In emulation scenarios, the CLSID that users think they are working with is not the CLSID of your object itself. This means GetUserClassID gives the container (and OLE) a way of knowing what the object actually is.

You can implement the three advise functions using an advise holder created with CreateOleAdviseHolder. This advise holder implements the interface IOleAdviseHolder, through which we can multicast the OnSave, OnRename, and OnClose calls to IAdviseSink interfaces. We create the advise holder in Advise and release it in the CFigure destructor. Advise, Unadvise, and EnumAdvise then delegate to this holder. When we need to send notifications, we call members such as IOleAdviseHolder::SendOnSave and others, as we'll see later.

Required Functions

In this set, we find DoVerb, Close, and SetHostNames, the three most important members (in that order) of IOleObject. DoVerb asks an object to execute one of its verbs. If DoVerb didn't exist, activation wouldn't exist, so it is really the crux of OLE Documents. The whole process of in-place activation begins with this function, as we'll see in Chapters 22 and 23.

As discussed in Chapter 17, the DoVerb function takes an object from the loaded or running state to the active state, or from the active state to the running state. This function receives a number of arguments, the first of which, iVerb, is the number of the verb to execute. This will be either OLEIVERB_PRIMARY (value 0), one of the object's custom verbs as selected from the verb menu created for this object in the container (a positive value), or one of the following standard verbs:3

OLEIVERB_SHOW (-1)

"Make the object visible." The object calls ShowWindow(hWnd, SW_SHOW), in which hWnd is the topmost window that is necessary to show to make this object visible. After ShowWindow, the object calls SetFocus(hWnd) followed by calls to IOleClientSite::ShowObject and IOleClientSite::OnShowWindow(TRUE). IOleClientSite is the client site returned from SetClientSite.

OLEIVERB_OPEN (-2)

"Open the object for editing." Outside of in-place activation, this has the same semantics as OLEIVERB_SHOW.

OLEIVERB_HIDE (-3)

"Hide the object." The object calls ShowWindow(hWnd, SW_HIDE), in which hWnd is the topmost window that is suitable for hiding the object's user interface, such as the main application window (for a single-object server) or a document window (when multiple objects or documents are open). The object also calls IOleClientSite::OnShowWindow(FALSE). IOleClientSite is the client site returned from SetClientSite.


All embedded objects should support at least these three verbs along with OLEIVERB_PRIMARY. Any other unsupported verb should cause DoVerb to return OLE_E_INVALIDVERB. The exact meaning of any positive verb, as well as OLEIVERB_PRIMARY, is something known only to the object and the end user. Cosmo's single (and primary) verb is Edit. Invoking Edit shows Cosmo's window, as with OLEIVERB_SHOW and OLEIVERB_OPEN. Another type of object, for example a sound object that has a Play verb, would only play the sound and not actually show any windows, nor would it call anything in IOleClientSite.

The other arguments to DoVerb provide the object with information that it can use to modify its behavior. The argument lpMsg tells the object what message (for example, WM_LBUTTONDBLCLK) actually caused the DoVerb call from the container. This information is important mostly for a type of in-place object called inside-out, which is covered in Chapters 22 and 23. The IOleClientSite pointer pActiveSite is used in other special cases that are not yet important in our discussion. The argument lindex is always 0 and is reserved for future use. Finally, hWndParent and pRectPos are useful to objects such as video clips, which temporarily play in the context of the container without having to implement full in-place activation. An object is allowed to temporarily create a window inside hWndParent in which to play or to call GetDC(hWndParent). It is also allowed to draw directly onto the container's window. The pRectPos parameter provides you with the position of your object (that is, the container's site) in hWndParent. Although you can create a window that is larger than this rectangle, you should never draw outside the rectangle on the container's hDC. Also, it is important to know that anything you do with these two parameters must be done entirely within DoVerb. If you need any other type of in-place capabilities, you need to implement in-place activation.

The next most important member of IOleObject is Close, which moves an object from the running or active state to the loaded state. Close is also called either when the container closes the compound document that contains the object or when the user deletes the object from that document altogether. In any case, the object generally closes its user interface, calls IOleClientSite::OnShowWindow(FALSE), and starts server shutdown as necessary.

I said that the object "generally" does these things because the dwSaveOption argument can modify this behavior:

Value

Description

OLECLOSE_SAVEIFDIRTY

If the object is dirty, it should save itself before closing.

OLECLOSE_NOSAVE

Close the object without saving.

OLECLOSE_PROMPTSAVE

Display a message box with a message something like, "This object has been changed. Do you want to update <container document> before closing?"* and Yes, No, and Cancel buttons. If the user chooses Yes, save the object and close. If the user chooses No, simply close. On Cancel, return OLE_E_PROMPTSAVECANCELLED without doing anything else. The <container document> string is obtained through IOleObject::SetHostNames, as we'll see shortly.


* I could find no standard for this message, so I'm making an educated guess based on an old OLE 1 standard.

The process of saving, when necessary, has two steps:

If the object has been modified, call IOleClientSite::SaveObject using the most recent pointer seen in SetClientSite. In Cosmo, this is accomplished by calling CFigure::SendAdvise with OBJECTCODE_SAVEDOBJECT.

Call IAdviseSink::OnSave through your advise holder's IOleAdviseHolder::SendOnSave. Again, Cosmo handles this through CFigure::SendAdvise with OBJECTCODE_SAVED.

The final required function in IOleObject with a sizable implementation is SetHostNames. This function informs the object that it's being embedded in a container and is a signal to the server to show the appropriate user interface for embedding. At this point in our discussion, it's not necessary to fully implement this function, so we'll come back to it later in this chapter in "Modify the Server's User Interface."

Optional Functions

The four functions in this group—SetExtent, InitFromData, GetClipboardData, and SetColorScheme—are not required for standard operation of compound documents, so you can implement them as suits your fancy.

SetExtent adds a nice touch to the interaction between a container and a server, so I do recommend that you implement it. A container will call this function when it resizes an object in one of its documents if your object is running or if you have marked it with OLEMISC_RECOMPOSEONRESIZE. Handling SetExtent ensures that your object always looks good in the container—no matter what the scale—because you can render the object as appropriate for that scale. When the object is active in its own user interface, this call can also be used to reduce or enlarge the object in its editing window as the user resizes it in the container. Cosmo, for example, scales the Polyline window (and the document window in which it lives) in such a way that it's as close to the size of the container's site as possible (and within reason). SetExtent works best for graphical objects; it does not work as well for text or table objects for which scale is much less important than the textual or numeric data. SetExtent is also sensitive to the display aspect that is passed in the dwAspect parameter.

InitFromData allows a container to either paste into your object directly or provide initial data during creation. This function is passed an IDataObject pointer, which you can use to retrieve the data, and the flag fCreation, which indicates the scenario in which this function is being called. If fCreation is FALSE, you should integrate the data in the data object with your current data, as if Edit Paste had been performed in the server itself. If fCreation is TRUE, the container is attempting to create a new instance of your object on the basis of a selection in the container that is described by the data object. Cosmo happens to treat both cases identically by passing the data object to CCosmoDoc::PasteFromData. Again, I highly recommend that you make a function that pastes data from any arbitrary data object, such as those you can get from the clipboard, from drag and drop, or from a function such as InitFromData.

GetClipboardData, on the other hand, asks the object for an IDataObject pointer that is identical to the one the server would place on the clipboard if the user performed an Edit Copy operation. This allows a caller to get a snapshot of the object—as opposed to the IDataObject interface on the object itself—that always reflects the most recent data. If you implement this function, you have to return an IDataObject pointer for an object whose data will not change. Cosmo has the handy function CCosmoDoc::TransferObjectCreate, which does the job for us.

Finally, SetColorScheme provides the object with the container's recommended palette. The object might choose to ignore this without any dire consequences, but if you can, try to use the colors provided. Now, the colors provided in the LOGPALETTE structure are not actually colors in a GDI palette. The first palette entry in the structure actually specifies the foreground color recommended by the container. The second palette entry specifies the background color. The first half of the remaining palette entries are fill colors, and the second half are colors for the lines and text. Container applications typically specify an even number of palette entries. When there is an uneven number of entries, the server should round up to the fill colors. In other words, if there are five entries, the first three should be interpreted as fill colors and the last two as line and text colors.

But It Still Doesn't Work

After implementing and compiling this mammoth interface, you have most of the server side complete. Now, when you use Insert Object from a container, it will launch your application, obtain your class factory, create an object, and fire off calls such as IPersistStorage::InitNew, IOleObject::SetClientSite, IOleObject::GetExtent, IOleObject::SetHostNames, and IOleObject::DoVerb. Your window will appear ready for editing. If you have implemented IOleObject::Close along with a call to IOleClientSite::SaveObject, you see a call to IPersistStorage::Save when you close your application. When you activate the object from the container (with a quick double click), you see a call to IPersistStorage::Load, followed by the same calls to IOleObject as before. Your application should at this point be visible again, with the previously saved data ready for editing.

The container, however, has no presentation, or whatever presentation there is is not being updated as it should be when you make changes to the object. In addition, when you close your application, you probably get a prompt that says, "Document has changed; do you want to save?" If you answer "Yes," you get a File Save dialog box. Well, that's not part of the OLE user interface for embedded object servers. In fact, there's nothing else to tell you that you are working with an embedded object (as opposed to an untitled file). To solve both these problems, we have to modify the server to show a user interface that is appropriate for an embedded object and to complete the notifications we send to the container.

3 We'll see other standard verbs in the context of in-place activation.