Chapter 32
Advanced Programming with ATL
In preceding chapters, you learned how to use the Active Template Library
(ATL) in your applications and ActiveX controls. Now it's time to explore
some advanced features of ATL that you can use to extend and enhance your
controls? usefulness. In addition to familiar features like Clipboard and
Drag and Drop support, you will learn how to implement asynchronous properties
and optimized drawing, which are key parts of the OLE Control 96 (OC 96)
specification.
-
Adding asynchronous properties
-
Supporting property enumeration
-
Enabling optimized drawing
-
Providing Clipboard and Drag-and-Drop support
-
Using Advanced ActiveX features in ATL
The complete source code and executable file for the ATLControlWin control
is located in the Chap31\ATLControlWin folder on the book's CD-ROM.

Adding Asynchronous Properties
In Chapter 31, ?Creating ActiveX Controls with ATL,? you learned how to
use ambient, stock, and user-defined properties in your ATL ActiveX controls.
In addition to these three types, ATL enables you to use asynchronous
properties in your controls.
Asynchronous properties are properties in which the data is loaded in a
background process or thread. Properties that typically contain a large
amount of data, such as bitmapped images or sounds, can be made asynchronous
so as not to interfere with the normal processing of the control and the
container. When an asynchronous property is loaded, the control can still
respond to user interface events, and the user can stop the data loading.
For example, a control that downloads and parses a file of stock quotes
from an Internet server can load the data asynchronously. When the control
needs to load data, it can begin the download and register a callback function
to be called when the transfer is complete. The callback function is called
to signal that the property's data has been retrieved and that it can be
used normally. Your controls can process the incoming data as it arrives,
or they can wait until all data has arrived before doing anything with
it.
These callbacks are implemented with OLE's IBindStatusCallback interface
and the corresponding CBindStatusCallback class. In addition, your control
should support the ReadyState property, which is used by containers to
tell what state the control is in. To properly support ReadyState, you
also need to add the ReadyStateChange event, which is used by the control
to notify the container that the value of the control's ReadyState has
changed.
Adding the ReadyState Property
You add the ReadyState property just like any other property, as described
in Chapter 31:
-
From the ClassView tab in the Project Workspace window, select the
IATLControlWin interface.
-
Click the right mouse button, and select the Add Property menu item.
-
In the Add Property to Interface dialog box, set the Property Type
to long and the Property Name to ReadyState.
-
Uncheck the Put Function check box, and leave the remainder of the
settings at their default values (see Figure 32.1).
-
Click OK to confirm the entry and close the dialog box.
Add the ReadyState property to the control.
FIG. 32.1
ReadyState is a stock property, even though it doesn't appear in the ATL
Object Wizard stock property dialog box. Because you added it as a user-defined
property, you must change its dispid. Before you proceed, open the ATLControl.idl
file, and change the dispid of the ReadyState property to DISPID_READYSTATE.
Listing 32.1 shows the IDL file after editing.
Listing 32.1 ATLCONTROL.IDL?Changing the dispid of the ReadyState Property
to the Stock Property dispid?DISPID_READYSTATE
. . .
interface IATLControlWin : IDispatch
{
[id(1), helpstring("method CaptionMethod")]
HRESULT CaptionMethod(
[in] BSTR bstrCaption, [in, optional] VARIANT varAlignment,
[out, retval] long * lRetVal);
[propget, id(DISPID_READYSTATE), helpstring("property
ReadyState")]
HRESULT ReadyState([out, retval] long *pVal);
[propget, id(DISPID_BACKCOLOR), helpstring("property
BackColor")]
HRESULT BackColor([out, retval] OLE_COLOR *pVal);
[propput, id(DISPID_BACKCOLOR), helpstring("property
BackColor")]
HRESULT BackColor([in] OLE_COLOR newVal);
[propget, id(dispidCaptionProp), helpstring("property
CaptionProp")]
HRESULT CaptionProp([in, optional] VARIANT varAlignment,
[out, retval] BSTR *pVal);
[propput, id(dispidCaptionProp), helpstring("property
CaptionProp")]
HRESULT CaptionProp([in, optional] VARIANT varAlignment,
[in] BSTR newVal);
[propget, id(dispidAlignment), helpstring("property
Alignment")]
HRESULT Alignment([out, retval] long *pVal);
[propput, id(dispidAlignment), helpstring("property
Alignment")]
HRESULT Alignment([in] long newVal);
};
. . .
Of course, implementing the ReadyState property requires a member variable
to store ReadyState's value. Add the m_lReadyState member to the class
declaration of the CATLControlWin class, as shown in Listing 32.2.
Listing 32.2 ATLCONTROLWIN.H?Adding m_lReadyState to the CATLControlWin
Class Definition
int iCharWidthArray[256];
int iCharacterSpacing, iCharacterHeight;
// for the ReadyState property
long m_lReadyState;
};
Don't forget to initialize the m_lReadyState member variable in the constructor
of the CATLControlWin class; if you don't, the asynchronous transfers might
not progress as you'd expect!
The last step is to implement the get_ReadyState function, which simply
returns the current value of the m_lReadyState member variable, as shown
in Listing 32.3.
Listing 32.3 ATLCONTROLWIN.CPP?Implementing the get_ReadyState Function
so that It Returns the m_lReadyState Member
STDMETHODIMP CATLControlWin::get_ReadyState(long
* pVal)
{
// set the return value to the value of the member
variable
*pVal = m_lReadyState;
return S_OK;
}
Supporting the ReadyStateChange Event
Now that you've added the ReadyState property, you still need to provide
a notification hook to signal when ReadyState changes. You can do this
with the ReadyStateChange event. Open the ATLControl.idl file, and add
the ReadyStateChange function to the event interface, as shown in Listing
32.5.
Listing 32.4 ATLCONTROLWIN.IDL?Adding the ReadyStateChange Event to
the IDL File so that It Can Be Triggered
. . .
[
uuid(C31D4C71-7AD7-11d0-BEF6-00400538977D),
helpstring("ATLControlWin Event Interface")
]
dispinterface _DATLControlWin
{
properties:
methods:
[id(1)] void Change([in, out]BSTR * bstrCaption,
[in, out] long * lAlignment);
[id(DISPID_READYSTATECHANGE)] void ReadyStateChange();
};
. . .
Remember that ATL doesn't automatically support events? you must manually
rebuild the CPATLControl.h file that contains your connection point interface
definitions. In Chapter 31, you added support for the Change event. To
add support for ReadyStateChange, you must rebuild the file that defines
the connection point interfaces so that the connection points include both
events. To do this, follow these steps:
-
Compile the IDL file; you must do this first, because the event interface
header file is built from the type library.
-
From the Project menu, select the
Add to Project menu
item, and then select the Components and Controls submenu item.
-
In the Components and Controls Gallery dialog box, double-click the
Developer Studio Components folder, and then double-click the ATL Proxy
Generator icon. Click OK in the confirmation dialog that asks whether you
want to insert the Proxy Generator.
-
When the ATL Proxy Generator dialog box appears, click the ... button
to display the Open dialog box. Select the ATLControl.tlb file, and click
Open.
-
Select the DATLControlWin entry in the Not Selected list box, and click
the > button to move the entry to the
Selected list
box. Ensure that the Proxy Type is set to Connection Point and click Insert.
-
A Save dialog box will appear with the file CPATLControl.h in the File
name edit box. Click
Save to continue. Click Yes to replace
the existing CPATLControl.h file.
-
Click OK in the confirmation dialog box that indicates the operation
was successful.
-
Close the ATL Proxy Generator and Components and Controls Gallery dialog
boxes.
The Fire_ReadyStateChange method has now been added to the CProxy_DATLControlWin
class.
Asynchronous properties are based on URLs, not on the data type of the
data to be downloaded. The URL associated with each asynchronous property
should be stored in a string property of the control. For our control,
the property is called TextDataPath, and you need to add it to the control.
From the ClassView tab in the Project Workspace window, select the IATLControlWin
interface, click the right mouse button, and select the Add Property...
menu item.
In the Add Property to Interface dialog box, set the Property Type
to BSTR, the Property Name to TextDataPath, and leave
the remainder of the settings at their default values(see Figure 32.2).
Click OK to confirm the entry and close the dialog.
Add the TextDataPath property to the control by using the ATL ClassWizard.
FIG. 32.2
Because you added a new property, you should also add a new dispid constant,
dispidTextDataPath, to the PROPDISPIDS enumeration in the ATLControl.idl
file, and then update the TextDataPath function to use the constant value
(see Listing 32.5).
Listing 32.5 ATLCONTROL.IDL?Adding the dispidTextDataPath Enumeration
to the IDL File
. . .
typedef enum propdispids
{
dispidAlignment = 2,
dispidCaptionProp = 3,
dispidTextDataPath = 4,
}PROPDISPIDS;
. . .
interface IATLControlWin : IDispatch
{
[id(1), helpstring("method CaptionMethod")]
HRESULT CaptionMethod(
[in] BSTR bstrCaption, [in, optional] VARIANT varAlignment,
[out, retval] long * lRetVal);
[propget, id(dispidTextDataPath), helpstring("property
TextDataPath")]
HRESULT TextDataPath([out, retval] BSTR *pVal);
[propput, id(dispidTextDataPath), helpstring("property
TextDataPath")]
HRESULT TextDataPath([in] BSTR newVal);
. . .
The TextDataPath property is used to store the URL of the data that the
property represents. To complete your implementation of the property, add
a member variable named m_bstrTextDataPath to keep the URL available to
the class. This member is declared to be of type CComBSTR, which is a BSTR
wrapper class provided with ATL; see the ATL documentation for more information.
We?ve chosen to use CComBSTR to demonstrate the different implementation
styles available to you with ATL. Instead, you could use an LPTSTR or BSTR
without difficulty.
For this example, we're assuming our URL will always return a block of
text, so you need to add the m_bstrText member variable, also of type CComBSTR,
to store the data as it is downloaded. You'll see these two members used
in "Transferring the Data" later in the chapter; Listing 32.6
shows their declarations.
Listing 32.6 ATLCONTROLWIN.H?Adding Member Variables for the URL and
the Data that It Returns
. . .
//OnData will be used as a callback functin by the
CBindStatusCallback object.
//OnData will be called periodically with data from
the asynchronous transfer
void OnData(CBindStatusCallback<CATLControlWin>*
pbsc, BYTE* pBytes, DWORD dwSize);
protected:
. . .
// for the ReadyState property
long m_lReadyState;
// for the TextDataPath property
CComBSTR m_bstrTextDataPath;
// to hold the data as it is passed in
CComBSTR m_bstrText;
};
Transferring the Data
The get_TextDataPath and put_TextDataPath accessor functions are where
the asynchronous data transfer actually takes place, as shown in Listing
32.7. All get_TextDataPath does is returns the current value stored in
the m_bstrTextDataPath member variable.
The put_TextDataPath function stores the requested location of the data
and then initiates a transfer of the data to the control with a call to
CBindStatusCallback<CATLControlWin>::Download(). CBindStatusCallback
is an ATL wrapper class that wraps the IBindStatusCallback interface. In
turn, IBindStatusCallback provides the actual asynchronous transfer by
using the WinInet classes described in Chapter 24, "Programming Internet
Applications with WinInet."
CBindStatusCallback handles all of the details of the data transfer and
only requires that you implement a callback function, which we'll call
OnData. When you call Download, the ATL wrapper class links your callback
to the IBindStatusCallback interface and starts the download. Note that,
as with other types of callbacks, OnData must conform to a prototype; in
this case, it must be declared as a ATL_PDATAAVAILABLE function. See the
ATL documentation on the CBindStatusCallback interface for more information.
Listing 32.7 ATLCONTROLWIN.CPP?The TextDataPath Accessors Handle the
Asynchronous Transfer
STDMETHODIMP CATLControlWin::get_TextDataPath(BSTR
* pVal)
{
// return a copy of the member variable
*pVal = m_bstrTextDataPath.Copy();
return S_OK;
}
STDMETHODIMP CATLControlWin::put_TextDataPath(BSTR
newVal)
{
HRESULT hResult = S_OK;
// copy the new string to the member variable
m_bstrTextDataPath = newVal;
// clear the data buffer
m_bstrText = _T("");
// start the asynchronous download of the data
CBindStatusCallback<CATLControlWin>::Download(this,
OnData, m_bstrTextDataPath,
m_spClientSite, FALSE);
// let the container know that the property has changed
this->SetDirty(TRUE);
// this->SetModifiedFlag(); <== MFC version
return hResult;
}
OnData, shown in Listing 32.8, is a very basic implementation of the asynchronous
data transfer mechanism provided by the IBindStatusCallback interface and
the CBindStatusCallback class. When new data arrives, CBindStatusCallback
triggers OnData to handle it. In our case, OnData will append each new
block of data to the m_bstrText member variable, and then call CaptionMethod
to update the control's display.
Throughout the OnData implementation, note the use of the CBindStatusCallback
members to determine the status of the current call to OnData. Also note
the use of the ReadyState property to indicate to the container application
that an asynchronous download is taking place and when it has finished.
OnData fires events to advise the container of changes in its state.
Listing 32.8 ATLCONTROLWIN.CPP?Calling OnData when Data Arrives
//OnData will be used as a callback function by the
CBindStatusCallback object.
//OnData will be called periodically with data from
the asynchronous transfer
void CATLControlWin::OnData(CBindStatusCallback<CATLControlWin>*
pbsc, BYTE* pBytes,
DWORD dwSize)
{
// if we have not read any data yet
if(pbsc->m_dwTotalRead == 0)
{
// clear the buffer
m_bstrText = _T("");
// set the ready state of the control
m_lReadyState = READYSTATE_LOADING;
// let the container know that the property has changed
this->Fire_ReadyStateChange();
}
// add the data to our buffer
m_bstrText.Append((LPCSTR) pBytes);
long lRetVal;
VARIANT varAlignment;
// initialize the variant
::VariantInit(&varAlignment);
// defer to the CaptionMethod implementation
this->CaptionMethod(m_bstrText, varAlignment,
&lRetVal);
// if the function returned success
if(TRUE == lRetVal)
// let the control know that the property has changed
this->SetDirty(TRUE);
// this->SetModifiedFlag(); <== MFC version
// if there is nothing left
if(pbsc->m_dwAvailableToRead == 0)
{
// set the ready state of the control
m_lReadyState = READYSTATE_COMPLETE;
// let the container know that the property has changed
this->Fire_ReadyStateChange();
}
}
Making the Property Persistent
The crowning touch in your asynchronous property implementation is to add
the TextDataPath property to the property map so that it will be persistent.
The updated property map is shown in Listing 32.9. Do not add the ReadyState
property to the persistence because its value is not valid across execution
lifetimes.
Listing 32.9 ATLCONTROLWIN.H?Adding TextDataPath to the Property Map
to Make It Persistent
. . .
BEGIN_PROPERTY_MAP(CATLControlWin)
// PROP_ENTRY("Description", dispid, clsid)
PROP_ENTRY("TextDataPath", dispidTextDataPath,
CLSID_ATLControlWinPPG)
PROP_ENTRY("Alignment", dispidAlignment,
CLSID_ATLControlWinPPG)
PROP_ENTRY("BackColor", DISPID_BACKCOLOR,
CLSID_ATLControlWinPPG)
PROP_PAGE(CLSID_CColorPropPage)
END_PROPERTY_MAP()
. . .
Static and Dynamic Property Enumeration
Property enumeration restricts a property's values to a specific set of
values. For example, a control that supports a property called TextAligment
might use values of 0, 1, and 2 for left-, center-, and right-justified
text, or it could use enumerated values named LeftJustified, RightJustified,
and CenterJustified. Another example is a property used to determine what
languages a control supports. A language-based property might be a good
candidate for both a static set, say English and German, and a dynamic
set, say for all the languages available on a particular machine.
Property enumeration makes it easier for script writers to drive your control
because they can write natural-sounding expressions like theText.Justification
= CenterJustified instead of relying on meaningless integer values.
You can create property enumerations in two ways. The simplest way is to
use static enumeration, where the enumerations are defined in the
control?s ODL or IDL file. As soon as the control is compiled, static enumerations
stay fixed. The ATLControlWin version in Chapter 31 used this approach.
The alternative, dynamic enumeration, is a little more complicated,
but it enables you to change which enumerated values you support at runtime.
Supporting Dynamic Property Enumeration
Dynamic property enumeration is based on the interface IPerPropertyBrowsing,
which provides a standardized way for containers to get lists of available
enumerated values. The ATL Object Wizard won't add this interface to your
control class automatically, so you'll have to add it yourself. The first
step is to add the IPerPropertyBrowsingImpl class to your control inheritance
structure:
public IPerPropertyBrowsingImpl<CATLControlWin>
Next, add the IPerPropertyBrowsing interface to the COM interface map:
COM_INTERFACE_ENTRY_IMPL(IPerPropertyBrowsing)
The final step is to implement the actual IPerPropertyBrowsing interface
functions. First, add the function prototypes to the class declaration,
as shown in Listing 32.10.
Listing 32.10 ATLCONTROLWIN.H?Four Functions that Implement the IPerPropertyBrowsing
Interface on Your Control
. . .
STDMETHOD(MapPropertyToPage)(DISPID dispID, CLSID
*pClsid);
STDMETHOD(GetPredefinedStrings)(DISPID dispID, CALPOLESTR
*pCaStringsOut,
CADWORD *pCaCookiesOut);
STDMETHOD(GetPredefinedValue)(DISPID dispID, DWORD
dwCookie, VARIANT* pVarOut);
STDMETHOD(GetDisplayString)(DISPID dispID,BSTR *pBstr);
. . .
Getting the Right Property Page
MapPropertyToPage (see Listing 32.11) is used to link a property to a specific
control or system-defined property page. In this case, you return E_NOTIMPL
if the requested dispid matches that of the Alignment property. By returning
E_NOTIMPL, you are preventing the container application from displaying
the property page associated with the property; instead, the container
will use the property enumeration that you have implemented. The connection
between the property and the property page is made in the property map
macro in the class declaration. Remember that one of the parameters in
the macro was the CLSID of the property page for the property.
Listing 32.11 ATLCONTROLWIN.CPP?Using MapPropertyToPage to Handle Translation
Between a Requested Property and the Appropriate Page
STDMETHODIMP CATLControlWin::MapPropertyToPage(DISPID
dispID, CLSID *pClsid)
{
// if this is the dispid property
if(dispID == dispidAlignment)
// defer to the property enumeration and not the
property page
return E_NOTIMPL;
else
// defer to the base class implementation
return IPerPropertyBrowsingImpl<CATLControlWin>::
MapPropertyToPage(dispID, pClsid);
}
Getting the Enumerations' String Values
GetPredefinedStrings is the first function to be called (see Listing 32.12).
When this method is called, the dispid of the property that is currently
being referenced will be passed in. This method is called for all properties
that the control supports, so take care when adding code. If the function
is called with a dispid that the control owns, the control is required
to create an array of strings and cookies. A cookie is just a 32-bit
value that has some meaning to the control implementation. In this case,
the cookies that you supply are the enumeration constants that you added
in Chapter 31: EALIGN_LEFT, EALIGN_RIGHT, and EALIGN_CENTER. The strings
are placed in a list from which the user of the control can select the
appropriate value for the property.
GetPredefinedStrings uses string literals defined in AlignmentEnumerations.h
for its strings. In your controls, consider using string tables; not only
does this isolate the effect of changing the strings, but it makes it easier
to internationalize your controls.

Listing 32.12 ATLCONTROLWIN.CPP?GetPredefinedStrings Returns the Strings
Corresponding to the Available Enumerated Values
STDMETHODIMP CATLControlWin::GetPredefinedStrings(DISPID
dispid,
CALPOLESTR * lpcaStringsOut, CADWORD * lpcaCookiesOut)
{
USES_CONVERSION;
HRESULT hResult = S_FALSE;
// we should have gotten two pointers if we didn't
if((lpcaStringsOut == NULL) || (lpcaCookiesOut ==
NULL))
// we are out of here
return E_POINTER;
// if this is the property that we are looking for
if(dispid == dispidAlignment)
{
ULONG ulElems = 3;
// allocate the memory for our string array
lpcaStringsOut->pElems = (LPOLESTR *) ::CoTaskMemAlloc(
sizeof(LPOLESTR) * ulElems);
// if we couldn't allocate the memory
if(lpcaStringsOut->pElems == NULL)
// we're out of here
return E_OUTOFMEMORY;
// allocate the memory for our cookie array
lpcaCookiesOut->pElems = (DWORD*) ::CoTaskMemAlloc(sizeof(DWORD*)
* ulElems);
// if we couldn't allocate the memory
if (lpcaCookiesOut->pElems == NULL)
{
// free the string array
::CoTaskMemFree(lpcaStringsOut->pElems);
// exit the function
return E_OUTOFMEMORY;
}
// store the number of elements in each array
lpcaStringsOut->cElems = ulElems;
lpcaCookiesOut->cElems = ulElems;
// allocate the strings
lpcaStringsOut->pElems[0] = ATLA2WHELPER((LPWSTR)::CoTaskMemAlloc(
(lstrlen(EALIGN_LEFT_TEXT) + 1) * 2), EALIGN_LEFT_TEXT,
lstrlen(EALIGN_LEFT_TEXT) + 1);
lpcaStringsOut->pElems[1] = ATLA2WHELPER((LPWSTR)::CoTaskMemAlloc(
(lstrlen(EALIGN_CENTER_TEXT) + 1) * 2), EALIGN_CENTER_TEXT,
lstrlen(EALIGN_CENTER_TEXT) + 1);
lpcaStringsOut->pElems[2] = ATLA2WHELPER((LPWSTR)::CoTaskMemAlloc(
(lstrlen(EALIGN_RIGHT_TEXT) + 1) * 2), EALIGN_RIGHT_TEXT,
lstrlen(EALIGN_RIGHT_TEXT) + 1);
// assign the cookie value
lpcaCookiesOut->pElems[0] = EALIGN_LEFT;
lpcaCookiesOut->pElems[1] = EALIGN_CENTER;
lpcaCookiesOut->pElems[2] = EALIGN_RIGHT;
hResult = S_OK;
}
return hResult;
}
Getting an Enumeration Value
GetPredefinedValue, shown in Listing 32.13, is called when the container's
property browser needs the real enumeration value associated with a particular
dispid and cookie. The value returned is the actual value that is stored
in the property, not the string that was used to represent it. For our
control, the returned value will always be a VT_I4, but if, for example,
we were storing a string instead, we'd return a string variant.
Listing 32.13 ATLCONTROLWIN.CPP?Using GetPredefinedValue to Map the
Control-Defined Cookie into a Variant
STDMETHODIMP CATLControlWin::GetPredefinedValue(DISPID
dispid, DWORD dwCookie,
VARIANT* lpvarOut)
{
BOOL bResult = FALSE;
// which property is it
switch(dispid)
{
case dispidAlignment:
// clear the variant
::VariantInit(lpvarOut);
// set the type to a long
lpvarOut->vt = VT_I4;
// set the value to the value that was stored with
the string
lpvarOut->lVal = dwCookie;
// set the return value
bResult = TRUE;
break;
}
return bResult;
}
Turning an Enumeration into a String
After the container calls GetPredefinedValue, it can set the property with
the correct value?but it still needs to map that value to a human-readable
string. GetDisplayString does just that: It returns the string that is
associated with the current property setting.
Listing 32.14 ATLCONTROLWIN.CPP?Tying an Eumeration Back to a Text String
with GetDisplayString
STDMETHODIMP CATLControlWin::GetDisplayString(DISPID
dispid, BSTR* lpbstr)
{
USES_CONVERSION;
HRESULT hResult = S_FALSE;
// which property is it
switch(dispid)
{
case dispidAlignment:
{
switch(m_lAlignment)
{
case EALIGN_LEFT:
*lpbstr = ::SysAllocString(T2OLE(EALIGN_LEFT_TEXT));
break;
case EALIGN_CENTER:
*lpbstr = ::SysAllocString(T2OLE(EALIGN_CENTER_TEXT));
break;
case EALIGN_RIGHT:
*lpbstr = ::SysAllocString(T2OLE(EALIGN_RIGHT_TEXT));
break;
}
// set the return value
hResult = S_OK;
}
break;
}
return hResult;
}
The property browser already gets a string for every value when it calls
GetPredefinedStrings, so GetDisplayString might seem a little redundant.
You should have GetDisplayString because it can be implemented without
implementing the other methods. Property types that do not use the standard
property selection mechanism (for example, fonts) can use GetDisplayString
to fill out a list in a dialog.

Optimizing Control Drawing
Optimized drawing enables you to create drawing objects, such as
pens or brushes, once. Rather than removing them when you are finished
drawing, you can cache them as control member variables and reuse them
each time your control draws itself. The benefit is that you create drawing
resources once for the lifetime of your control, rather than every time
it draws.
If your control doesn't do a lot of custom drawing, optimized drawing might
not boost its performance. There's also a size-versus-speed tradeoff between
standard and optimized drawing: standard drawing doesn't use member variables
for the drawing objects that are created and used, so it saves space at
the expense of having to reallocate the drawing objects each time the control
is drawn. Optimized drawing trades that space for the increased speed resulting
from reusing graphic resources in the redraw loop.
There's an additional drawback to optimized drawing: the container that
your control ends up in might not support it. Your controls must continue
to support standard drawing functionality, switching to optimized only
if it is available.
ATL's optimized drawing capabilities are only a subset of those defined
in the OC 96 specification. The OC 96 specification further breaks optimized
drawing into aspects. For more information on aspect drawing, please
see the OC 96 Specification that ships with the ActiveX SDK.

In Chapter 31, you learned how to implement standard drawing so that your
control could draw itself. Supporting optimized drawing is fairly simple,
as you'll see.
First, you need to check whether or not the container supports optimized
drawing. The bOptimize member of the ATL_DRAWINFO structure passed to your
draw method will be FALSE if the container can't handle optimized drawing.
In that case, you have to clean up all of your allocated resources and
restore any original values. If the container does support optimized
drawing, you ignore the cleanup and reuse the allocated resources the next
time around. Listing 32.15 shows the modified OnDraw function.
Listing 32.15 ATLCONTROLWIN.CPP?Optimizing Drawing by Caching Drawing
Resources?if the Container Supports It
HRESULT CATLControlWin::OnDraw(ATL_DRAWINFO &
di)
{
. . .
// The container does not support optimized drawing.
if(!di.bOptimize)
{
// select the old brush back
::SelectObject(di.hdcDraw, hOldBrush);
// destroy the brush we created
::DeleteObject(hBrush);
// clear the brush handles
hBrush = hOldBrush = NULL;
}
return S_OK;
}
To clean up the cached variables, you must add a message handler, OnDestroy,
for the Windows message WM_DESTROY. Also add an OnDestroy function prototype
to the class declaration, as shown in Listing 32.16. OnDestroy is used
to clean up the drawing resources if any are still allocated when the control
is destroyed. This should happen only if the container supports
optimized drawing; if not, the resources should be freed normally when
drawing is done.
Listing 32.16 ATLCONTROLWIN.H?Adding WM_DESTROY Support to Enable Cleanup
BEGIN_MSG_MAP(CATLControlWin)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode)
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
MESSAGE_HANDLER(WM_KILLFOCUS, OnKillFocus)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
END_MSG_MAP()
. . .
LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
. . .
The last step is to add the OnDestroy implementation, which will clean
up the resources if they are still allocated (see Listing 32.17).
Listing 32.17 ATLCONTROLWIN.CPP?Cleanup Performed by OnDestroy
LRESULT CATLControlWin::OnDestroy(UINT uMsg, WPARAM
wParam, LPARAM lParam, BOOL & bHandled)
{
// if there is an old brush
if(hOldBrush)
{
// get the DC
HDC hDC = this->GetDC();
// select the old brush back
::SelectObject(hDC, hOldBrush);
// release the DC
this->ReleaseDC(hDC);
}
// if we created a brush
if(hBrush)
// destroy the brush we created
::DeleteObject(hBrush);
return TRUE;
}
Adding Clipboard and Drag-and-Drop Support
The basic OLE Clipboard and Drag-and-Drop interfaces are only partially
implemented by the default ATL control implementation. As with the IPerPropertyBrowsing
interface, you must provide the remaining required interfaces yourself.
Supporting the Clipboard and Drag-and-Drop in your control adds polish
and ease-of-use for the end user with relatively little effort on your
part.
Talking to the Clipboard
Clipboard support in ActiveX controls comes from the IDataObject and IEnumFORMATETC
interfaces. IDataObject provides an interface for the Clipboard to request
data from your control, and IEnumFORMATETC is the interface used by other
applications to determine what data types your control's IDataObject interface
supports. In a basic ATL control project, only the IDataObject interface
is supported. The IEnumFORMATETC interface must be added.
Before adding the specific interfaces required by ActiveX to enable Clipboard
transfers, you must first decide which keystroke combinations will be used
to initiate the cut, copy, or paste operations. You should either use Ctrl+X,
Ctrl+C, and Ctrl+V or Shift+Delete, Ctrl+Insert, and Shift+Insert for cut,
copy, and paste respectively.
Watching for the Editing Keys
To trap these keystrokes in your control, you'll need a message handler
for the WM_KEYDOWN message in the form of a method called OnKeyDown (see
Listing 32.18).
Listing 32.18 ATLCONTROLWIN.H?Adding WM_KEYDOWN to the Message Map and
then Adding an OnKeyDown Handler
. . .
BEGIN_MSG_MAP(CATLControlWin)
MESSAGE_HANDLER(WM_PAINT, OnPaint)
MESSAGE_HANDLER(WM_GETDLGCODE, OnGetDlgCode)
MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
MESSAGE_HANDLER(WM_KILLFOCUS, OnKillFocus)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
MESSAGE_HANDLER(WM_KEYDOWN, OnKeyDown)
END_MSG_MAP()
. . .
LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
LRESULT OnKeyDown(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
. . .
The OnKeyDown method, shown in Listing 32.19, looks for the predefined
editing keystroke combinations listed in the preceding paragraph; if the
method finds one, it calls the inherited data transfer functions to complete
the requested Clipboard operation. If the user requested a Cut operation,
OnKeyDown fires the Change and ViewChange events to redisplay the control
after clearing its data.
Listing 32.19 ATLCONTROLWIN.CPP?OnKeyDown, Calling the Inherited IDataObject
Interfaces when the User Requests a Cut or Copy Request
LRESULT CATLControlWin::OnKeyDown(UINT uMsg, WPARAM
wParam, LPARAM lParam,
BOOL & bHandled)
{
UINT nChar = wParam;
UINT nRepCnt = LOWORD(lParam);
UINT nFlags = HIWORD(lParam);
// find out if the shift key is being held down
short sShift = ::GetKeyState(VK_SHIFT);
// find out if the control key is being held down
short sControl = ::GetKeyState(VK_CONTROL);
switch(nChar)
{
// COPY
case 0x43: // 'C'
case 0x63: // 'c'
case VK_INSERT:
// if the control key is being held down
if(sControl & 0x8000)
{
// copy the data to the clipboard
this->CopyDataToClipboard();
// we don't need to pass this key to the base implementation
bHandled = TRUE;
}
break;
// CUT
case 0x58: // 'X'
case 0x78: // 'x'
case VK_DELETE:
// if this is a shift delete OR CTRL-X/x
if((nChar == VK_DELETE && (sShift & 0x8000))
||
((nChar == 0x58 || nChar == 0x78) && (sControl
& 0x8000)))
{
this->CopyDataToClipboard();
// clear the string since this is a CUT operation
delete [] m_lptstrCaption;
// NULL terminate the string reference
m_lptstrCaption = new TCHAR[1];
m_lptstrCaption[0] = '\0';
// fire the global change event
this->FireChange();
// force the control to repaint itself
this->FireViewChange();
// we don't need to pass this key to the base implementation
bHandled = TRUE;
}
break;
}
return TRUE;
}
Supporting the Clipboard
There are two types of Clipboard support: a Clipboard source and a Clipboard
target. When you're a Clipboard source, your control can copy data
to the Clipboard. Having your control be a Clipboard target
means that it can accept data from the Clipboard and use it as though
the user had provided it directly.
Windows supports a number of built-in formats for transferring data via
the Clipboard. Our control will send and accept data by using the CF_TEXT
format, which is the built-in format for transferring ANSI text. Don't
despair if you're writing controls that support other types of data; Windows
also includes support for bitmaps, TIFF files, device-independent bitmaps,
Windows metafiles, SYLK-format tabular data, files, and a variety of other
formats. (As you'll see later in this section, you can even define your
own custom formats, which other Clipboard clients can use for rich data
exchange.)
Any code that wants to use the OLE-enabled Clipboard must support two special
COM interfaces to act as a Clipboard source or target: IDataObject and
IEnumFORMATETC.
-
IDataObject provides methods to actually transfer data to and
from a storage medium, or STGMEDIUM (that's the type name.) A STGMEDIUM
is a specially formatted holder for data that stores information about
the data's source, its type, and COM interfaces that can be used to manipulate
it. IDataObject also includes interfaces for automatically notifying containers
when data changes, but ATL wraps those interfaces for you and manages them
automatically.
-
IEnumFORMATETC is a special type of COM interface called an enumerator.
It implements four interfaces: Next, Skip, Reset, and Clone. Callers use
these methods to iterate through a list of formats by calling IDataObject::EnumFormatEtc
to find out what formats the data can be translated into.
The combination of these two interfaces makes it possible for a Clipboard
client to use Windows' built-in data formats for exchange. When your control
receives data from the Clipboard, you can use IDataObject::EnumFormatEtc
to find the most appropriate format; likewise, any other client reading
data you put on the Clipboard can do the same.
ATL-based controls already inherit from IDataObject, but the IEnumFORMATETC
interface must be added, as shown in Listing 32.20. Because the control
has to provide new versions of some functions of the IDataObject and the
IEnumFORMATETC interfaces, you'll need to add function prototypes to the
class declaration of your control. Note that you only need to add prototypes
for the functions that you're going to override; the remainder can use
their default implementations.
Listing 32.20 ATLCONTROLWIN.H?Adding the Clipboard's COM Interfaces
to the CATLControlWin Class Declaration
. . .
public IPerPropertyBrowsingImpl<CATLControlWin>,
public IEnumFORMATETC
{
public:
. . .
// IDataObject
STDMETHOD(GetData)(LPFORMATETC, LPSTGMEDIUM);
STDMETHOD(EnumFormatEtc)(DWORD, LPENUMFORMATETC*);
// IEnumFORMATETC
STDMETHOD(Next)(ULONG celt, FORMATETC __RPC_FAR *
rgelt,
ULONG __RPC_FAR * pceltFetched);
STDMETHOD(Skip)(ULONG celt);
STDMETHOD(Reset)(void);
STDMETHOD(Clone)(IEnumFORMATETC __RPC_FAR *__RPC_FAR
* ppenum);
. . .
Our IDataObject implementation is straightforward and involves only two
functions, as shown in Listing 32.21. The rest of the IDataObject interface
functions are already implemented by the ATL class IDataObjectImpl. GetData
will, if the format type matches that of a format that your control understands,
copy the control?s current data to the STGMEDIUM parameter that is passed
to the control. If the format type is unrecognized, you must defer to the
base class implementation of GetData. This deferral is very important because
the CComControlBase ATL class implements the IDataObject::GetData function
for copying a range of standard Windows data formats.
EnumFormatEtc is used to return an IEnumFORMATETC reference to the requesting
application so that it can determine what types of data formats are supported
by the control implementation. Again, you must defer to the base class
implementation to ensure that the control functions correctly.
Listing 32.21 ATLCONTROLWIN.CPP?IDataObject Interface Implementation
. . .
STDMETHODIMP CATLControlWin::GetData(LPFORMATETC
lpFormatEtc,
LPSTGMEDIUM lpStgMedium)
{
// if this is a format that we can deal with
if(lpFormatEtc->cfFormat == CF_TEXT &&
lpFormatEtc->tymed & TYMED_HGLOBAL)
{
// get a copy of the current stgmedium
this->CopyStgMedium(lpStgMedium, &sTextStgMedium,
CF_TEXT);
return S_OK;
}
else
return IDataObjectImpl<CATLControlWin>::GetData(lpFormatEtc,
lpStgMedium);
}
STDMETHODIMP CATLControlWin::EnumFormatEtc(DWORD
dwDirection,
LPENUMFORMATETC* ppenumFormatEtc)
{
// we support "get" operations
if(dwDirection == DATADIR_GET)
{
// make the assignment
*ppenumFormatEtc = (IEnumFORMATETC *) this;
// increment the reference count
(*ppenumFormatEtc)->AddRef();
// return success
return S_OK;
}
return IDataObjectImpl<CATLControlWin>::EnumFormatEtc(
dwDirection, ppenumFormatEtc);
}
. . .
-
The IEnumFORMATETC implementation, found in Listing 32.22, is also
simple. Unlike IDataObject, ATL doesn't provide a default implementation,
so your code doesn't have to defer to the base class implementation for
formats that you don't handle. COM enumerators implement four functions:
-
Next is used to retrieve the next element in the enumeration; in this
case, you need to be concerned with only one element, CF_TEXT. After filling
in the FORMATETC structure with the appropriate data, you must increment
the counter and exit the function.
-
The Skip method increments the enumerator's counter and exits the function,
enabling you to skip over enumerated items that you're not interested in.
-
Reset sets the enumerator's counter back to zero.
-
Clone makes a copy of the current enumerator; we just return E_NOTIMPL
because we don't have anything to clone.
Microsoft specifies that enumerators should return available formats in
the same order in which you put multiple formats on the Clipboard. For
example, if your control puts both a DIB and a metafile on the Clipboard,
the enumerator should return both types in the same order.
Listing 32.22 ATLCONTROLWIN.CPP?IEnumFORMATETC Interface Implementation
STDMETHODIMP CATLControlWin::Next(ULONG celt, FORMATETC_RPC_FAR
* rgelt,
ULONG RPC_FAR * pceltFetched)
{
// if we are at the beginning of the enumeration
if(ulFORMATETCElement == 0 && celt > 0)
{
// copy all of the members
rgelt->cfFormat = CF_TEXT;
rgelt->ptd = NULL;
rgelt->dwAspect = 0;
rgelt->lindex = -1;
rgelt->tymed = TYMED_HGLOBAL;
// if the caller wants to know how many we copied
if(pceltFetched)
*pceltFetched = 1;
// increment the counter
ulFORMATETCElement++;
// return success
return S_OK;
}
else
// return failure
return S_FALSE;
}
STDMETHODIMP CATLControlWin::Skip(ULONG celt)
{
// move the counter by the number of elements supplied
ulFORMATETCElement += celt;
// return success
return S_OK;
}
STDMETHODIMP CATLControlWin::Reset(void)
{
// reset to the beginning of the enumerator
ulFORMATETCElement = 0;
// return success
return S_OK;
}
STDMETHODIMP CATLControlWin::Clone(
IEnumFORMATETC_RPC_FAR *__RPC_FAR * /*ppenum*/)
{
return E_NOTIMPL;
}
Exchanging Data with the Clipboard
In addition to the code that you added to trap the keystrokes and to support
the needed COM interfaces, you need to add methods for transferring data
to and from the Clipboard. For this control, there will be a total of five
methods.
-
PrepareDataForTransfer takes data from the control and packages it
into a format that the Clipboard can understand.
-
CopyDataToClipboard actually transfers the data to the Clipboard; it
calls PrepareDataForTransfer to load data from the control, and then uses
the OleXXXClipboard functions to actually effect the transfer.
-
GetDataFromTransfer asks the OLE subsystem to convert whatever data
is on the clipboard to a representation suitable for the control. In our
case, that's a text string, but if our control displayed bitmaps, we could
ask the Clipboard to convert whatever data it had into a DIB or metafile
for our use instead.
-
GetDataFromClipboard uses GetDataFromTransfer to prepare the data,
but only if the data came from outside our control. It then puts the received
data into the control and redraws it.
-
CopyStgMedium copies one STGMEDIUM to another; it's called from GetData,
as shown in Listing 32.21.
You might ask why there are four helper functions. As you'll see in the
section on Drag-and-Drop support, you send and receive data the same way
for drag operations and Clipboard operations. The transfer process involves
the same set of COM interfaces, so GetDataFromTransfer and PrepareDataForTransfer
can be used in both cases.
Putting Data on the Clipboard
Now that the control supports the needed interfaces, let's examine the
specifics of making ATLControlWin a Clipboard source. First of all, we'll
need some additional member variables. sTextFormatEtc and sTextStgMedium
will hold the format and store the text when the user copies data to the
Clipboard?we'll need that data when the paste target requests it. ulFORMATETCElement
is the internal reference counter for the IEnumFORMATETC interface. Add
these declarations and function prototypes for CopyDataToClipboard, PrepareDataForTransfer,
and CopyStgMedium, to the CATLControlWin class declaration. The relevant
code is in Listing 32.23.
Listing 32.23 ATLCONTROLWIN.H?Helper Functions and Member Variables
for Clipboard Support
. . .
void CopyDataToClipboard(void);
void PrepareDataForTransfer(void);
void CopyStgMedium(LPSTGMEDIUM lpTargetStgMedium,
LPSTGMEDIUM lpSourceStgMedium, CLIPFORMAT cfSourceFormat);
ULONG ulFORMATETCElement;
private:
FORMATETC sTextFormatEtc;
STGMEDIUM sTextStgMedium;
};
sTextFormatEtc and sTextStgMedium must be initialized in the constructor
of your class so that they will not contain meaningless data the first
time that they are used (see Listing 32.24).
Listing 32.24 ATLCONTROLWIN.H?Member Initialization in the Class Constructor
. . .
// set the initial state of the ReadyState property
m_lReadyState = READYSTATE_LOADING;
// set to the first element
ulFORMATETCElement = 0;
// clear the storage medium
sTextStgMedium.hGlobal = NULL;
}
. . .
When the user initiates a data transfer to the Clipboard, a reference to
the source?s IDataObject interface is placed on the Clipboard. At that
time, you have to take a snapshot of the control's contents and place it
in a STGMEDIUM object. You do this because the data might not be copied
from the Clipboard immediately, and the data needs to reflect the state
of the control when the copy operation was initiated rather than when the
paste operation takes place.
After the IDataObject interface is on the Clipboard, you simply wait until
someone requests the data. If the request is for a format that you support,
copy the data from your snapshot (in this case, the sTextStgMedium member)
to the STGMEDIUM structure that was passed to you.
Now turn to the member functions that actually take the data snapshot and
copy it to the Clipboard. CopyDataToClipboard is called to initiate the
Clipboard transfer and is shown in Listing 32.25. The code first checks
to see whether or not it already owns the Clipboard, and then it prepares
the caption data for transfer to the clipboard by calling PrepareDataForTransfer.
Finally, if the Clipboard is owned by another program, it flushes whatever
data is on the Clipboard and passes its IDataObject reference to the Clipboard
for later use.
Listing 32.25 ATLCONTROLWIN.CPP?CopyDataToClipboard Helper Function
Implementation
void CATLControlWin::CopyDataToClipboard(void)
{
BOOL bHaveClipboard = TRUE;
// if we don't have an IDataObject on the clipboard?
if(::OleIsCurrentClipboard((IDataObject *) this)
!= S_OK)
// set the flag
bHaveClipboard = FALSE;
// put data in the storage
this->PrepareDataForTransfer();
// if we don't have the clipboard
if(!bHaveClipboard)
{
// clear the clipboard
::OleFlushClipboard();
// put the data on the clipboard
::OleSetClipboard(reinterpret_cast<IDataObject*>
(static_cast<IDataObjectImpl<CATLControlWin>*>(this)));
}
}
PrepareDataForTransfer actually does the work of copying data from your
control to the STGMEDIUM ?snapshot? that will hold it on the Clipboard.
As shown in Listing 32.26, PrepareDataForTransfer starts by allocating
and locking a block of global memory to hold the caption as an ANSI string.
Next, it fills in the FORMATETC and STGMEDIUM structures to reflect the
correct data type. Finally, it releases the STGMEDIUM's storage, if any,
and replaces it with the allocated global block. The effect of all this
is to create a copy of the string pointed to by the control's sTextStgMedium
member.
Listing 32.26 ATLCONTROLWIN.CPP?PrepareDataForTransfer Helper Function
Implementation
void CATLControlWin::PrepareDataForTransfer(void)
{
// get the length of the data to copy
long lLength = lstrlen(m_lptstrCaption) + 1;
// create a global memory object
HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE,
sizeof(TCHAR) * lLength);
// lock the memory down
LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(hGlobal);
// copy the string
for(long lCount = 0; lCount < lLength - 1; lCount++)
lpTempBuffer[lCount] = m_lptstrCaption[lCount];
// null terminate the string
lpTempBuffer[lCount] = '\0';
// unlock the memory
::GlobalUnlock(hGlobal);
// set all of the members
sTextFormatEtc.cfFormat = CF_TEXT;
sTextFormatEtc.ptd = NULL;
sTextFormatEtc.dwAspect = 0;
sTextFormatEtc.lindex = -1;
sTextFormatEtc.tymed = TYMED_HGLOBAL;
// if we have already allocated the data
if(sTextStgMedium.hGlobal)
// release it
::ReleaseStgMedium(&sTextStgMedium);
sTextStgMedium.tymed = TYMED_HGLOBAL;
sTextStgMedium.hGlobal = hGlobal;
sTextStgMedium.pUnkForRelease = NULL;
}
Lastly, CopyStgMedium, shown in Listing 32.27, is a general- purpose helper
function for copying one STGMEDIUM to another. Our IDataObject::GetData
method (shown earlier in Listing 32.21) calls CopyStgMedium, which we'll
use later when we implement custom formats in "Handling Custom Clipboard
and Drag-and-Drop Formats." This function is useful because it allocates
a new global data object instead of copying the reference to the existing
object. This keeps the copy available even if the source STGMEDIUM is freed.
Listing 32.27 ATLCONTROLWIN.CPP?CopyStgMedium Helper Function Implementation
void CATLControlWin::CopyStgMedium(LPSTGMEDIUM lpTargetStgMedium,
LPSTGMEDIUM lpSourceStgMedium, CLIPFORMAT cfSourceFormat)
{
// copy the stgmedium members
lpTargetStgMedium->tymed = lpSourceStgMedium->tymed;
lpTargetStgMedium->pUnkForRelease = lpSourceStgMedium->pUnkForRelease;
lpTargetStgMedium->hGlobal = ::OleDuplicateData(lpSourceStgMedium->hGlobal,
cfSourceFormat, GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT);
}
Getting Data from the Clipboard
The opposite of being a Clipboard source is being a Clipboard target. To
be a Clipboard target, you first need to update the CATLControlWin class
declaration to include two new helper functions, GetDataFromClipboard and
GetDataFromTransfer, as shown in Listing 32.28.
Listing 32.28 ATLCONTROLWIN.H?Clipboard Target Support Helper Function
Prototypes
. . .
void CopyDataToClipboard(void);
void PrepareDataForTransfer(void);
void GetDataFromClipboard(void);
void GetDataFromTransfer(IDataObject * ipDataObj);
void CopyStgMedium(LPSTGMEDIUM lpTargetStgMedium,
LPSTGMEDIUM lpSourceStgMedium, CLIPFORMAT cfSourceFormat);
. . .
Getting data from the Clipboard is almost as simple as putting the data
on the Clipboard in the first place. The primary method is GetDataFromClipboard,
which gets the data from the Clipboard and transfers it to the control.
As you can see in Listing 32.29, the function first checks the Clipboard
to see whether or not the control already owns it. If the control does
own the Clipboard, it refreshes the control?s data with the data that is
stored in the cached STGMEDIUM structure. The function is written this
way because the data stored in the control might have changed since it
was originally pasted to the Clipboard.
If another process owns the Clipboard, GetDataFromClipboard gets a reference
to the owner's IDataObject and passes it on to the GetDataFromTransfer
function.
Listing 32.29 MFCCONTROLWINCTL.CPP?GetDataFromClipboard Implementation
void CATLControlWin::GetDataFromClipboard(void)
{
// get an IDataObject pointer
IDataObject * ipClipboardDataObj = NULL;
// do we have an IDataObject on the clipboard?
if(::OleIsCurrentClipboard((IDataObject *) this)
== S_OK)
{
// get the global data for this format and lock down
the memory
LPTSTR lpTempBuffer = (LPTSTR) ::GlobalLock(sTextStgMedium.hGlobal);
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer)
+ 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lpTempBuffer);
// unlock the memory
::GlobalUnlock(sTextStgMedium.hGlobal);
return;
}
else if(::OleGetClipboard(&ipClipboardDataObj)
== S_OK)
{
// transfer the data to the control
this->GetDataFromTransfer(ipClipboardDataObj);
// release the IDataObject
ipClipboardDataObj->Release();
}
}
GetDataFromTransfer, which appears in Listing 32.30, begins by requesting
the IEnumFORMATETC interface from the provided IDataObject and cycling
through all of the supported formats to look for CF_TEXT. Upon finding
the appropriate format, it requests the data from IDataObject::GetData,
supplying a FORMATETC and a STGMEDIUM structure. GetData puts the data
in the storage and fills in the FORMATETC to specify what data is being
returned and how it's stored. Next, the newly retrieved caption is loaded
into the control, and the STGMEDUIM is released. Finally, GetDataFromTransfer
releases the interface pointer for the enumerator, and, if it found a compatible
format, forces the control to repaint itself with the new caption.
If your control can handle multiple data formats?for example, if you want
to accept rich text if it's available and CF_TEXT if it's not?you can compare
the cfFormat field of the enumerator's returned value to look for the types
in your preferred order.

Listing 32.30 MFCCONTROLWINCTL.CPP?GetDataFromTransfer Implementation
void CATLControlWin::GetDataFromTransfer(IDataObject
* ipDataObj)
{
IEnumFORMATETC * ipenumFormatetc;
BOOL bFound = FALSE;
// get a FORMATETC enumerator
if(ipDataObj->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc)
== S_OK)
{
// reset the enumerator just to be safe
ipenumFormatetc->Reset();
FORMATETC etc;
// while there are formats to enumerate
while(ipenumFormatetc->Next(1, &etc, NULL)
== S_OK)
{
// is this a format that we are looking for?
if(etc.cfFormat == CF_TEXT && etc.tymed &
TYMED_HGLOBAL)
{
STGMEDIUM sStgMediumData;
// get the data from the stgmedium
if(ipDataObj->GetData(&etc, &sStgMediumData)
== S_OK)
{
// get the global data for this format
// and lock down the memory
LPTSTR lpTempBuffer = (LPTSTR)
::GlobalLock(sStgMediumData.hGlobal);
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer)
+ 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lpTempBuffer);
// unlock the memory
::GlobalUnlock(sStgMediumData.hGlobal);
// release the storage medium
::ReleaseStgMedium(&sStgMediumData);
// indicate success
bFound = TRUE;
}
}
}
// release the enumerator
ipenumFormatetc->Release();
}
// if we found a format
if(bFound == TRUE)
// force the control to repaint itself
this->FireViewChange();
}
You might have noticed that the version of OnKeyDown shown in Listing 32.19
didn't support pasting from the Clipboard. The code in Listing 32.31 shows
the changes needed to support pasting with either Ctrl+V or Shift+Insert.
In either case, OnKeyDown calls GetDataFromClipboard to get the data and
forces the control to update by firing a message with FireViewChange.
Listing 32.31 ATLCONTROLWIN.CPP?OnKeyDown Implementation
...
switch(nChar)
{
// PASTE
case 0x56: // 'V'
case 0x76: // 'v'
// if the control key is being held down
if(sControl & 0x8000)
{
// get any text from the clipboard
this->GetDataFromClipboard();
// force the control to repaint itself
this->FireViewChange();
// we don't need to pass this key to the base implementation
bHandled = TRUE;
}
break;
// COPY or PASTE
case 0x43: // 'C'
case 0x63: // 'c'
case VK_INSERT:
// if the control key is being held down
if(sControl & 0x8000)
{
// copy the data to the clipboard
this->CopyDataToClipboard();
// we don't need to pass this key to the base implementation
bHandled = TRUE;
}
// if the shift key is being held down it is a PASTE
else if(sShift & 0x8000 && nChar == VK_INSERT)
{
// get any text from the clipboard
this->GetDataFromClipboard();
// force the control to repaint itself
this->FireViewChange();
// we don't need to pass this key to the base implementation
bHandled = TRUE;
}
break;
...
Supporting Drag-and-Drop
The fundamentals of supporting Drag-and-Drop are very similar to those
needed for Clipboard support. Drag-and-Drop relies on the same set of interfaces
for the actual data transfer, but it adds two new interfaces: IDropSource
and IDropTarget. Drag sources?controls that create data that can
be dropped onto another application?need to be inherited from IDropSource.
The corresponding IDropTarget is for controls that can act as drop targets;
targets accept data that has been dropped from another application or control.
Because Drag-and-Drop is essentially a Clipboard transfer?with fewer steps
involved?Drag-and-Drop uses the same built-in data formats as Clipboard
transfers, and the overall implementation is quite similar.
If it doesn't make sense for your control to be only a drag target or drag
source but not both, don't worry. The two are independent, so you can implement
either or both.

Enabling a Control as a Drag-and-Drop Source
The first part of your implementation is to enable the control as a drag
source. To act as a drag source, the control must implement the IDropSource
interface, in addition to the IDataObject and IEnumFORMATETC interfaces
that you implemented in the "Supporting the Clipboard" section.
ATLControlWin inherits from IDropSource in the same manner as the other
COM interfaces that you've added previously. Because the interface consists
of only two methods, the implementation is easy. QueryContinueDrag is called
by the system event handler so that you can tell it whether or not to register
the drop event and send it to the target. GiveFeedback is called during
the drag, so you can change the displayed cursor or otherwise indicate
to the user that a drag is in progress. The necessary changes to ATLControlWin.h
are shown in Listing 32.32.
Listing 32.32 ATLCONTROLWIN.H?IDropSource Interface Added to the CATLControlWin
Class Declaration
. . .
public IEnumFORMATETC,
public IDropSource
{
public:
CATLControlWin()
. . .
COM_INTERFACE_ENTRY(IEnumFORMATETC)
COM_INTERFACE_ENTRY(IDropSource)
END_COM_MAP()
. . .
// IEnumFORMATETC
. . .
// IDropSource
STDMETHOD(QueryContinueDrag)(BOOL fEscapePressed,
DWORD dwKeyState);
STDMETHOD(GiveFeedback)(DWORD dwEffect);
. . .
Before you can actually initiate a drag, you need to know when the user
presses the left mouse button in your control. The easiest way to do this
is to catch the WM_LBUTTONDOWN message, so you'll need a message handler
for it. Add WM_LBUTTONDOWN message handler called OnLButtonDown to the
message map, as shown in Listing 32.33.
Listing 32.33 ATLCONTROLWIN.H?WM_LBUTTONDOWN and OnLButtonDown Message
Handler Added to the Class Declaration of the Control
. . .
BEGIN_MSG_MAP(CATLControlWin)
. . .
MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
END_MSG_MAP()
. . .
LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
LRESULT OnKeyDown(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
LRESULT OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
. . .
The OnLButtonDown implementation, as shown in Listing 32.34, is similar
to the CopyDataToClipboard method shown in Listing 32.25. OnLButtonDown
prepares the data for its transfer and calls the Win32 DoDragDrop function
to start the drag. Note that we always pass DROPEFFECT_COPY as the drop
modifier for DoDragDrop; you could check for modifier keys and pass a different
drop modifier to enable users to modify what happens on a drag.
Listing 32.34 ATLCONTROLWIN.CPP?OnLButtonDown Implementation
LRESULT CATLControlWin::OnLButtonDown(UINT uMsg,
WPARAM wParam, LPARAM lParam,
BOOL & bHandled)
{
// Un-comment these parameters if you need them
// UINT nFlags = wParam;
// short sHor = (short) LOWORD(lParam);
// short sVer = (short) HIWORD(lParam);
// call the common data preparation function
this->PrepareDataForTransfer();
DWORD dwDropEffect = DROPEFFECT_NONE;
// start the Drag and Drop operation
::DoDragDrop(reinterpret_cast<IDataObject*>
(static_cast<IDataObjectImpl<CATLControlWin>*>(this)),
(IDropSource *) this, DROPEFFECT_COPY, &dwDropEffect);
return TRUE;
}
The final step in making the control a drag source is to implement the
IDropSource interface, the code for which is shown in Listing 32.35
Listing 32.35 ATLCONTROLWIN.CPP?IDropSource Implementation
STDMETHODIMP CATLControlWin::QueryContinueDrag(BOOL
fEscapePressed,
DWORD dwKeyState)
{
// if the left button has been released
if(!(dwKeyState & MK_LBUTTON))
// it is OK to drop
return DRAGDROP_S_DROP;
else
// return success
return S_OK;
}
STDMETHODIMP CATLControlWin::GiveFeedback(DWORD dwEffect)
{
// use the default cursors
return DRAGDROP_S_USEDEFAULTCURSORS;
}
QueryContinueDrag is used to tell OLE how the drag operation should be
handled when the state of the keyboard or mouse changes. This might result
when the user pressed Esc, or when the left mouse button was released.
In this case, if the left mouse button is no longer being held down, you
declare the drop to be complete. Otherwise, you exit, because the drag
is still in progress.
GiveFeedback enables you to change the cursor displayed during a drop operation.
For example, you might display different cursors when modifier keys are
held during the drag, or you might change the cursor when the drag passes
over a screen region that's not a legal target. In this case, we use the
default cursors.
Enabling a Control as a Drag-and-Drop Target
To qualify as a drag target, a control must implement the IDropTarget interface
and register itself as a target. By the time you've implemented Clipboard
and drag source support, adding drag target support requires very little
work. You'll need to make your control inherit from the IDropTarget interface
in the same fashion as the other interfaces; you'll also need to support
the DragEnter, DragOver, DragLeave, and Drop interface methods. The needed
changes are shown in Listing 32.36.
Listing 32.36 ATLCONTROLWIN.H?IDropTarget Interface Added to the CATLControlWin
Class Declaration
. . .
public IDropSource,
public IDropTarget
{
public:
CATLControlWin()
. . .
COM_INTERFACE_ENTRY(IDropSource)
COM_INTERFACE_ENTRY(IDropTarget)
END_COM_MAP()
. . .
// IDropSource
STDMETHOD(QueryContinueDrag)(BOOL fEscapePressed,
DWORD dwKeyState);
STDMETHOD(GiveFeedback)(DWORD dwEffect);
// IDropTarget
STDMETHOD(DragEnter)(LPDATAOBJECT pDataObject, DWORD
dwKeyState, POINTL pt,
LPDWORD pdwEffect);
STDMETHOD(DragOver)(DWORD dwKeyState, POINTL pt,
LPDWORD pdwEffect);
STDMETHOD(DragLeave)(void);
STDMETHOD(Drop)(LPDATAOBJECT pDataObject, DWORD dwKeyState,
POINTL pt,
LPDWORD pdwEffect);
. . .
Registering and Unregistering the Target
In addition to supporting the required ActiveX interfaces, you have to
tell other, potentially non-ActiveX programs, that your control can be
a target. To accomplish this, register the control as a drop target with
Windows by calling the Win32 RegisterDragDrop function. Because you want
the control to be registered when it's created, add a handler for the WM_CREATE
message to the ATLControlWin message map, as shown in Listing 32.37.
Listing 32.37 ATLCONTROLWIN.H?WM_CREATE Message Handler
. . .
MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
END_MSG_MAP()
. . .
LRESULT OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM
lParam, BOOL & bHandled);
. . .
The OnCreate handler implementation calls RegisterDragDrop with the control's
window handle, as shown in Listing 32.38
Listing 32.38 ATLCONTROLWIN.CPP?OnCreate Implementation
LRESULT CATLControlWin::OnCreate(UINT uMsg, WPARAM
wParam, LPARAM lParam,
BOOL & bHandled)
{
// if we have a window handle
if(m_hWnd)
// register the control as a drag and drop target
::RegisterDragDrop(m_hWnd, (IDropTarget *) this);
return TRUE;
}
Correspondingly, when the control is being destroyed, you need to call
RevokeDragDrop to unregister the control as a valid drop target. You can
add a call to RevokeDragDrop to the control's OnDestroy function (refer
to Listing 32.17.) The added code is shown in Listing 32.39.
Listing 32.39 ATLCONTROLWIN.CPP?OnDestroy Implementation Updated to
Revoke the Control as a Valid Drop Target
LRESULT CATLControlWin::OnDestroy(UINT uMsg, WPARAM
wParam, LPARAM lParam,
BOOL & bHandled)
{
// if we have a window handle
if(m_hWnd)
// revoke the control as a drag and drop target
::RevokeDragDrop(m_hWnd);
. . .
Implementing IDropTarget
To make your control a real drag target, you must implement four functions
from the IDropTarget interface. DragEnter, shown in Listing 32.40, is called
when a drag enters your control. Your code must tell OLE whether or not
your control can accept data from the current drag operation. The code
first confirms that the mouse or keyboard state is appropriate?in this
case, the left mouse button must be held down. Next, DragEnter uses the
IEnumFORMATETC interface to see whether or not the IDataObject that was
passed to the function contains any formats that the control can understand.
If the data can be used, it returns DROPEFFECT_COPY to tell OLE that the
drag is acceptable; in any other case, it returns DROPEFFECT_NONE to indicate
that it's not.
Listing 32.40 ATLCONTROLWIN.CPP?DragEnter: Allowing You to Decide Whether
to Accept or Reject a Drag
STDMETHODIMP CATLControlWin::DragEnter(LPDATAOBJECT
pDataObject, DWORD dwKeyState,
POINTL pt, LPDWORD pdwEffect)
{
// if the left mouse button is being held down
if(dwKeyState & MK_LBUTTON)
{
IEnumFORMATETC * ipenumFormatetc;
BOOL bFound = FALSE;
// get a FORMATETC enumerator
if(pDataObject->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc)
== S_OK)
{
// reset the enumerator just to be safe
ipenumFormatetc->Reset();
FORMATETC etc;
// while there are formats to enumerate
while(ipenumFormatetc->Next(1, &etc, NULL)
== S_OK && !bFound)
{
// is this a format that we are looking for?
if(etc.cfFormat == CF_TEXT && etc.tymed &
TYMED_HGLOBAL)
bFound = TRUE;
}
// release the enumerator
ipenumFormatetc->Release();
}
// is there a text format available
if(bFound)
*pdwEffect = DROPEFFECT_COPY;
// everything else we can't deal with
else
*pdwEffect = DROPEFFECT_NONE;
}
else
// not the left mouse
*pdwEffect = DROPEFFECT_NONE;
// return success
return S_OK;
}
DragOver, shown in Listing 32.42, is used to test the current state of
the drag operation while it is over the control. This implementation is
very basic. One could, however, change the method to restrict the drag
operation. You might check the POINTL structure that was passed in and
compare it to various locations of the control. As another example, a grid
or "spreadsheet" control might enable only text data to be dropped
on the headings but both text and numeric data while over the columns.
Listing 32.41 ATLCONTROLWIN.CPP? Constraining the Drag, Based on the
Mouse Coordinates
STDMETHODIMP CATLControlWin::DragOver(DWORD dwKeyState,
POINTL pt,
LPDWORD pdwEffect)
{
// if the left mouse button is being held down
if(dwKeyState & MK_LBUTTON)
// copy
*pdwEffect = DROPEFFECT_COPY;
else
// not the left mouse
*pdwEffect = DROPEFFECT_NONE;
// return success
return S_OK;
}
DragLeave is used to clean up any state information that might have been
created when DragEnter was invoked. If you don't allocate anything during
DragLeave, just return E_NOTIMPL, as we do for ATLControlWin.
Drop (see Listing 32.43) is the last function that you need to implement.
It actually copies the data from the source IDataObject to the control
by using the GetDataFromTransfer method that we implemented for Clipboard
support.
Listing 32.42 ATLCONTROLWIN.CPP?IDropTarget Interface Implementation
STDMETHODIMP CATLControlWin::Drop(LPDATAOBJECT pDataObject,
DWORD dwKeyState,
POINTL pt, LPDWORD pdwEffect)
{
// transfer the data to the control
this->GetDataFromTransfer(pDataObject);
// return success
return S_OK;
}
That's it! If all you want is basic Drag-and-Drop support, you're done.
You can also support custom formats, however, with a few more lines of
code.
Handling Custom Clipboard and Drag-and-Drop Formats
In addition to the standard Windows formats, applications and controls
can define their own custom data formats for data exchange. For
example, Intergraph's Imagineer computer-aided design software defines
custom formats for 2-D and 3-D drawings. When transferring data via Drag-and-Drop
or the Clipboard, Imagineer supplies data in metafile, DIB, and its own
custom format. Applications that understand the Imagineer formats can use
them; applications that don't can use the metafile or DIB version of the
data instead.
Support for custom data formats is independent of the mechanism used to
initiate the data transfer. Because we isolated the methods used to prepare
data for transfer from those that actually do the transferring, we need
make only one set of changes to accommodate custom Clipboard and Drag-and-Drop
operations.
In preceding sections of this chapter, you implemented support for transferring
ATLControlWin's caption as plain text. Next you examine what would be required
to transfer the custom Alignment property along with the Caption.
Registering a Custom Format
Windows already provides predefined IDs for the common CF_XXX formats that
it provides. To make your custom formats visible, you'll have to register
them with Windows. This requires you to keep track of the ID number of
the registered custom format; add a member variable named m_uiCustomFormat
for this purpose. In addition, you also need an extra FORMATETC/STGMEDIUM
pair so that you can store your custom data and text data simultaneously.
Listing 32.43 shows the required change.
Listing 32.43 ATLCONTROLWIN.H?Custom Data Format Member Variables
. . .
private:
FORMATETC sTextFormatEtc;
STGMEDIUM sTextStgMedium;
// custom format storage variables
UINT m_uiCustomFormat;
FORMATETC sCustomFormatEtc;
STGMEDIUM sCustomStgMedium;
};
Next, we need to register the custom format and initialize the member variables
to valid values, which you do in the CATLControlWin constructor, as shown
in Listing 32.44. When you register the custom data format, you are actually
registering the format with Windows. That way, whenever an application
needs to use the format, that application will get the same ID for the
format as the application that registered the format in the first place.
All applications that need to use a particular custom format must retrieve
the format's ID before using it.
Listing 32.44 ATLCONTROLWIN.H?Registering the Custom Format and Initializing
the Member Variables in the Class Constructor
. . .
// set to the first element
ulFORMATETCElement = 0;
// clear the storage medium
sTextStgMedium.hGlobal = NULL;
// register a custom clipboard format
m_uiCustomFormat = ::RegisterClipboardFormat("BCFControlCtlCustomFormat");
// clear the storage medium
sCustomStgMedium.hGlobal = NULL;
}
Transferring the Custom Format
Now that the format is registered, the PrepareDataForTransfer function
needs to support the custom data format. In addition to the existing support
for the CF_TEXT format, we need to generate the outgoing data in our custom
format. When sending data to the Clipboard or a drag target, applications
that support multiple formats should render the outgoing data into each
format, enabling the receiving application to get the richest version of
the data?a drag target that understands only CF_TEXT can get the caption
text, but a target that understands the custom format can get it instead.
The code to add to PrepareDataForTransfer is shown in Listing 32.45
Listing 32.45 ATLCONTROLWIN.CPP?PrepareDataForTransfer Update
void CATLControlWin::PrepareDataForTransfer(void)
{
. . .
// if we have custom clipboard format support
if(m_uiCustomFormat)
{
// create a global memory object
HGLOBAL hGlobal = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_SHARE,
sizeof(m_lAlignment));
// lock the memory down
LONG * lpTempBuffer = (LONG *) ::GlobalLock(hGlobal);
// set our data buffer
*lpTempBuffer = m_lAlignment;
// unlock the memory
::GlobalUnlock(hGlobal);
// copy all of the members
sCustomFormatEtc.cfFormat = m_uiCustomFormat;
sCustomFormatEtc.ptd = NULL;
sCustomFormatEtc.dwAspect = 0;
sCustomFormatEtc.lindex = -1;
sCustomFormatEtc.tymed = TYMED_HGLOBAL;
// if we have already allocated the data
if(sCustomStgMedium.hGlobal)
// release it
::ReleaseStgMedium(&sCustomStgMedium);
sCustomStgMedium.tymed = TYMED_HGLOBAL;
sCustomStgMedium.hGlobal = hGlobal;
sCustomStgMedium.pUnkForRelease = NULL;
}
}
Listing 32.45 provides half of the needed support, but we still have to
update GetDataFromTransfer to accept the custom format on incoming transfers.
As with the PrepareDataForTransfer method, we support both the basic text
format and our custom format independent of one another. Note that the
while loop in this implementation is slightly diferent from the earlier
version; the change enables us to examine all of the formats returned by
the enumerator and get any data that we can use, instead of stopping at
the first usable format. This means that we can get several types of data
instead of being locked into whichever one occurs first. The updated GetDataFromTransfer
is shown in Listing 32.46.
Listing 32.46 ATLCONTROLWIN.CPP?GetDataFromTransfer Update
void CATLControlWin::GetDataFromTransfer(IDataObject
* ipDataObj)
{
IEnumFORMATETC * ipenumFormatetc;
BOOL bFound = FALSE;
// get a FORMATETC enumerator
if(ipDataObj->EnumFormatEtc(DATADIR_GET, &ipenumFormatetc)
== S_OK)
{
// reset the enumerator just to be safe
ipenumFormatetc->Reset();
FORMATETC etc;
// while there are formats to enumerate
while(ipenumFormatetc->Next(1, &etc, NULL)
== S_OK)
{
// is this a format that we are looking for?
if(etc.cfFormat == CF_TEXT && etc.tymed &
TYMED_HGLOBAL)
{
STGMEDIUM sStgMediumData;
// get the data from the stgmedium
if(ipDataObj->GetData(&etc, &sStgMediumData)
== S_OK)
{
// get the global data for this format
// and lock down the memory
LPTSTR lpTempBuffer = (LPTSTR)
::GlobalLock(sStgMediumData.hGlobal);
// if we have a string
if(m_lptstrCaption)
{
// delete the existing string
delete [] m_lptstrCaption;
// clear the reference just to be safe
m_lptstrCaption = NULL;
}
// allocate a new string
m_lptstrCaption = new TCHAR[lstrlen(lpTempBuffer)
+ 1];
// assign the string to our member variable
lstrcpy(m_lptstrCaption, lpTempBuffer);
// unlock the memory
::GlobalUnlock(sStgMediumData.hGlobal);
// release the storage medium
::ReleaseStgMedium(&sStgMediumData);
// indicate success
bFound = TRUE;
}
}
// is this a format that we are looking for?
else if(m_uiCustomFormat && etc.cfFormat
== m_uiCustomFormat &&
etc.tymed & TYMED_HGLOBAL)
{
STGMEDIUM sStgMediumData;
// get the data from the stgmedium
if(ipDataObj->GetData(&etc, &sStgMediumData)
== S_OK)
{
// get the global data for this format and lock down
the [ic:ccc]memory
LONG * lpTempBuffer =
(LONG *) ::GlobalLock(sStgMediumData.hGlobal);
// get the data
m_lAlignment = *lpTempBuffer;
// unlock the memory
::GlobalUnlock(sStgMediumData.hGlobal);
// release the storage medium
::ReleaseStgMedium(&sStgMediumData);
// indicate success
bFound = TRUE;
}
}
}
// release the enumerator
ipenumFormatetc->Release();
}
// if we found a format
if(bFound == TRUE)
// force the control to repaint itself
this->FireViewChange();
// this->InvalidateControl(); <== MFC Version
}
Providing Data in the Custom Format
Our transfer routines support the sending and receiving of data in our
custom format, but no one will know that the format exists until we modify
our IEnumFORMATETC::Next enumerator. The version shown in Listing 32.22
only admits to knowing about CF_TEXT. The new version, shown in Listing
32.47, returns CF_TEXT as the first supported format, followed by our custom
format (if the control supports it.) The function determines whether or
not the control supports a custom format and whether or not the enumerator
is at the second format.
Listing 32.47 ATLCONTROLWIN.CPP?IEnumFORMATETC::Next Update
STDMETHODIMP CATLControlWin::Next(ULONG celt, FORMATETC
RPC_FAR * rgelt,
ULONG_RPC_FAR * pceltFetched)
{
// if we are at the beginning of the enumeration
if(ulFORMATETCElement == 0 && celt > 0)
{
// copy all of the members
rgelt->cfFormat = CF_TEXT;
rgelt->ptd = NULL;
rgelt->dwAspect = 0;
rgelt->lindex = -1;
rgelt->tymed = TYMED_HGLOBAL;
// if the caller wants to know how many we copied
if(pceltFetched)
*pceltFetched = 1;
// increment the counter
ulFORMATETCElement++;
// return success
return S_OK;
}
else if(m_uiCustomFormat && ulFORMATETCElement
== 1 && celt > 0)
{
// copy all of the members
rgelt->cfFormat = m_uiCustomFormat;
rgelt->ptd = NULL;
rgelt->dwAspect = 0;
rgelt->lindex = -1;
rgelt->tymed = TYMED_HGLOBAL;
// if the caller wants to know how many we copied
if(pceltFetched)
*pceltFetched = 1;
// increment the counter
ulFORMATETCElement++;
// return success
return S_OK;
}
else
// return failure
return S_FALSE;
}
The final requirement is actually returning data to a caller. IEnumFORMATETC::GetData
has to be modified to return data from our text STGMEDIUM when text is
requested and from our custom format's STGMEDIUM when the custom format
is requested. The new version is in Listing 32.48. Note that you can still
use the CopyStgMedium function; the only difference is which of CATLControlWin's
cached STGMEDIUMs you call it with.
Listing 32.48 ATLCONTROLWIN.CPP?IEnumFORMATETC::GetData Update
STDMETHODIMP CATLControlWin::GetData(LPFORMATETC
lpFormatEtc, LPSTGMEDIUM lpStgMedium)
{
// if this is a format that we can deal with
if(lpFormatEtc->cfFormat == CF_TEXT &&
lpFormatEtc->tymed & TYMED_HGLOBAL)
{
// get a copy of the current stgmedium
this->CopyStgMedium(lpStgMedium, &sTextStgMedium,
CF_TEXT);
return S_OK;
}
else if(m_uiCustomFormat && lpFormatEtc->cfFormat
== m_uiCustomFormat
&& lpFormatEtc->tymed & TYMED_HGLOBAL)
{
// get a copy of the current stgmedium
this->CopyStgMedium(lpStgMedium, &sCustomStgMedium,
m_uiCustomFormat);
return S_OK;
}
else
return IDataObjectImpl<CATLControlWin>::GetData(lpFormatEtc,
lpStgMedium);
}
That?s all it takes to support custom formats. By taking a ?black box?
approach and separating the data transfer routines from the data preparation
routines, you can easily add support for other formats without extensive
code changes.
ATL Support for Other Advanced ActiveX Features
ATL enables you to take advantage of many of the available OC 96 and ActiveX
features. In some cases, support for these features is automatic.
Dual-Interface Controls
ActiveX controls can communicate with other applications in two ways: using
COM interfaces, like IDataObject, or by using interfaces derived from IDispatch.
ActiveX automation, discussed in detail in Chapter 29, ?Creating ActiveX
Controls,? depends on IDispatch. Without ATL, you'd have to manually add
support for IDispatch calls, which is not a light undertaking. By default,
ATL control implementations support both kinds of interface, so you do
not need to do any additional work. If for some reason you want to generate
a control with only one type of interface, you can do so in the Attributes
tab of the ATL Object Wizard Properties dialog box.
Windowless Activation
ActiveX controls can request that no new window be generated when they're
in-place activated. In this case, the container can receive window messages
for the control and dispatch them without requiring the control to have
its own window. The IOleInPlaceObjectWindowless interface, implemented
in the container, provides this capability. If the container doesn?t support
windowless activation, the control must be able to create a window for
itself. Windowless activation is a request, and there's a guarantee that
all containers will grant it.
ATL supports windowless controls out-of-the-box. When generating a new
control, use the Miscellaneous tab of the ATL Object Wizard Properties
dialog box to clear the Windowed Only check box if you
want the control to support windowless activation. The CComControl class
contains a member variable, m_bWindowOnly, which, if set to TRUE, instructs
the control to use windowless activation if the container supports it.
If set to FALSE, the control will always create a window handle and use
it.
Unclipped Device Context
Unclipped device context is an MFC-specific optimization and is not yet
implemented in ATL.
Flicker-Free Activation
Flicker-free activation requires that the container support the
IOleInPlaceSiteEx interface. ATL automatically attempts to find this interface
and will use it if the container supports it. Flicker-free Activation requires
no implementation on the part of the developer.
Mouse Pointer Notifications when Inactive
If you need to, you can request notification when the mouse pointer moves
over your control while it's inactive. These notifications are provided
through the IPointerInactive COM interface. To support this interface in
your control, you must add the ATL class IPointerInactiveImpl to your class
declaration and override the ATL implementations of the GetActivationPolicy,
OnInactiveMouseMove, and OnInactiveSetCursor methods.
Optimized Drawing Code
Optimized drawing is fairly simple to implement, as you saw in the ?Enabling
Optimized Drawing? section earlier in this chapter. A parameter of the
OnDraw method indicates whether or not the control is able to use optimized
drawing. In addition, ATL supports aspect-optimized drawing, which
is beyond the scope of this book. If you want to implement this feature,
please see the OC 96 specification included in the ActiveX SDK.
From Here...
This chapter focused on expanding the basic control implementation that
you created in Chapter 31 by adding support for some advanced ActiveX features.
As the user and developer market for ActiveX controls becomes more discerning,
these features will become increasingly important in separating successful
controls from unsuccessful ones. Fortunately, ATL provides solid support
for these features and minimizes the amount of effort required for you
to implement them.
The next three chapters explore using MFC for some traditional, but necessary,
tasks: managing linked lists, performing recursive operations, and generating
new classes from scratch. In the meantime, here are some other information
sources that you might find useful: