Chapter 21

The Active Template Library


The Active Template Library, ATL, is a collection of C++ class templates that you can use to build ActiveX controls. These small controls generally do not use MFC at all. Writing an ActiveX control with ATL requires a lot more knowledge of COM and interfaces than writing an MFC ActiveX control, because MFC protects you from a lot of low-level COM concepts. Using ATL is not for the timid, but it pays dividends in smaller, tighter controls. This chapter will rewrite the Dieroll control of Chapter 17, "Building an ActiveX Control," and Chapter 20, "Building an Internet ActiveX Control," by using ATL. You will learn the important COM/ActiveX concepts that were skimmed over while you were using MFC. For a fuller examination of these concepts, be sure to read the electronic copy of ìActiveX Programming with Visual C++,î which is included in its entirety on the CD that comes with this book.

Why use the ATL?

Building an ActiveX Control with MFC is quite simple, as you saw in Chapters 17, ìBuilding an ActiveX Control,î and 20, ìBuilding an Internet ActiveX Control.î You can get by without really knowing what a COM interface is, or how to use a type library. Your control can use all sorts of handy MFC classes, like CString and CWnd, can draw itself using CDC member functions, and more. The only downside is that users of your control need the MFC DLLs, and if those DLLs aren't on their system already, the delay while 700K or so of CAB file downloads will be significant.

The alternative is to get the ActiveX functionality from the Active Template Library (ATL) and to call Win32 SDK functions, just as C programmers did when writing for Windows in the days before Visual C++ and MFC. The Win32 SDK is a lot to learn, and won't be covered fully in this chapter. The good news is, if you're familiar with major MFC classes, like CWnd and CDC, you'll recognize a lot of these SDK functions even if you've never seen them before: many of the MFC member functions are just wrappers for SDK functions.

How much download time can you save? The MFC control from Chapter 20, ìBuilding an Internet ActiveX Control.î is about 30K, plus of course the MFC DLLs. The ATL control built in this chapter is at most 100K and is fully self-contained. With a few tricks you could get it down to 50K of control and 20K for the ATL DLLñone-tenth the size of the total control and DLL from Chapter 20!

Using AppWizard to Get Started

There's an AppWizard that knows how to make ATL controls, and it makes your job quite a bit simpler than it would have been without the wizard. As always, choose File, New and click the Projects tab on the New dialog. Fill in an appropriate directory and name the project DieRollControl, as shown in Figure 21.1. Click OK.

Fig. 21.1 AppWizard makes creating an ATL control simple.

It's tempting to name the project DieRoll, but later in this process you will be inserting a control into the projectñthat control will be called DieRoll, so to avoid name conflicts, choose a longer name for the project.

There is only one step in the ATL COM AppWizard and it is shown in Figure 21.2. The default choicesñDLL control, no merging proxy/stub code, no MFC supportñare the right ones for this project. The file extension will be DLL rather than OCX as it was for MFC controls, but that's not an important difference. Click Finish.

Fig. 21.2 Create a DLL control.

The New Project Information dialog box, shown in Figure 21.3, confirms the choices you have made. Click OK to create the project.

Fig. 21.3 Your ATL choices are summarized before you create the project.

Using the Object Wizard

The ATL COM AppWizard created seven files, but you don't have a skeleton control yet. First you have to follow the instructions included in the Step 1 dialog box, and insert an ATL object into the project.

Adding a Control to the project

Choose Insert, New ATL Object from the menu bar. This brings up the ATL Object Wizard, shown in Figure 21.4.

Fig. 21.4 Add an ATL Control to Your Project.

There are several kinds of ATL objects you can add to your project, but at the moment you are interested only in controls, so select Controls in the list box on the left. The choices in the list box on the left become Full Control, Internet Explorer Control, or Property Page. If you know for certain that this control would be used only in Internet Explorer, perhaps as part of an intranet project, you could choose Internet Explorer Control and save a little space. This dieroll control might end up in any browser, in a VB app, or anywhere else for that matter, so a Full Control is the way to go. You'll add a property page later in this chapter. Select Full Control and click Next.

Naming the control

Now the ATL Object Wizard Properties Dialog box appears. The first tab is the Names tab. Here you can customize all the names used for this control. Enter DieRoll for the short name of DieRoll and the rest will default to names based on it, as shown in Figure 21.5. You could change these names if you wanted, but there is no need to. Note that the Type name, DieRoll Class, is the name that will appear in the Insert Object dialog box of most containers. Since the MFC version of DieRoll is probably already in your Registry, having a different name for this version is actually a good thing. On other projects you might consider changing the type name.

Fig. 21.5 Set the names of the files and the control.

Setting control attributes

Click the Attributes tab. Leave the default values: Apartment Threading Model, Dual Interface, and Yes for Aggregation. Select the check boxes Support ISupportErrorInfo and Support Connection Points. Leave Free Threaded Marshaler deselected, as shown in Figure 21.6 atuvc06.pcx. Each of these choices is discussed in the paragraphs that follow.

Fig. 21.6 Set the COM properties of your control

Threading models

Avoid selecting the Single-Thread model even if your controls do not have any threading. In order to be sure that no two functions of such a control are running at the same time, all calls to methods of a single-threaded control must be marshalled through a proxy, which slows execution significantly. The Apartment setting is a better choice for new controls..

The Apartment model refers to STA, or Single-Threaded Apartment model. This means that access to any resources shared by instances of the control (globals and statics) is through serialization. Instance datañlocal automatic variables and objects dynamically allocated on the heapñdoes not need this protection. This makes STA controls faster than single-threaded controls. Internet Explorer exploits STA in controls it contains.

If the design for your control includes a lot of globals and statics, it may be a great deal of work to use the Apartment model. This is not a good reason to write a single-threaded control; it is a good reason to redesign your control as a more object-oriented system.

The Free threading (Multithreaded Apartment or MTA) model refers to controls that are threaded and that already include protection against thread collisions. While it might seem like a great idea to write a multithreaded control, using such a control in a non threaded or STA container will result in marshalling again, this time to protect the container against having two functions called at once. This, too, introduces inefficiencies. As well, there will be significant extra work for you, the developer, to create a free threaded control, since you must add the thread collision protection.

The Both option in the threading column asks the wizard to make a control that can be STA or MTA, avoiding inefficiences when used in a container that is single-threaded or STA, and exploiting the power of MTA models when available. You will have to add the threading-protection work, just as when you write an MTA control.

At the moment, controls for Internet Explorer should be STA. DCOM controls that may be accessed by several connections at once can benefit from being MTA.

Dual and Custom interfaces

COM objects communicate through interfaces, a collection of function names that describe the possible behavior of a COM object. To use an interface, you get a pointer to it and then call a member function of the interface. All Automation servers and ActiveX controls have an IDispatch interface in addition to any other interfaces that may be specific to what the server or control is for. To call a method of a control, you can use the Invoke() method of the IDispatch interface, passing in the dispid of the method you wish to invoke. (This technique was developed so that methods could be called from Visual Basic and other pointerless languages.)

Simply put, a dual-interface control lets you call methods both ways: by using a member function of a custom interface, or by using IDispatch. MFC controls only use IDispatch, but this is slower than using a custom interface. The Interface column on this dialog box lets you choose Dual or Custom: custom leaves IDispatch out of the picture altogether. Select Dual so that the control can be used from Visual Basic if necessary.

Aggregation

The third column, Aggregation, governs whether or not a COM class can use this COM class by containing a reference to an instance of it. Choosing Yes means that other COM objects may use this class; No means they cannot; Only means they must: this object cannot stand alone.

Other control settings

Selecting support for ISupportErrorInfo means that your control will be able to return richer error information to the container. Selecting support for Connection Points is vital for a control, like this one, that will fire events. Selecting Free-Threaded Marshaler is not required for an STA control.

Click the Miscellaneous tab and examine all the settings, which can be left at their default values (see Figure 21.7). The control should be opaque, with a solid background, and should use a normalized DC even though that's slightly less efficient, because your draw code will be much easier to write.

If you'd like to see how a DC is normalized for an ATL control, remember that all the ATL source is available to you, just as the MFC source is. In Program Files\DevStudio\VC\atl\include\ATLCTL.CPP, you will find CComControlBase::OnDrawAdvanced(), which normalizes a DC and calls OnDraw() for you.

Fig. 21.7 Leave the Miscellaneous properties at the defaults.

Supporting Stock Properties

Click the Stock Properties tab to specify which stock properties the control will support. To add support for a stock property, select it in the left Not Supported list box, then click the > button and it will be moved to the Supported list on the right. Add support for Background Color and Foreground Color, as shown in Figure 21.8. If you plan to support a lot of properties, use the >> button to move them all to the supported list and then move back the ones you don't want to support.

Fig. 21.8 Support Background Color and Foreground Color.

Click OK on the Object Wizard to complete the control creation. At this point you can build the project if you wish, though the control does nothing at the moment.

Adding Properties to the Control

The MFC versions of Dieroll featured three stock properties: BackColor, ForeColor and ReadyState. The first two have been added already, but the ReadyState stock properties must be added by hand. As well there are two custom properties, Number and Dots, and an asynchronous property, Image.

Code From Object Wizard

A COM class that implements or uses an interface does so by inheriting from a class representing that interface. Listing 21.1 shows all the classes that CDieRoll inherits from.

Listing 21.1ñExcerpt from DieRollControl.hñInheritance

class ATL_NO_VTABLE CDieRoll : 
     
public CComObjectRootEx<CComSingleThreadModel>,
     public CComCoClass<CDieRoll, 
&CLSID_DieRoll>,
     public CComControl<CDieRoll>,
     public CStockPropImpl<CDieRoll, IDieRoll, &IID_IDieRoll, 
&LIBID_DIEROLLCONTROLLib>,
     public IProvideClassInfo2Impl<&CLSID_DieRoll, NULL, 
&LIBID_DIEROLLCONTROLLib>,
     public 
IPersistStreamInitImpl<CDieRoll>,
     public IPersistStorageImpl<CDieRoll>,
     
public IQuickActivateImpl<CDieRoll>,
     public IOleControlImpl<CDieRoll>,
     public IOleObjectImpl<CDieRoll>,
     
public 
IOleInPlaceActiveObjectImpl<CDieRoll>,
     public IViewObjectExImpl<CDieRoll>,
     public IOleInPlaceObjectWindowlessImpl<CDieRoll>,
     public IDataObjectImpl<CDieRoll>,
     public ISupportErrorInfo,
     public 
IConnectionPointContainerImpl<CDieRoll>,
     public ISpecifyPropertyPagesImpl<CDieRoll>

Now you can see where the T in ATL comes in: all these classes are template classes. (If you aren't familiar with templates, read Chapter 26,"Exceptions, Templates, and the Latest Additions to C++.") You add support for an interface to a control by adding another entry to this list of interface classes from which it inherits.

Notice the names follow the pattern IxxxImpl: that means that this class implements the Ixxx interface. Classes inheriting from IxxxImpl inherit code as well as function names. For example, CDieRoll inherits from ISupportErrorInfo, not ISupportErrorInfoImpl<CDieRoll>, even though such a template does exist. That is because the code in that template implementation class is not appropriate for an ATL control, so the control only inherits the names of the functions from the original interface, and provides code for them in the source file as you will shortly see.

Futher down the header file you will find the COM map, shown in Listing 21.2.

Listing 21.2ñexcerpt from DieRollControl.hñCOM Map

BEGIN_COM_MAP(CDieRoll)
     COM_INTERFACE_ENTRY(IDieRoll)
     COM_INTERFACE_ENTRY(IDispatch)
     
COM_INTERFACE_ENTRY_IMPL(IViewObjectEx)
     COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject2, IViewObjectEx)
     COM_INTERFACE_ENTRY_IMPL_IID(IID_IViewObject, 
IViewObjectEx)
     
COM_INTERFACE_ENTRY_IMPL(IOleInPlaceObjectWindowless)
     COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleInPlaceObject, IOleInPlaceObjectWindowless)
     
COM_INTERFACE_ENTRY_IMPL_IID(IID_IOleWindow, IOleInPlaceObjectWindowless)
     COM_INTERFACE_ENTRY_IMPL(IOleInPlaceActiveObject)
     

COM_INTERFACE_ENTRY_IMPL(IOleControl)
     COM_INTERFACE_ENTRY_IMPL(IOleObject)
     COM_INTERFACE_ENTRY_IMPL(IQuickActivate)
     

COM_INTERFACE_ENTRY_IMPL(IPersistStorage)
     COM_INTERFACE_ENTRY_IMPL(IPersistStreamInit)
     COM_INTERFACE_ENTRY_IMPL(ISpecifyPropertyPages)
     COM_INTERFACE_ENTRY_IMPL(IDataObject)
     COM_INTERFACE_ENTRY(IProvideClassInfo)
     COM_INTERFACE_ENTRY(IProvideClassInfo2)
     COM_INTERFACE_ENTRY(ISupportErrorInfo)
     COM_INTERFACE_ENTRY_IMPL(IConnectionPointContainer)
END_COM_MAP()

This COM map is the connection between IUnknown::QueryInterface() and all the interfaces supported by the control. All COM objects must implement IUnknown, and QueryInterface() can be used to determine what other interfaces the control supports and obtain a pointer to them. The macros connect the Ixxx interfaces to the IxxxImpl classes from which CDieRoll inherits.

Looking back at the inheritance list for CDieRoll, most of the templates take only one parameter, the name of this class, and came from AppWizard. This entry came from ObjectWizard:

     public 
CStockPropImpl<CDieRoll, IDieRoll, &IID_IDieRoll, 
         [ccc]&LIBID_DIEROLLCONTROLLib>,

This line is how ObjectWizard arranged for support for stock properties. Notice there is no indication which properties are supported. Further down the header file, two member variables have been added to CDieRoll:

    OLE_COLOR m_clrBackColor;
    OLE_COLOR m_clrForeColor; 

The ObjectWizard also updated DieRollControl.idl, the interface definition file, to show these two stock properties, as shown in Listing 21.3.

Listing 21.3ñExcerpt From DieRollControl.idlñStock Properties

    [
        object,
        uuid(2DE15F32-8A71-11D0-9B10-0080C81A397C),
        dual,
        helpstring("IDieRoll Interface"),
        pointer_default(unique)
    
]
    interface 
IDieRoll : IDispatch
    {
    [propput, id(DISPID_BACKCOLOR)]
        
HRESULT BackColor([in]OLE_COLOR clr);
    [propget, id(DISPID_BACKCOLOR)]
        HRESULT BackColor([out,retval]OLE_COLOR* pclr);
    
[propput, id(DISPID_FORECOLOR)]
        HRESULT ForeColor([in]OLE_COLOR clr);
    [propget, id(DISPID_FORECOLOR)]
        HRESULT 
ForeColor([out,retval]OLE_COLOR* pclr);
    };

This class will provide all the support for the get and put funtions and will notify the container when one of these properties changes.

Adding the ReadyState Stock Property

Although ReadyState was not on the stock property list in the ATL Object Wizard, it is supported by CStockPropImpl. You can add another stock property by editing the header and idl files. In the header file, immediately after the lines that declared m_clrBackColor and m_clrForeColor, declare another member variable:

.
         long m_nReadyState;

This property will be used in the same way as the ReadyState property in the MFC version of Dieroll: to implement Image as an asynchronous property. In DieRollControl.idl, add these lines to the IDispatch block, after the lines for BackColor and ForeColor:

    [propget, 
id(DISPID_READYSTATE)]
    HRESULT 
ReadyState([out,retval]long* pclr);

You don't need to add a pair of lines to implement put for this property, since external objects cannot update ReadyState. Save the header and idl files to update ClassViewñif you do not, you will not be able to add more properties with ClassView. Expand CDieRoll and IDieRoll in ClassView to see that the member variable has been added to CDieRoll and a ReadyState() function has been added to IDieRoll.

Adding Custom Properties

To add custom properties, you will use an ATL tool similar to the MFC ClassWizard. Right-click on IDieRoll (the top-level one, not the one under CDieRoll) in ClassView to bring up the shortcut menu shown in Figure 21.9, and choose Add Property.

Fig. 21.9 ATL projects have a different ClassView shortcut menu than MFC projects.

The Add Property to Interface dialog box, shown in Figure 21.10, appears. Choose short for the type and fill in Number for the name. Deselect Put Function since containers will not need to change the number showing on the die. Leave the rest of the settings unchanged and click OK to add the property.

Fig. 21.10 Add Number as a Read-Only Property.

Repeat this process for the BOOL Dots, which should have both get and put functions. (Leave the put radio button at PropPut.) The ClassView now shows entries under both CDieRoll and IDieRoll related to these new properties, as shown in Figure 21.11. Try double-clicking on the new entries. For example, double-clicking on get_Dots() under the IDieRoll that is under CDieRoll opens the source (cpp) file scrolled to the get_Dots() function. Double-clicking on Dots() under the top-level IDieRoll opens the idl file scrolled to the propget entry for Dots.

Although a number of entries have been added to CDieRoll, no member variables have been added. Only you can add the member variables that correspond to the new properties. While in many cases it's safe to assume that the new properties are simply member variables of the control class, they might not be. For example, Number might have been the dimension of some array kept within the class rather than a variable of its own..

Add to the header file, after the declarations of m_clrBackColor, m_clrForeColor, and m_nReadyState:

     short 
m_sNumber;
     BOOL 
m_bDots;

In the idl file, the new propget and propput entries use hard coded dispids of 1 and 2, like this:

[propget, id(1), 
helpstring("property Number")] 
    HRESULT Number([out, retval] short *pVal);
[propget, id(2), helpstring("property Dots")] 
    HRESULT Dots([out, retval] BOOL 
*pVal);
[propput, id(2), helpstring("property Dots")] 
    HRESULT Dots([in] BOOL newVal); 

To make the code more readable, use an enum of dispids. Adding the declaration of the enum to the idl file will make it usable in both the idl and header file. Add these lines to the beginning of DieRollControl.idl:

     typedef enum 
propertydispids
          {
               dispidNumber = 1,
          dispidDots = 2,
          
}PROPERTYDISPIDS;

Now you can change the propget and propput lines:

[propget, id(dispidNumber), 
helpstring("property Number")] 
    HRESULT 
Number([out, retval] short *pVal);
[propget, id(dispidDots), helpstring("property Dots")] 
    HRESULT Dots([out, retval] BOOL *pVal);
[propput, id(dispidDots), helpstring("property Dots")] 
    HRESULT Dots([in] BOOL newVal);

The next step is to code the get and set functions to use the member variables. Listing 21.4 shows the completed functions. (If you can't see these in ClassView, expand the IDieRoll under CDieRoll.)

Listing 21.4ñExcerpt from DieRoll.cppñGet and Set Functions

STDMETHODIMP CDieRoll::get_Number(short * pVal)
{
     *pVal = m_sNumber;
     return S_OK;
}
STDMETHODIMP CDieRoll::get_Dots(BOOL * pVal)
{
     *pVal = 
m_bDots;
     return S_OK;
}
STDMETHODIMP 
CDieRoll::put_Dots(BOOL newVal)
{
     if (FireOnRequestEdit(dispidDots) == S_FALSE)
     {
          return S_FALSE;
     }
     m_bDots = newVal;
     SetDirty(TRUE);
     FireOnChanged(dispidDots);
     
FireViewChange();
     return S_OK;
}

The code in the two get functions is simple and straightforward. The put_dots() code is a little more complex, because it fires notifications. FireOnRequestEdit() notifies all the IPropertyNotifySink interfaces that this property is going to change. Any one of these interfaces can deny the request, and if one does then this function will return S_FALSE to forbid the change.

Assuming the change is allowed, the member variable is changed and the control is marked as modified ("dirty") so that it will be saved. The call to FireOnChange() notifies the IPropertyNotifySink interfaces that this property has changed, and the call to FireViewChange() tells the container to redraw the control.

Initializing the Properties

Having added the code to get and set these properties, you should now change the CDieRoll constructor to initialize all the stock and custom properties, as shown in Listing 21.5. A stub for the constructor is in the header file for you to edit.

Listing 21.5ñExcerpt from DieRoll.hñConstructor

    CDieRoll()
    {
        srand( (unsigned)time( NULL ) );
        m_nReadyState = 
READYSTATE_INTERACTIVE;
        m_clrBackColor = 0x80000000 | COLOR_WINDOW;
        m_clrForeColor = 0x80000000 | COLOR_WINDOWTEXT;
        m_sNumber = Roll();
        m_bDots = TRUE;
    
}

At the top of the header, add this line to bring in a declaration of the time() function:

#include "time.h"

Just as you did in the MFC version of this control, you initialize m_sNumber to a random number between 1 and 6, returned by the Roll() function. Add this function to CDieRoll by right-clicking on the class name in ClassView and choosing Add Member Function from the shortcut menu. Roll() is protected, takes no parameters, and returns a short. The code for Roll() is in Listing 21.6, and was explained in Chapter 17, "Building an ActiveX Control."

Listing 21.6ñCDieRoll::Roll()

short 
CDieRoll::Roll()
{
     double number = 
rand();
     number /= RAND_MAX + 1;
     number 
*= 6;
     return (short)number + 1;
}

Adding the Asynchronous Property

Just as in Chapter 20, "Building an Internet ActiveX Control," the Image property represents a bitmap to be loaded asynchronously and used as a background image. Add the property to the interface just as Number and Dots were added. Use BSTR for the type and Image for the name. Update the enum in the idl file, so that dispidImage is 3, and edit the propget and propput lines in the idl file to use the enum value:

[propget, id(dispidImage), helpstring("property Image")] 
    HRESULT Image([out, retval] BSTR *pVal);
[propput, id(dispidImage), helpstring("property Image")] 
    HRESULT Image([in] BSTR 
newVal);

Add a member variable, m_bstrImage, to the class:

CComBSTR m_bstrImage;

CComBSTR is an ATL wrapper class with useful member functions for manipulating a BSTR.

A number of other member variables must be added to handle the bitmap and the asynchronous loading. Add these lines to DieRoll.h:

     HBITMAP 
hBitmap;
     BITMAPINFOHEADER bmih;
     char *lpvBits;
     BITMAPINFO *lpbmi;
     
HGLOBAL hmem1;
     HGLOBAL hmem2;
     BOOL BitmapDataLoaded;
     char *m_Data;
     
unsigned long m_DataLength;

The first six of these new variables are used to draw the bitmap and will not be discussed. The last three combine to achieve the same behavior as the data path property used in the MFC version of this control.

Add these three lines to the constructor:

        m_Data = 
NULL;
        m_DataLength = 0;
        BitmapDataLoaded = 
FALSE;

Add a destructor to CDieRoll (in the header file) and add the code in listing 21.7.

Listing 21.7ñCDieRoll::~CDieRoll()

    ~CDieRoll()
    
{
        if (BitmapDataLoaded)
        
{
            GlobalUnlock(hmem1);
            
GlobalFree(hmem1);
            GlobalUnlock(hmem2);
            GlobalFree(hmem2);
            BitmapDataLoaded = 
FALSE;
        }
        if (m_Data != NULL)
        {
            delete 
m_Data;
        }
    }

The Image property has get and put functions. Code them as in Listing 21.8.

Listing 21.8ñDieRoll.cppñget_Image() and put_Image()

STDMETHODIMP CDieRoll::get_Image(BSTR * 
pVal)
{
    *pVal = 
m_bstrImage.Copy();
    return S_OK;
}
STDMETHODIMP CDieRoll::put_Image(BSTR newVal)
{
    USES_CONVERSION;
    if (FireOnRequestEdit(dispidImage) == 
S_FALSE)
    {
        return S_FALSE;
    }
// if there was an old bitmap or data, delete 
them
    if 
(BitmapDataLoaded)
    {
        GlobalUnlock(hmem1);
        
GlobalFree(hmem1);
        
GlobalUnlock(hmem2);
        GlobalFree(hmem2);
        BitmapDataLoaded = FALSE;
    }
    
if (m_Data != NULL)
    {
        delete m_Data;
    }
    m_Data = NULL;
    m_DataLength = 0;
    m_bstrImage = newVal;
    LPSTR string = 
W2A(m_bstrImage);
    if (string != 
NULL && strlen(string) > 0)
    {
        // not a null string so try to load 
it
        BOOL relativeURL = 
FALSE;
        if (strchr(string, ':') == NULL)
        {
            
relativeURL = TRUE;
        
}
        m_nReadyState = READYSTATE_LOADING;
        HRESULT ret = 
CBindStatusCallback<CDieRoll>::Download(this, 
            
OnData, m_bstrImage, m_spClientSite, relativeURL);
    }    else
    
{
        // was a null string so don't try to load 
it
        m_nReadyState = READYSTATE_INTERACTIVE;
}
    SetDirty(TRUE);
    
FireOnChanged(dispidImage);
    return S_OK;
}

As with Numbers and Dots, the get function is straightforward, and the put function is a bit more complicated. The beginning and end of the function is like put_Dots(), firing notifications to check if the variable can be changed, then other notifications that it was changed. In between is the code unique to an asynchronous property.

To start the download of the asynchronous property, this function will call CBindStatusCallback<CDieRoll>::Download() but first it needs to determine whether the URL in m_bstrImage is a relative or absolute URL. Use the ATL macro W2A to convert the wide BSTR to an ordinary C string so that the C function strchr() can be used to search for a : character in the URL. A URL with no : in it is assumed to be a relative URL.

A BSTR is a wide (double-byte) character on all 32-bit Windows platforms. It is a narrow (single-byte) string on a PowerMac.

In the MFC version of the dieroll control with an asynchronous image property, whenever a block of data came through, the OnDataAvailable() function was called. The call to Download() arranges for a function called OnData() to be called when data arrives. You will write the OnData() function. Add it to the class with the other public functions and add the implementation shown in Listing 21.9 to DieRoll.cpp.

Listing 21.9ñDieRoll.cppñCDieRoll::OnData()

void CDieRoll::OnData(CBindStatusCallback<CDieRoll>* pbsc, 
                      
BYTE * 
pBytes, DWORD dwSize)
{
    char *newData = new char[m_DataLength + dwSize];
    memcpy(newData, m_Data, 
m_DataLength);
    memcpy(newData+m_DataLength, pBytes, dwSize);
    m_DataLength += dwSize;
    delete m_Data;
    m_Data = 
newData;
    if (ReadBitmap())
    {
        m_nReadyState = 
READYSTATE_COMPLETE;
}
}

Since there is no realloc() when using new, this function uses new to allocate enough chars to hold the data that has already been read (m_DataLength) and the new data that is coming in (dwSize), then copies m_Data to this block, and the new data (pBytes) after m_Data. It then attempts to convert the data that has been recieved so far into a bitmap: if this succeeds the download must be complete and the call to FireViewChange() sends a notification to the container to redraw the view. You can get the ReadBitmap() function from the CD and add it to your project: it's much like the MFC version but it does not use any MFC classes like Cfile.

Drawing the Control

Now that all the properties have been added, you can code OnDraw(). While the basic structure of this function is the same as in the MFC version of Chapter 20, ìBuilding an Internet ActiveX Control,î there is a lot more work to be done because you cannot rely on MFC to do some of it for you.

The structure of OnDraw() is:

HRESULT CDieRoll::OnDraw(ATL_DRAWINFO& di)
// if the bitmap is 
ready, draw it
// else draw a plan background using BackColor
// if !Dots draw a number in ForeColor
// else draw the 
dots

First, you need to test if the bitmap is ready, and draw it if possible. This code is in Listing 21.10: add it to the existing OnDraw() in place of the code left for you by AppWizard. Notice that if ReadyState is READYSTATE_COMPLETE but the call to CreateDIBitmap() doesn't result in a valid bitmap handle, the bitmap member variables are cleared away to make subsequent calls to this function give up a little faster. This chapter will not discuss how to draw bitmaps.

Listing 21.10ñCDieRoll::OnDraw()ñUse the Bitmap

    int width = (di.prcBounds->right - 
di.prcBounds->left + 1);
    int height = (di.prcBounds->bottom - 
di.prcBounds->top + 1); 
    BOOL drawn = FALSE;
    if 
(m_nReadyState == READYSTATE_COMPLETE)
    {
        if (BitmapDataLoaded)
        {
            
hBitmap = ::CreateDIBitmap(di.hdcDraw, &bmih, CBM_INIT, lpvBits, 
                lpbmi, DIB_RGB_COLORS);
            if (hBitmap)
            {
                HDC hmemdc;
                hmemdc = ::CreateCompatibleDC(di.hdcDraw);
                
::SelectObject(hmemdc, hBitmap);
                DIBSECTION 
ds;
                ::GetObject(hBitmap,sizeof(DIBSECTION),(LPSTR)&ds);
                ::StretchBlt(di.hdcDraw,
                                
di.prcBounds->left, // left
                                
di.prcBounds->top,  // top
                                width, // target width
                                height, // target height
                                hmemdc,        // the image
                                0,            
//offset into image -x
                                
0,            //offset into image -y
                                ds.dsBm.bmWidth, // width
                                ds.dsBm.bmHeight, // height
                                SRCCOPY);    //copy it over
                drawn = 
TRUE;
                ::DeleteObject(hBitmap);
                hBitmap = NULL;
                ::DeleteDC(hmemdc);
            }
            
else
            {
                GlobalUnlock(hmem1);
                GlobalFree(hmem1);
                
GlobalUnlock(hmem2);
                GlobalFree(hmem2);
                
BitmapDataLoaded = FALSE;
            }
        }
    }

If the bitmap was not drawn, either because ReadyState is not READYSTATE_COMPLETE yet or because there was a problem with the bitmap, OnDraw() draws a solid background by using the BackColor property, as shown in Listing 21.11. Add this code to OnDraw(). The SDK calls are very similar to the MFC calls used in the MFC version of DieRoll: for example ::OleTranslateColor() corresponds to TranslateColor().

Listing 21.11ñCDieRoll::OnDraw()ñDraw a solid background

    if (!drawn)
    {
        COLORREF back;
        
::OleTranslateColor(m_clrBackColor, NULL, &back);
        HBRUSH backbrush = ::CreateSolidBrush(back);
        ::FillRect(di.hdcDraw, (RECT *)di.prcBounds, 
backbrush);
        ::DeleteObject(backbrush);
    }

With the background drawn, either as a bitmap image or a solid color, OnDraw() must now tackle the foreground. Getting the foreground color is quite simple. Add these two lines to OnDraw():

    COLORREF fore;
    
::OleTranslateColor(m_clrForeColor, NULL, &fore);

If Dots is FALSE, the die should be drawn with a number on it. Add the code in Listing 21.12 to OnDraw(). Again the SDK functions do the same job as the similarly named MFC functions used in the MFC version of DieRoll.

Listing 21.12ñCDieRoll::OnDraw()ñDraw a Number

     if (!m_bDots)
     {
        _TCHAR val[20]; //character representation of the short value
        _itot(m_sNumber, val, 10);
            
::SetTextColor(di.hdcDraw, 
fore);
        ::ExtTextOut(di.hdcDraw, 0, 0, ETO_OPAQUE, 
            (RECT *)di.prcBounds, val, _tcslen(val), NULL );  
     
}

The code that draws dots is in Listing 21.13. Add it to OnDraw() to complete the function. This code is long, but was explained in Chapter 17, "Building an ActiveX Control." As in the rest of OnDraw(), MFC function calls have been replaced with SDK calls.

Listing 21.12ñCDieRoll::OnDraw()ñDraw Dots

     
else
     {
         //dots are 4 units wide and high, one unit from the edge
         int Xunit = width/16;
         
int Yunit = height/16;
         int Xleft = width%16;
         int Yleft = height%16;
         // adjust top 
left by amount left 
over
         int Top = di.prcBounds->top + Yleft/2;
         int Left = di.prcBounds->left + Xleft/2; 
         
HBRUSH 
forebrush;
         forebrush = ::CreateSolidBrush(fore);
         HBRUSH savebrush = (HBRUSH)::SelectObject(di.hdcDraw, forebrush);
         switch(m_sNumber)
         {
         case 1:
             ::Ellipse(di.hdcDraw, Left+6*Xunit, 

Top+6*Yunit,
                            Left+10*Xunit, Top + 10*Yunit); //center
              break;
         case 
2:
              ::Ellipse(di.hdcDraw, Left+Xunit, Top+Yunit,
                            Left+5*Xunit, Top + 5*Yunit);   //upper left
              
::Ellipse(di.hdcDraw, Left+11*Xunit, Top+11*Yunit,
                            Left+15*Xunit, Top + 15*Yunit); //lower right
              
break;
         case 3:
              ::Ellipse(di.hdcDraw, Left+Xunit, Top+Yunit,
                            Left+5*Xunit, Top + 
5*Yunit);   //upper 
left
              ::Ellipse(di.hdcDraw, Left+6*Xunit, Top+6*Yunit,
                            Left+10*Xunit, Top + 10*Yunit); //center
              ::Ellipse(di.hdcDraw, Left+11*Xunit, Top+11*Yunit,
                            Left+15*Xunit, Top + 15*Yunit); //lower right
              

break;
         case 4:
              ::Ellipse(di.hdcDraw, Left+Xunit, Top+Yunit,
                            Left+5*Xunit, Top + 

5*Yunit);   //upper left
              ::Ellipse(di.hdcDraw, Left+11*Xunit, Top+Yunit,
                            Left+15*Xunit, Top + 5*Yunit);  //upper 
right
              ::Ellipse(di.hdcDraw, Left+Xunit, Top+11*Yunit,
                            Left+5*Xunit, Top + 15*Yunit);  //lower left
              
::Ellipse(di.hdcDraw, Left+11*Xunit, Top+11*Yunit,
                            Left+15*Xunit, Top + 15*Yunit); //lower right
              
break;
         case 5:
              ::Ellipse(di.hdcDraw, Left+Xunit, Top+Yunit,
                            Left+5*Xunit, Top + 
5*Yunit);   //upper 
left
              ::Ellipse(di.hdcDraw, Left+11*Xunit, Top+Yunit,
                            Left+15*Xunit, Top + 5*Yunit);  //upper 
right
              ::Ellipse(di.hdcDraw, Left+6*Xunit, Top+6*Yunit,
                            Left+10*Xunit, Top + 10*Yunit); //center
              
::Ellipse(di.hdcDraw, Left+Xunit, Top+11*Yunit,
                            Left+5*Xunit, Top + 15*Yunit);  //lower left
              
::Ellipse(di.hdcDraw, Left+11*Xunit, 
Top+11*Yunit,
                            Left+15*Xunit, Top + 15*Yunit); //lower right
              break;
         case 
6:
               ::Ellipse(di.hdcDraw, Left+Xunit, Top+Yunit,
                    Left+5*Xunit, Top + 5*Yunit);   //upper left
               
::Ellipse(di.hdcDraw, Left+11*Xunit, Top+Yunit,
                    Left+15*Xunit, Top + 5*Yunit);  //upper right
               
::Ellipse(di.hdcDraw, Left+Xunit, 
Top+6*Yunit,
                    Left+5*Xunit, Top + 10*Yunit);  //center left
               ::Ellipse(di.hdcDraw, Left+11*Xunit, 
Top+6*Yunit,
                      Left+15*Xunit, Top + 10*Yunit); //center right
               ::Ellipse(di.hdcDraw, Left+Xunit, Top+11*Yunit,
                    
Left+5*Xunit, Top + 15*Yunit);  //lower left
               ::Ellipse(di.hdcDraw, Left+11*Xunit, Top+11*Yunit,
                    
Left+15*Xunit, Top + 15*Yunit); //lower 
right
               break;
         }
         ::SelectObject(di.hdcDraw, 
savebrush);
         
::DeleteObject(forebrush);
     }
    return S_OK;
}

Persistence and a Property Page

The properties have been added to the control and used in the drawing of the control. Now all that remains is to make the properties persistent and to add a property page.

Adding a Property Page

Choose Insert, New ATL Object from the menu bar to bring up the ATL Object Wizard. Select Controls in the left pane and Property Page in the right pane, then click Next. On the Names tab, enter DieRollPPG for the Short Name, and click the Strings tab (the sttings on the Attributes tab will not be changed.) Enter General for the Title and DieRoll Property Page for the Doc String. Blank out the Helpfile Name. Click OK to add the property page to the project.

Switch to ResourceView in the Workspace pane, and open the dialog IDD_DIEROLLPPG. Add a check box with the resource ID IDC_DOTS and the caption Display Dot Pattern and an edit box with the resource ID IDC_IMAGE labelled Image URL, as shown in Figure 21.12.

Fig. 21.12 Add two controls to the property page.

At the top of DieRollPPG.h, add this line:

#include "DieRollControl.h"

You need to connect the controls on this property page to properties of the dieroll control. The first step is to add three lines to the message map in DieRollPPG.h so that it resembles Listing 21.13.

Listing 21.13ñDieRollPPG.hñMessage Map

BEGIN_MSG_MAP(CDieRollPPG)
    MESSAGE_HANDLER(WM_INITDIALOG, 
OnInitDialog)
    
COMMAND_HANDLER(IDC_DOTS, BN_CLICKED, OnDotsChanged)
    COMMAND_HANDLER(IDC_IMAGE, EN_CHANGE, OnImageChanged)
    

CHAIN_MSG_MAP(IPropertyPageImpl<CDieRollPPG>)
END_MSG_MAP()

These new lines ensure that OnInitDialog() will be called when the dialog box is initialized, and OnDotsChanged() or OnImageChanged() will be called whenever Dots or Image are changed (the other properties don't have put methods and so can't be changed.)

Add the code in Listing 21.14 to the header file to declare and implement OnInitDialog().

Listing 21.14ñDieRollPPG.hñCDieRollPPG::OnInitDialog()

    

LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, 
                         BOOL & bHandled)
    {
        

USES_CONVERSION;
        CComQIPtr<IDieRoll, &IID_IDieRoll> pDieRoll(m_ppUnk[0]);
        BOOL dots;
        

pDieRoll->get_Dots(&dots);
        ::SendDlgItemMessage(m_hWnd, IDC_DOTS, BM_SETCHECK, dots, 0L); 
        BSTR image;
        

pDieRoll->get_Image(&image);
        LPTSTR image_URL = W2T(image);
        SetDlgItemText(IDC_IMAGE, image_URL);
        
return 
TRUE;
    }

This code starts by declaring a pointer to an IDieRoll interface using the CComQIPtr template class and initializing it to the first element of the m_ppUnk array in this class, CDieRollPPG. (A property page can be associated with multiple controls.) The constructor for the CComQIPtr template class uses the QueryInterface() method of the IUnknown pointer that was passed in to the constructor to find a pointer to an IDieRoll interface. Now you can call member functions of this interface to access the properties of the DieRoll control.

Getting the value of the Dots property of the CDieRoll object is simple enough: just call get_Dots(). To use that value to initialize the check box on the property page, send a message to the control using the SDK function ::SendDlgItemMessage(). The BM_SETCHECK parameter indicates that you are setting whether the box is checked (selected) or not. Passings dots as the fourth parameter ensures that IDC_DOTS will be selected if dots is TRUE and deselected if dots is FALSE. Similarly, get the URL for the image with get_Image(), convert it from wide characters, then use SetDlgItemText() to set the edit box contents to that URL.

OnDotsChanged() and OnImageChanged() are quite simple: add the code in Listing 21.15 to the header file, after OnInitDialog().

Listing 21.15ñDieRollPPG.hñThe OnChanged functions

    LRESULT OnDotsChanged(WORD wNotify, WORD wID, HWND hWnd, BOOL& bHandled)
    {
        SetDirty(TRUE);
        
return FALSE;
    }
    LRESULT 
OnImageChanged(WORD wNotify, WORD wID, HWND hWnd, BOOL& bHandled)
    {
        SetDirty(TRUE);
        return FALSE;
    }

The calls to SetDirty() in these functions ensure that the Apply() function will be called when the user clicks OK on the property page.

The ObjectWizard generated a simple Apply() function, but it doesn't affect the Dots or Number properties. Edit Apply() so that it resembles listing 21.16.

Listing 21.16ñDieRollPPG.hñCDieRollPPG::Apply()

    STDMETHOD(Apply)(void)
    {
        USES_CONVERSION;
        BSTR image = NULL;
        GetDlgItemText(IDC_IMAGE, image);
        
BOOL dots = (BOOL)::SendDlgItemMessage(m_hWnd, IDC_DOTS, 
                BM_GETCHECK, 0, 0L); 
        ATLTRACE(_T("CDieRollPPG::Apply\n"));
        for (UINT i = 0; i < m_nObjects; i++)
        {
            CComQIPtr<IDieRoll, &IID_IDieRoll> 
pDieRoll(m_ppUnk[i]);
            if FAILED(pDieRoll->put_Dots(dots))
            {
                CComPtr<IErrorInfo> 
pError;
                CComBSTR            strError;
                GetErrorInfo(0, 
&pError);
                
pError->GetDescription(&strError);
                MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION);
                return 
E_FAIL;
            }
            if FAILED(pDieRoll->put_Image(image))
            
{
                 CComPtr<IErrorInfo> pError;
                CComBSTR            strError;
                GetErrorInfo(0, 
&pError);
                pError->GetDescription(&strError);
                MessageBox(OLE2T(strError), _T("Error"), MB_ICONEXCLAMATION);
                
return E_FAIL;
            }
        }
        m_bDirty = FALSE;
        return 
S_OK;
    }

Apply starts by getting dots and image from the dialog box. Notice in the call to ::SendDlgItemMessage() that the third parameter is BM_GETCHECK, so this call gets the selected state (TRUE or FALSE) of the check box. Then a call to ATLTRACE prints a trace message to aid debugging. Like the trace statements discussed in Chapter 24, "Improving Your Application's Performance," this statement disappears in a release build.

The majority of Apply() is a for loop that is executed once for each control associated with this property page. It gets an IDieRoll interface pointer just as in OnInitDialog(), and tries calling the put_Dots() and put_Image() member functions of that interface. If either calls fails, a message box informs the user of the problem. After the loop, the m_bDirty member variable can be set to FALSE.

Connecting the Property Page to CDieRoll

The changes to CDieRollPPG are complete. There are some changes to be made to CDieRoll to connect it to the property page class. Specifically, the property map needs some more entries. Edit it until it looks like Listing 21.17.

Listing 21.16ñDieRollPPG.hñCDieRollPPG::Apply()

BEGIN_PROPERTY_MAP(CDieRoll)
    PROP_ENTRY( 

"Dots", dispidDots, CLSID_DieRollPPG)
    PROP_ENTRY( "Image", dispidImage, CLSID_DieRollPPG)
    PROP_ENTRY( "Fore Color", DISPID_FORECOLOR, 

CLSID_StockColorPage )
    PROP_ENTRY( "Back Color", DISPID_BACKCOLOR, CLSID_StockColorPage )
END_PROPERTY_MAP()

For Dots and Image the entries are new: for Fore Color and Back Color, replacing the PROP_PAGE macro supplied by ObjectWizard with PROP_ENTRY macros ensures that the properties will persist, that is they will be saved with the control.

Persistence in a Property Bag

There are a number of different ways that Internet Explorer can get property values out of some HTML and into a control wrapped in an <OBJECT> tag. With stream persistence, provided by default, you use a DATA attribute in the <OBJECT> tag. If you would like to use <PARAM> tags, which are far more readable, the control must support property bag persistence through the IPersistPropertyBag interface.

Add another class to the list of base classes at the start of the CDieRoll class:

     public IPersistPropertyBagImpl<CDieRoll>,

Add this line to the COM map:

     
COM_INTERFACE_ENTRY_IMPL(IPersistPropertyBag)

Now you can use <PARAM> tags to set properties of the control.

Using the Control in Control Pad

You've added a lot of code to CDieRoll and CDieRollPPG, and it's time to build the control. After fixing any typos or minor errors, you can actually use the control.

You are going to build the HTML to display this control in Microsoft's Control Pad. If you don't have Control Pad, it's freely downloadable from http://www.microsoft.com/workshop/author/cpad/download.htm. If you have a copy of Control Pad from before January 1997, get the latest one. If you use the old version, the init safe and script safe work you'll do later in this chapter will appear not to function properly.

When you start Control pad it makes an empty HTML document. With the cursor between <BODY> and </BODY>, choose Edit, Insert ActiveX Control. The Insert ActiveX Control dialog appears: choose DieRoll Class from the list (you may recall from Figure 21.5 that the type name for this control is DieRoll Class) and click OK. The control and a Properties dialog, appear. Click on the Image property and enter the full path to the file beans.bmp (available on the CD) in the edit box at the top of the Properties dialog. Click Apply, and the control redraws with a background of jelly beans, as shown in Figure 21.13. Close the Properties dialog and the Edit ActiveX Control dialog and you will see the HTML generated for you, including the <PARAM> tags that were added because Control Pad could determine that DieRoll supports the IPersistPropertyBag interface.

Fig. 21.13 Inserting the control into Control Pad displays it for you.

The control does not have its full functionality yet: it does not roll itself when you click on it. The next section will add events.

Adding Events

There are two events to be added: one when the user clicks on the control, and one when the ready state changes. The Click event was discussed in Chapter 17, "Building an ActiveX Control," and the ReadyStateChanged event was discussed in Chapter 20, "Building an Internet ActiveX Control."

Editing the idl file

There is no ATL Add Event dialog: you will add the events entirely by hand. First, you will need a guid for the event interface. This is a globally unique identifier and should be generated on your own machine with the guidgen utility. Choose Start, Run, enter guidgen and click OK. The Create GUID dialog box, shown in Figure 21.14, appears. Select Registry Format, which is closest to the format used in idl files, then Click Copy to copy the new guid into the clipboard. Click Exit, and return to Developer Studio.

Fig. 21.14 Generate a guid for your event interface.

You will add the lines in Listing ATL. to the idl file, in the library section, after the two importlib statements. Paste in your new guid in place of the one you see in listing 21.17.

Listing 21.17ñDieRoll.idlñLines to add to the library section

   [
        
uuid(6E46C460-8C00-11d0-9B12-0080C81A397C),
        helpstring("Event interface for 
DieRoll")
   ]
   dispinterface 
_DieRollEvents
   {
      properties:
      methods:
     [id(DISPID_CLICK)] void 
Click();
     [id(DISPID_READYSTATECHANGE)] void ReadyStateChange();    
   };

A few lines further down, in the coclass block for DieRoll, add this line:

[default, source] dispinterface _DieRollEvents;

A Wrapper Class for the IConnectionPoint Interface

In order to fire events, you will implement the IConnectionPoint interface. The ATL Proxy component gets you started. First, save the idl file and build the project so that the typelib associated with the project is up to date.

Choose Project, Add to Project, Components and Controls. The Components and Controls Gallery dialog comes up. Double click on Developer Studio Components. Select ATL Proxy Generator, as shown in Figure 21.15, and click Insert. Click OK when asked if you want to insert the component or not.

Fig. 21.15 Insert an ATL Proxy Generator component.

Click the ... button to browse for the typelib file. Select DieRollControl.tlb and click Open. In the Not Selected listbox on the left, click _DieRollEvents, then click > to move it to the Selected pane. Select Connection Point for Proxy Type, as shown in Figure 21.16, and click Insert.

Fig. 21.16 Select the new interface component, then click Insert.

A save dialog will come up and prompt you to save it in CPDieRollControl.h. Click Save. Click OK on the "Successfully generated proxy." message, and close the ATL Proxy Generator dialog, then close the Components and Controls Gallery dialog.

The proxy generator created a wrapper class, CProxy_DieRollEvents, in CPDieRollControl.h. Add this include statement to DieRoll.h:

#include "CPDieRollControl.h"

To add this interface to CDieRoll, add it to the inheritance list at the top of the class:

     public CProxy_DieRollEvents<CDieRoll>,

The new connection point must be added to the empty connection point map. It should look like this:

BEGIN_CONNECTION_POINT_MAP(CDieRoll)
    CONNECTION_POINT_ENTRY(DIID__DieRollEvents)
END_CONNECTION_POINT_MAP()

The general preparation is complete.

Firing the Click event

When the user clicks on the control, it should fire a Click event. Add an entry to the message map:

     MESSAGE_HANDLER(WM_LBUTTONDOWN, OnLButtonDown)

Add the member function OnLButtonDown() to CDieRoll and add the code shown in Listing 21.18.

Listing 21.18ñCDieRoll::OnLButtonDown()

LRESULT 

CDieRoll::OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL & bHandled)
{
   m_sNumber = Roll();
   

FireOnChanged(dispidNumber);
   Fire_Click();
   FireViewChange();
   return 0;
}

This code rolls the die, fires a notification that Number has changed, fires a Click event, and notifies the container that the control should be redrawn.

Firing the ReadyStateChange Event

put_Image() and OnData() can now fire events when the ready state changes. Look for a line in put_Image() like this:

    
m_nReadyState = READYSTATE_LOADING;

Add immediately after that line:

    Fire_ReadyStateChange();

Then find this line:

    m_nReadyState = 
READYSTATE_INTERACTIVE;

Add the call to Fire_ReadyStateChange() after this line as well. In OnData(), find this line:

    m_nReadyState = 
READYSTATE_COMPLETE;

Build the control again, and insert it into a new page in Control Pad. Click on the die in the Edit ActiveX Control window, and it will roll a new number each time that you click. As another test, bring up the ActiveX Control Test container (available from the Tools menu in Developer Studio) and insert a control, then use the event log to confirm that Click and ReadyStateChange events are being fired.

Exposing the DoRoll() function

The next stage in the development of this control is to expose a function that will allow the container to roll the die. One use for this is to arrange for the container to roll one die whenever the other is clicked. Right-click on the IDieRoll interface in ClassView and choose Add Method. Enter DoRoll for Method Name, and leave the parameters section blank. Click OK.

Functions have a dispid just as properties do. Add an entry to the enum of dispids in the idl file, so that dispidDoRoll is 4. This ensures that if you add another property later, you won't collids with the dispid for DoRoll(). When you added the function to the interface, a line was added after the get and put entries for the properties. Change it to use the new dispid, so it looks like this:

[id(dispidDoRoll), helpstring("method DoRoll")] 
HRESULT DoRoll();

The code for DoRoll() is in Listing 21.19. Add it to DieRoll.cpp.

Listing 21.19ñCDieRoll::DoRoll()

STDMETHODIMP CDieRoll::DoRoll()
{
     m_sNumber = Roll();
     
FireOnChanged(dispidNumber);
     FireViewChange();
     return S_OK;
} 

This code is just like OnLButtonDown but does not fire a Click event.

Registering as init Safe and script Safe

In Chapter 20, ìBuilding an Internet ActiveX Control,î you added Registry entries to indicate that the control was safe to accept parameters in a Web page and to interact with a script. For an ATL control you can achieve this by supporting the IObjectSafety interface. A container will query this interface to see if the control is safe.

Add this following line to the inheritance list for CDieRoll:

     public 
IObjectSafetyImpl<CDieRoll>,

Add this line to the COM map:

COM_INTERFACE_ENTRY_IMPL(IObjectSafety)

This will automatically make the control script safe. The default behavior in ATL does not make controls init safe. To make this control init safe, you will override the default definition of IObjectSafetyImpl::GetInterfaceSafetyOptions() and IObjectSafetyImpl::SetInterfaceSafetyOptions().

The code for these functions, modified from the default ATL source in DevStudio\VC\ATL\INCLUDE\ATLCTL.H, is in listing 21.20 and ATL.21. Add these functions after the destructor in DieRoll.h.

Listing 21.20ñCDieRoll::GetInterfaceSafetyOptions()

    
STDMETHOD(GetInterfaceSafetyOptions)(REFIID riid, 
          DWORD *pdwSupportedOptions, DWORD *pdwEnabledOptions)
    {
        

ATLTRACE(_T("IObjectSafetyImpl::GetInterfaceSafetyOptions\n"));
        if (pdwSupportedOptions == NULL || pdwEnabledOptions == NULL)
            return 

E_POINTER;
        *pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER 
          || INTERFACESAFE_FOR_UNTRUSTED_DATA;
        

*pdwEnabledOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER 
          || INTERFACESAFE_FOR_UNTRUSTED_DATA;
        return S_OK;
    

}

Listing 21.20ñCDieRoll::DoRoll()

    STDMETHOD(SetInterfaceSafetyOptions)(REFIID riid, 
                                         DWORD dwOptionSetMask, 

                                         DWORD dwEnabledOptions)
    {
        

ATLTRACE(_T("IObjectSafetyImpl::SetInterfaceSafetyOptions\n"));
        return S_OK;
    }

The changes here are that the supported and enabled options are INTERFACESAFE_FOR_UNTRUSTED_DATA and INTERFACESAFE_FOR_UNTRUSTED_CALLER instead of just INTERFACESAFE_FOR_UNTRUSTED_CALLER. Instead of restricting these options to only the IDispatch interface, this code reports the control is safe to all interfaces.

Preparing the Control for Use in Design Mode

When a developer is building a form or dialog box in an application like Visual Basic or Visual C++, a control palette makes it simple to identify the controls to be added. Building the icon used on that palette is the next step in completing this control.

Create a bitmap resource by choosing Insert, Resource and double-clicking Bitmap. Choose View, Properties and set both the height and width to 16. Change the resource ID to IDB_DIEROLL and draw the icon shown in Figure 21.17.

Fig. 21.17 Draw an icon for the control.

The Registry Script for this control refers to this icon by resource number. To discover whatnumber has been assigned to IDB_DIEROLL, choose View, Resource Symbols and note the numeric value associated with IDB_DIEROLL. (On the machine where this sample was written, it is 202.) Open DieRoll.rgs (the script file) from FileView and look for this line:

ForceRemove 
'ToolboxBitmap32' = s '%MODULE%, 
1'

Change it to:

ForceRemove 'ToolboxBitmap32' = s '%MODULE%, 202'

Be sure to use your value rather than 202. Build the control again. Run the Control Pad again and choose File, New HTML Layout Control. Select the Additional tab on the Toolbox palette and then right-click on the page. From the shortcut menu that appears, choose Additional Controls. Find DieRoll Class on the list and select it, then click OK. The new icon appears on the Additional tab, as shown in Figure 21.18.

Fig. 21.18 Add the DieRoll class to the HTML Layout toolbox.

Minimizing Executable Size

Up until now, you have been building debug versions of the control. Dieroll.dll is about 300K. While that's a lot smaller than the 700K of cab file for the MFC DLLs that the MFC version of dieroll might require, it's a lot larger than the 30K or so that the release version of dieroll.ocx takes up. With development complete, it's time to build a release version.

Choose Build, Set Active Configuration to bring up the Set Active Configuration dialog shown in Figure 21.19. You will notice that there are twice as many release versions in an ATL project as in an MFC project: in addition to choosing whether or not you suport Unicode, you must choose "MinSize" or "MinDependency."

Fig. 21.19 Choose a build type from the Set Active Configuration dialog box.

The minimum size release version makes the control as small as possible by linking dynamically to an ATL DLL and the ATL Registrar. The minimum dependencies version links to these statically, which makes the control larger but self contained. If you choose minimum size, you will need to set up cab files for the control and the DLLs as discussed in Chapter 20,îBuilding an Internet ActiveX Control,î for the MFC DLLs. At this early stage of ATL acceptance, it's probably better to choose minimum dependencies.

If you choose minimum dependency and build, you will get these error messages from the linker:

Linking...
   Creating library ReleaseMinDependency/DieRollControl.lib and 
[ccc]object 
ReleaseMinDependency/DieRollControl.exp
LIBCMT.lib(crt0.obj) : error LNK2001: unresolved external symbol _main
ReleaseMinDependency/DieRollControl.dll : 
[ccc]fatal error LNK1120: 1 unresolved 
externals
Error executing link.exe.
DieRollControl.dll - 2 error(s), 0 warning(s)

This error is not because of any mistake you may have made. By default, ATL release builds use a tiny version of the C runtime library (CRT) so that they will build as small a DLL as possible. This minimal CRT does not include the time(), rand(), and srand() functions used to roll the die. The linker finds these functions in the full-size CRT, but that library expects a main() function in your control. Since there isn't one, the link fails.

This behaviour is controlled with a linker setting. Choose Project, Settings. From the drop down box at the upper left, choose Win32 Release MinDependency. Click the C/C++ tab on the right. Click in the Preprocessor Definitions box and press END to move to the end of the box. Remove the _ATL_MIN_CRT flag, highlighted in Figure 21.20, and the comma immediately before it. Build the project again, and the linker errors disappear.

Fig. 21.20 Turn off the flag that links in only a tiny version of the C runtime library.

If you comment out the calls to rand(), srand(), and time(), so that the control no longer works, it will link with _ATL_MIN_CRT into a 48K DLL. With _ATL_MIN_CRT removed, it is 104Kña significant increase, but still substantially smaller than the MFC control and its DLLs. A minimum size release build with _ATL_MIN_CRT removed is 98K: the savings is hardly worth the trouble to package up the ATL DLLs. With rand(), srand(), and time() commented out, a minimum size release build with _ATL_MIN_CRT left in is only 45K.

Removing the _ATL_MIN_CRT flag increases the size of the control by over 50K. Although there is no way to rewrite this control so that it doesn't need the rand(), srand(), and time() functions, you could write your own versions of them and include them in the project so that the control would still link with the _ATL_MIN_CRT flag. You can find algorithms for random number generators and their seed functions in books of algorithms. The SDK GetSystemTime() function can substitute for time(). If you were writing a control that would be used for the first time by many users in a time sensitive application, this extra work might be worth it. Remember that the second time a user comes to a web page with an ActiveX control, the control does not need to be downloaded again.

You can find a version of the ATL Dieroll control with rand() and srand() functions included so that the C runtime isn't required by following the link on the support page at http://www.gregcons.com/uvc50.htm.

Using the Control in a Web Page

This control has a slightly different name and different CLSID than the MFC version built in Chapter 20, Building an Internet ActiveX Control.î You can use them both together in a single web page to compare them. Listing 21.21 is some HTML that puts the two controls in a table. (Use your own CLSID values when you create this page ó you many want to use Control Pad as described earlier.) Figure 21.21 shows this page in Explorer. If you want to load the page in Netscape Navigator, run dieroll.htm through the NCompassLabs converter as discussed in Chapter 20.

Listing 21.21ñdieroll.htm

</HEAD>
<BODY>
<TABLE 

CELLSPACING=15>
<TR>
<TD>
Here's the MFC die:<BR>
<OBJECT 

ID="MFCDie" 
 CLASSID="CLSID:46646B43-EA16-11CF-870C-00201801DDD6"
 WIDTH="200" HEIGHT="200">
    <PARAM NAME="ForeColor" VALUE="0"> 
    <PARAM NAME="BackColor" VALUE="16777215"> 
    <PARAM 

NAME="Image" VALUE="beans.bmp"> 
If you see this text, your browser does not support the OBJECT tag.
</OBJECT>
</TD>
<TD>
Here's the ATL die:<BR>
<OBJECT ID="ATLDie" WIDTH=200 
HEIGHT=200
 CLASSID="CLSID:2DE15F35-8A71-11D0-9B10-0080C81A397C">
    <PARAM NAME="Dots" VALUE="1">
    <PARAM 
NAME="Image" VALUE="beans.bmp">
    <PARAM NAME="Fore Color" VALUE="2147483656">
    
<PARAM NAME="Back Color" 
VALUE="2147483653">
</OBJECT>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>

Fig. 21.21 The ATL control can be used wherever the MFC control was used.

You can edit HTML files in Developer Studio as easily as source files, and with syntax coloring too! Simply choose File, New then select HTML Page from the list on the File tab. When you have typed in the HTML, right-click in the editor area and choose Preview to have InfoViewer show you the page just as Explorer would.

Fixing Flicker

When you use the two controls side-by-side like this, you may notice a tiny difference between them. When the die rolls and redraws, the MFC die does not redraw the background. The ATL die draws a white background before redrawing the bitmap. This tiny flicker is noticeable and annoying. If you load any of the sample ATL controls included with Visual C++ 5.0, you will see the same momentary flicker and you might conclude this is an unavoidable defect of ATL. It is not, and you can eliminate the flicker.

You are going to override the FireViewChange() function that was called by OnData(). Listing 21.22 shows the code from the base class.

Listing 21.22ñdieroll.htm

HRESULT 
CComControlBase::FireViewChange()
{
    if (m_bInPlaceActive)
    {
        // 
Active
        if (m_hWndCD != NULL)
            return ::InvalidateRect(m_hWndCD, NULL, TRUE); // Window based
        if 
(m_spInPlaceSite != NULL)
            return 
m_spInPlaceSite->InvalidateRect(NULL, TRUE); // Windowless
    }
    // 
Inactive
    
SendOnViewChange(DVASPECT_CONTENT);
    return S_OK;
}

FireViewChange() features calls two different InvalidateRect() functions, depending on whether your control is window based or windowless. Both InvalidateRect() functions have a parameter that determines whether the background should be erased before the control is redrawn. It is the last parameter in these calls, and is TRUE in the base class code.

Add the function FireViewChange() to CDieRoll() and copy in the code from CComControlBase::FireViewChange() (you can find it in Program Files\DevStudio\VC\atl\include\ATLCTL.CPP,) then change each TRUE to FALSE. Rebuild the project and reload the page with the two controls side by sideñthe flicker should be gone.

From Here...

This chapter introduced you to the Active Template Library, which makes use of template technology to simplify building ActiveX controls without MFC. It's a lot more work to build a control this way, and you can't ignore the underlying COM concepts as you can when using MFC, but you can build controls that are small, so that they download quickly, yet have all the functionality of MFC controls.

Some other chapters that may interest you are:


© 1997, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.