Implement IViewObject2

Now comes one of the most frequent uses for object handlers: optimizing drawing for particular devices. Your implementation of IPersistStorage loads the data to draw, and your IViewObject2 handles all the drawing and other visual aspects. If you implement IViewObject2::Draw for any given aspect, you also need to implement Freeze, Unfreeze, SetAdvise, and GetAdvise for the same aspect. This makes IViewObject2 the most complicated interface in your handler, as you can see from the amount of code shown in Listing 19-1.

IVIEWOBJ.CPP


[Constructor, destructor, IUnknown members omitted]

STDMETHODIMP CImpIViewObject2::Draw(DWORD dwAspect, LONG lindex
, void *pvAspect, DVTARGETDEVICE *ptd, HDC hICDev
, HDC hDC, LPCRECTL pRectBounds, LPCRECTL pRectWBounds
, BOOL (CALLBACK * pfnContinue) (DWORD), DWORD dwContinue)
{
RECT rc;
POLYLINEDATA pl;
PPOLYLINEDATA ppl=&m_pObj->m_pl;

RECTFROMRECTL(rc, *pRectBounds);

//Delegate iconic and printed representations.
if (!((DVASPECT_CONTENT œ DVASPECT_THUMBNAIL) & dwAspect))
{
return m_pObj->m_pDefIViewObject2->Draw(dwAspect
, lindex, pvAspect, ptd, hICDev, hDC, pRectBounds
, pRectWBounds, pfnContinue, dwContinue);
}

/*
* If we're asked to draw a frozen aspect, use data from
* a copy we made in IViewObject2::Freeze. Otherwise, use
* current data.
*/ if (dwAspect & m_pObj->m_dwFrozenAspects)
{
//Point to data to actually use.
if (DVASPECT_CONTENT==dwAspect)
ppl=&m_pObj->m_plContent;
else
ppl=&m_pObj->m_plThumbnail;
}

//Make copy so we can modify it.
memcpy(&pl, ppl, CBPOLYLINEDATA);

/*
* If we're going to a printer, check if it's color capable.
* If not, use black on white for this figure.
*/
if (NULL!=hICDev)
{
if (GetDeviceCaps(hICDev, NUMCOLORS) <= 2)
{
pl.rgbBackground=RGB(255, 255, 255);
pl.rgbLine=RGB(0, 0, 0);
}
}

m_pObj->Draw(hDC, &rc, dwAspect, ptd, hICDev, &pl);
return NOERROR;
}


STDMETHODIMP CImpIViewObject2::GetColorSet(DWORD dwDrawAspect
, LONG lindex, LPVOID pvAspect, DVTARGETDEVICE *ptd
, HDC hICDev, LPLOGPALETTE *ppColorSet)
{
return ResultFromScode(S_FALSE);
}


STDMETHODIMP CImpIViewObject2::Freeze(DWORD dwAspect, LONG lindex
, LPVOID pvAspect, LPDWORD pdwFreeze)
{
//Delegate any aspect we don't handle. if (!((DVASPECT_CONTENT œ DVASPECT_THUMBNAIL) & dwAspect))
{
return m_pObj->m_pDefIViewObject2->Freeze(dwAspect, lindex
, pvAspect, pdwFreeze);
}

if (dwAspect & m_pObj->m_dwFrozenAspects)
{
*pdwFreeze=dwAspect + FREEZE_KEY_OFFSET;
return ResultFromScode(VIEW_S_ALREADY_FROZEN);
}

m_pObj->m_dwFrozenAspects œ= dwAspect;

/*
* For whatever aspects become frozen, make a copy of the
* data. Later, when drawing, if such a frozen aspect is
* requested, we'll draw from this data rather than from
* our current data.
*/
if (DVASPECT_CONTENT & dwAspect)
{
memcpy(&m_pObj->m_plContent, &m_pObj->m_pl
, CBPOLYLINEDATA);
}

if (DVASPECT_THUMBNAIL & dwAspect)
{
memcpy(&m_pObj->m_plThumbnail, &m_pObj->m_pl
, CBPOLYLINEDATA);
}

if (NULL!=pdwFreeze)
*pdwFreeze=dwAspect + FREEZE_KEY_OFFSET;

return NOERROR;
}


STDMETHODIMP CImpIViewObject2::Unfreeze(DWORD dwFreeze)
{
DWORD dwAspect=dwFreeze - FREEZE_KEY_OFFSET; //Delegate any aspect we don't handle.
if (!((DVASPECT_CONTENT œ DVASPECT_THUMBNAIL) & dwAspect))
return m_pObj->m_pDefIViewObject2->Unfreeze(dwFreeze);

//Aspect to unfreeze is in key.
m_pObj->m_dwFrozenAspects &= ~(dwAspect);

/*
* Since we always kept our current data up-to-date, we don't
* have to do anything here such as requesting data again.
* Because we removed dwAspect from m_dwFrozenAspects, Draw
* will again use current data.
*/

return NOERROR;
}


STDMETHODIMP CImpIViewObject2::SetAdvise(DWORD dwAspects
, DWORD dwAdvf, LPADVISESINK pIAdviseSink)
{
//Pass on through anything we don't support.
if (!((DVASPECT_CONTENT œ DVASPECT_THUMBNAIL) & dwAspects))
{
return m_pObj->m_pDefIViewObject2->SetAdvise(dwAspects
, dwAdvf, pIAdviseSink);
}

if (NULL!=m_pObj->m_pIAdvSinkView)
m_pObj->m_pIAdvSinkView->Release();

m_pObj->m_dwAdviseAspects=dwAspects;
m_pObj->m_dwAdviseFlags=dwAdvf;

m_pObj->m_pIAdvSinkView=pIAdviseSink;

if (NULL!=m_pObj->m_pIAdvSinkView)
m_pObj->m_pIAdvSinkView->AddRef();

return NOERROR;
} STDMETHODIMP CImpIViewObject2::GetAdvise(LPDWORD pdwAspects
, LPDWORD pdwAdvf, LPADVISESINK *ppAdvSink)
{
if (NULL==m_pObj->m_pIAdvSinkView)
{
return m_pObj->m_pDefIViewObject2->GetAdvise(pdwAspects
, pdwAdvf, ppAdvSink);
}

if (NULL==ppAdvSink)
return ResultFromScode(E_INVALIDARG);
else
{
*ppAdvSink=m_pObj->m_pIAdvSinkView;
m_pObj->m_pIAdvSinkView->AddRef();
}

if (NULL!=pdwAspects)
*pdwAspects=m_pObj->m_dwAdviseAspects;

if (NULL!=pdwAdvf)
*pdwAdvf=m_pObj->m_dwAdviseFlags;

return NOERROR;
}


STDMETHODIMP CImpIViewObject2::GetExtent(DWORD dwAspect
, LONG lindex, DVTARGETDEVICE *ptd, LPSIZEL pszl)
{
HDC hDC;
int iXppli, iYppli;
RECT rc;

/*
* We can answer for CONTENT/THUMBNAIL, but try server for
* others. In addition, always delegate if server is running
* since it has a window to define the size.
*/
if (!((DVASPECT_CONTENT œ DVASPECT_THUMBNAIL) & dwAspect)
œœ OleIsRunning(m_pObj->m_pDefIOleObject))
return m_pObj->m_pDefIOleObject->GetExtent(dwAspect, pszl); /*
* The size is in rc field of POLYLINEDATA structure,
* which we now have to convert to HIMETRIC.
*/

hDC=GetDC(NULL);
iXppli=GetDeviceCaps(hDC, LOGPIXELSX);
iYppli=GetDeviceCaps(hDC, LOGPIXELSY);

RECTSTORECT(m_pObj->m_pl.rc, rc);
pszl->cx=(long)MulDiv(HIMETRIC_PER_INCH
, (rc.right-rc.left), iXppli);

pszl->cy=(long)MulDiv(HIMETRIC_PER_INCH
, (rc.bottom-rc.top), iYppli);

ReleaseDC(NULL, hDC);
return NOERROR;
}

Listing 19-1

Implementation of the IViewObject2 interface in HCosmo.

Let's look at the simpler member functions before we jump into Draw. First, GetColorSet is unimportant for this handler (and for Cosmo as well), so we simply return S_FALSE to say we have nothing. Next, SetAdvise and GetAdvise handle a container's IAdviseSink to which we must send OnViewChange notifications when any data change occurs in our object that requires us to repaint. We delegate both of these calls to the default handler for DVASPECT_ICON and DVASPECT_DOCPRINT because we rely on the cache to handle those aspects for us in all other parts of the handler. That leaves us in SetAdvise to save the advise aspects, the flags, and the IAdviseSink pointer, to which we'll later send notifications, as described in "Synchronized Swimming with Your Local Server" later in this chapter. We need to hold all of these arguments so that we can return them through GetAdvise, as shown in the listing. Remember to call AddRef on the IAdviseSink pointer when you save a copy in SetAdvise (and release it before overwriting it) as well as to call AddRef on the pointer when returning a copy from GetAdvise.

Note: A container can call SetAdvise with a NULL IAdviseSink pointer. This means that the container is terminating the connection. Be sure you don't attempt to call AddRef on this pointer without checking for NULL.

Freeze and Unfreeze are a pair of functions that the container uses to control when a presentation is allowed to change. A change in the presentation, however, does not mean that you freeze underlying data; a freeze affects only one aspect. In HCosmo's case, a freeze on DVASPECT_CONTENT cannot freeze the data because DVASPECT_THUMBNAIL is drawn from the same data, and it is not frozen. Therefore, we must make a snapshot of the frozen data so that when we're asked to draw that aspect, we use the frozen copy instead of the current data. This allows the current data to change as needed. This also allows IPersistStorage::Save to write the current data without having to consider a frozen view aspect, which should not affect storage in any way.

Your implementation of Freeze must somehow remember that the aspect is frozen and make a snapshot of the data. HCosmo's code performs a bitwise OR operation to add a new aspect to the list of currently frozen aspects in CFigure::m_dwFrozenAspects. We then take a snapshot of the current data, putting it in either CFigure's m_plContent or m_plThumbnail structure, depending on the aspect. Draw will later use all of this to determine exactly which data to use. In addition, Freeze must return some sort of key that can be passed later to Unfreeze. A good key is the aspect plus some random number to make the number meaningless to the caller. For example, I use FREEZE_KEY_OFFSET, which I define in HCOSMO.H as 0x0723.2 When this key is later passed to Unfreeze, we subtract the offset to yield an aspect and remove that aspect from m_dwFrozenAspects. When Draw is called subsequently, we see that the aspect is not frozen and will therefore draw from the current data. (We can assume Draw will be called because a container that thaws a view object will generally want to update immediately.)

Also, when working with Freeze you should first check to see whether the requested aspect is already frozen and return VIEW_S_ALREADY_FROZEN if it is. It must still return a key, however, which the caller can later pass to Unfreeze.

In the GetExtent member of IViewObject2, we delegate to the default handler for those aspects we do not support directly. In addition, we always delegate if the local object is running because that local object (we know) has a window to define its size accurately. Otherwise, we calculate the object's extents in HIMETRIC units and return.

This brings us to Draw, which generally calls either CFigure::Draw (FIGURE.CPP) for DVASPECT_CONTENT and DVASPECT_THUMBNAIL or the default handler's IViewObject2::Draw for any other aspect. The latter check happens first, so we can ignore those cases.

For content and thumbnail aspects, we now have to see whether they are frozen. If they are, we use the data we copied in Freeze instead of the current data. In Listing 19-1, Draw sets the pointer ppl initially to the current data. Later, if the aspect is frozen, Draw points ppl instead to the snapshot of that aspect. In this way, Draw can copy whatever ppl points to into a temporary POLYLINEDATA structure so that we can now do a few device optimizations. HCosmo doesn't do anything fancy with devices, but it does provide an example of how to render differently for a printer and for the screen. If the hICDev parameter in Draw is non-NULL, that means the data is going to a device other than the screen. If that is true, Draw checks for the number of colors the device supports. If it's a black-and-white device with only two colors, we force the background color of the rendering to be white and the line color to be black, which avoids potentially ugly dithering or large black blocks on the printer. (I can't say that this is the best thing to do, but it's just an example.)

IViewObject2::Draw then calls CFigure::Draw with the temporary POLYLINEDATA structure, which might be a frozen aspect or data modified for a printer. In any case, CFigure::Draw draws the image with a bunch of GDI calls. While we don't need to see all the code here, I do want to point out one bug I encountered when writing it. This code was originally taken from the full Cosmo's WM_PAINT handling of the Polyline window. Because Polyline was always in its own window, its client area always started at (0,0). This meant that I did not have the code in place to handle cases in which the upper left corner was not (0,0). When I first compiled HCosmo with this IViewObject2 implementation, it continually drew in the upper left corner of the container instead of in the container's site. Not good. I had to be sure that HCosmo's implementation of the drawing code would work for any rectangle.

After implementing IViewObject2, you now have a handler that can load, save, and display or print your object's data to a device without requiring a local server. After you have debugged your drawing code, you might want to create a compound document and copy it to another machine. Then you can open it again with and without your handler installed in the registry. This will give you an indication of what will happen in the absence of both a handler and a local server.

2 If you can figure out where I got this number, I congratulate you on your resourcefulness. But don't expect any prizes; its meaning is entirely personal.