| Tenouk C & C++ | MFC Home | ATL and ActiveX Controls 6 | Another ATL Tutorial 1 | Download | Site Index |


 

 

 

 

 

 

 

ATL and ActiveX Controls 7

 

 

 

 

 

 

 

Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2. Topics and sub topics for this tutorial are listed below. Don’t forget to read Tenouk’s small disclaimer. The supplementary note for this tutorial is control class.

  1. Property Persistence

  2. Bidirectional Communication (Events)

  3. Using the Control

  4. Conclusion

 

Property Persistence

 

Once you have added properties to the control, it's logical that you might want to have those properties persist with their container. For example, imagine Hasbro buys your dice control to include in its new Windows version of Monopoly. The game vendor uses your dice control within one of the Monopoly dialog boxes and configures the control so that the dice are blue and they roll 23 times before stopping. If the dice control had a sound property, the Monopoly authors could configure the dice to emit a beep every time they roll. When someone plays the game and rolls the dice, that person will see a pair of blue dice that roll 23 times before stopping and they will hear the dice make a sound while they roll. Remember that these properties are all properties of the control. If you're using the control in an application, chances are good you'll want these properties to be saved with the application.

Fortunately, adding persistence support to your control is almost free when you use the ATL property macros. You've already seen how to add the property pages to the control DLL using the property map macros. As it turns out, these macros also make the properties persistent.

You can find ATL's code for handling the persistence of a control's properties within the CComControlBase class. CComControlBase has a member function named IPersistStreamInit_Save() that handles saving a control's properties to a stream provided by the client. Whenever the container calls IPersistStreamInit::Save, ATL ends up calling IPersistStreamInit_Save() to do the actual work. IPersistStreamInit_Save() works by retrieving the control's property map, the list of properties maintained by the control. Remember that the BEGIN_PROPERTY_MAP macro adds a function named GetPropertyMap() to the control. The first item written out by IPersistStreamInit_Save() is the control's extents (its size on the screen). IPersistStreamInit_Save() then cycles through the property map to write the contents of the property map out to the stream. For each property, the control calls QueryInterface() on itself to get its own dispatch interface. As IPersistStreamInit_Save() goes through the list of properties, the control calls IDispatch::Invoke on itself to get the property based on the DISPID associated with the property. The property's DISPID is included as part of the property map structure. The property comes back from IDispatch::Invoke as a Variant, and IPersistStreamInit_Save() writes the property to the stream provided by the client.

 

Bidirectional Communication (Events)

 

Now that the dice control has properties and property pages and renders itself to a device context, the last thing to do is to add some events to the control. Events provide a way for the control to call back to the client code and inform the client code of certain events as they occur.

For example, the user can roll the dice. Then when the dice stop rolling, the client application can fish the dice values out of the control. However, another way to implement the control is to set it up so that the control notifies the client application when the dice have rolled using an event. Here you'll see how to add some events to the dice control. We'll start by understanding how ActiveX Control events work.

 

 

How Events Work

 

When a control is embedded in a container (such as a Visual Basic form or an MFC-based dialog box), one of the steps the client code takes is to establish a connection to the control's event set. That is, the client implements an interface that has been described by the control and makes that interface available to the control. That way, the control can talk back to the container.

Part of developing a control involves defining an interface that the control can use to call back to the client. For example, if you're developing the control using MFC, ClassWizard will define the interface and produce some functions you can call from within the control to fire events back to the client. If you're developing the control in ATL, you can accomplish the same result by defining the event callback interface in the control's IDL and using ClassView to create a set of callback proxy functions for firing the events to the container. When the callback interface is defined by the control, the container needs to implement that interface and hand it over to the control. The client and the control do this through the IConnectionPointContainer and IConnectionPoint interfaces.

IConnectionPointContainer is the interface that a COM object implements to indicate that it supports connections. IConnectionPointContainer represents a collection of connections available to the client. Within the context of ActiveX Controls, one of these connections is usually the control's main event set. Here's the IConnectionPointContainer interface:

interface IConnectionPointContainer : IUnknown

{

    HRESULT FindConnectionPoint(REFIID riid, IConnectionPoint **ppcp) = 0;

    HRESULT EnumConnectionPoints(IEnumConnectionsPoint **ppec) = 0;

};

IConnectionPointContainer represents a collection of IConnectionPoint interfaces. Here's the IConnectionPoint interface:

interface IConnectionPoint : IUnknown

{

    HRESULT GetConnectionInterface(IID *pid) = 0;

    HRESULT GetConnectionPointContainer(IConnectionPointContainer **ppcpc) = 0;

    HRESULT Advise(IUnknown *pUnk, DWORD *pdwCookie) = 0;

    HRESULT Unadvise(dwCookie) = 0;

    HRESULT EnumConnections(IEnumConnections **ppec) = 0;

}

The container creates the control by calling CoCreateInstance() on the control. As the control and the container are establishing the interface connections between themselves, one of the interfaces the container asks for is IConnectionPointContainer (that is, the container calls QueryInterface() asking for IID_IConnectionPointContainer). If the control supports connection points (the control answers "Yes" when queried for IConnectionPointContainer), the control uses IConnectionPointContainer::FindConnectionPoint to get the IConnectionPoint interface representing the main event set. The container knows the GUID representing the main event set by looking at the control's type information as the control is inserted into the container.

If the container can establish a connection point to the control's main event set (that is, IConnectionPointContainer::FindConnectionPoint returns an IConnectionPoint interface pointer), the container uses IConnectionPoint::Advise to subscribe to the callbacks. Of course, to do this the container needs to implement the callback interface defined by the control (which the container can learn about by using the control's type library). Once the connection is established, the control can call back to the container whenever the control fires off an event. Here's what it takes to make events work within an ATL-based ActiveX control.

 

Adding Events to the Dice Control

 

There are several steps to adding event sets to your control. Some of them are hidden by clever wizardry. First, use IDL to describe the events. Second, add a proxy that encapsulates the connection points and event functions. Finally, fill out the control's connection map so that the client and the object have a way to connect to each other. Let's examine each step in detail.

When using ATL to write an ActiveX control, IDL is the place to start adding events to your control. The event callback interface is described within the IDL so the client knows how to implement the callback interface correctly. The IDL is compiled into a type library that the client will use to figure out how to implement the callback interface. For example, if you wanted to add events indicating the dice were rolled, doubles were rolled, and snake eyes were rolled, you'd describe the callback interface like this in the control's IDL file:

library ATLDICESRVRLib

{

    importlib("stdole32.tlb");

    importlib("stdole2.tlb");

 

    [

        uuid(21C85C43-0BFF-11d1-8CAA-FD10872CC837),

        helpstring("Events created from rolling dice")

    ]

    dispinterface _IMyatldiceobjEvents

    {

        properties:

        methods:

            [id(1)] void DiceRolled([in]short x, [in] short y);

            [id(2)] void Doubles([in] short x);

            [id(3)] void SnakeEyes();

    }

 

    [

        uuid(6AED4EBD-0991-11D1-8CAA-FD10872CC837), helpstring("Myatldiceob Class")

    ]

    coclass Myatldiceob

    {

        [default] interface IATLDieceObj;

        [default, source] dispinterface _IMyatldiceobjEvents;

    };

The control's callback interface is defined as a dispatch interface (note the dispinterface keyword) because that's the most generic kind of interface available. When it comes to callback interfaces, most environments understand only IDispatch. The code on the previous page describes a callback interface to be implemented by the client (if the client decides it wants to receive these callbacks). We added this dice events interface by hand. The Object Wizard will put one in for you. It might have a different name than the one we have listed. For example, the Wizard is likely to put in an interface named IATLObjEvents.

 

Implementing the Connection Point

 

After you've described the callback interface within the IDL and compiled the control, the control's type information will contain the callback interface description so that the client will know how to implement the callback interface. However, you don't yet have a convenient way to fire these events from the control. You could, of course, call back to the client by setting up calls to IDispatch::Invoke by hand. However, a better way to do this is to set up a proxy (a set of functions wrapping calls to IDispatch) to handle the hard work for you. To generate a set of functions that you can call to fire events in the container, use the Implement Connection Point menu option from ClassView.

In ClassView, click the right mouse button while the cursor is hovering over the Cmyatldiceob symbol. This brings up the context menu for the Cmyatldiceob item. Choose Implement Connection Point from the menu to bring up the Implement Connection Point dialog box. This dialog box asks you to locate the type information describing the interface you expect to use when calling back to the container (the _IMyatldiceobjEvents interface, in this case). By default, this dialog box looks at your control's type library. The dialog box reads the type library and shows the interfaces found within it. Choose _IMyatldiceobjEvents and click OK. Doing so creates a C++ class that wraps the dice events interface. Given the above interface definition, here's the code generated by the Implement Connection Point dialog box:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

template <class T>

class CProxy_IATLDieceObjEvents :

public IConnectionPointImpl<T, &DIID__IATLDieceObjEvents,  CComDynamicUnkArray>

{

    //Warning this class may be recreated by the wizard.

public:

};

 

{

    //Warning this class may be recreated by the wizard.

public:

    VOID Fire_Doubles(SHORT x)

    {

        T* pT = static_cast<T*>(this);

        int nConnectionIndex;

        CComVariant* pvars = new CComVariant[1];

        int nConnections = m_vec.GetSize();

 

        for (nConnectionIndex = 0;

             nConnectionIndex < nConnections;

             nConnectionIndex++)

        {

            pT->Lock();

            CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);

            pT->Unlock();

            IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);

            if (pDispatch != NULL)

            {

                pvars[0].vt = VT_I2;

                pvars[0].iVal= x;

                DISPPARAMS disp = { pvars, NULL, 1, 0 };

                pDispatch->Invoke(0x1, IID_NULL,

                                  LOCALE_USER_DEFAULT,

                                  DISPATCH_METHOD, &disp,

                                  NULL, NULL, NULL);

            }

        }

        delete[ ] pvars;

    }

    VOID Fire_DiceRolled(SHORT x, SHORT y)

    {

        T* pT = static_cast<T*>(this);

        int nConnectionIndex;

        CComVariant* pvars = new CComVariant[2];

        int nConnections = m_vec.GetSize();

 

        for (nConnectionIndex = 0;

             nConnectionIndex < nConnections;

             nConnectionIndex++)

        {

            pT->Lock();

            CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);

            pT->Unlock();

            IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);

            if (pDispatch != NULL)

            {

                pvars[1].vt = VT_I2;

                pvars[1].iVal= x;

                pvars[0].vt = VT_I2;

                pvars[0].iVal= y;

                DISPPARAMS disp = { pvars, NULL, 2, 0 };

                pDispatch->Invoke(0x2, IID_NULL,

                                  LOCALE_USER_DEFAULT,

                                  DISPATCH_METHOD, &disp,

                                  NULL, NULL, NULL);

            }

        }

        delete[ ] pvars;

 

    }

 

    VOID Fire_SnakeEyes()

    {

        T* pT = static_cast<T*>(this);

        int nConnectionIndex;

        int nConnections = m_vec.GetSize();

 

        for (nConnectionIndex = 0;

             nConnectionIndex < nConnections;

             nConnectionIndex++)

        {

            pT->Lock();

            CComPtr<IUnknown> sp = m_vec.GetAt(nConnectionIndex);

            pT->Unlock();

            IDispatch* pDispatch = reinterpret_cast<IDispatch*>(sp.p);

            if (pDispatch != NULL)

            {

                DISPPARAMS disp = { NULL, NULL, 0, 0 };

                pDispatch->Invoke(0x3, IID_NULL,

                                  LOCALE_USER_DEFAULT,

                                  DISPATCH_METHOD, &disp,

                                  NULL, NULL, NULL);

            }

        }

    }

};

 

The C++ class generated by the connection point generator serves a dual purpose. First, it acts as the specific connection point. Notice that it derives from IConnectionPointImpl. Second, the class serves as a proxy to the interface implemented by the container. For example, if you want to call over to the client and tell the client that doubles were rolled, you'd simply call the proxy's Fire_Doubles() function. Notice how the proxy wraps the IDispatch call so that you don't have to get your hands messy dealing with variants by yourself.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Establishing the Connection and Firing the Events

 

The final step in setting up the event set is to add the connection point to the dice control and turn on the IConnectionPointContainer interface. The connection point dialog box added the CProxy_IMyatldiceobjEvents class to the dice control's inheritance list, which provides the IConnectionPoint implementation inside the control. An ATL class named IConnectionPointContainerImpl provides the implementation of IConnectionPointContainer. These two interfaces should be in the dice control's inheritance list like this:

class Cmyatldiceob :

    public CComObjectRootEx<CComSingleThreadModel>,

    public CStockPropImpl<Cmyatldiceob, IATLDieceObj, &IID_IATLDieceObj, &LIBID_ATLDICESRVRLib>,

    public CComControl<Cmyatldiceob>,

    public IPersistStreamInitImpl<Cmyatldiceob>,

    public IOleControlImpl<Cmyatldiceob>,

    public IOleObjectImpl<Cmyatldiceob>,

    public IOleInPlaceActiveObjectImpl<Cmyatldiceob>,

    public IViewObjectExImpl<Cmyatldiceob>,

    public IOleInPlaceObjectWindowlessImpl<Cmyatldiceob>,

    public IConnectionPointContainerImpl<Cmyatldiceob>,

    public IPersistStorageImpl<Cmyatldiceob>,

    public ISpecifyPropertyPagesImpl<Cmyatldiceob>,

    public IQuickActivateImpl<Cmyatldiceob>,

    public IDataObjectImpl<Cmyatldiceob>,

    public IProvideClassInfo2Impl<&CLSID_Myatldiceob, &DIID__IMyatldiceobjEvents, &LIBID_ATLDICESRVRLib>,

    public IPropertyNotifySinkCP<Cmyatldiceob>,

    public CComCoClass<Cmyatldiceob, &CLSID_Myatldiceob>,

    public CProxy_DDiceEvents< Cmyatldiceob >

{

  ...

  ...

  ...

};

 

Having these classes in the inheritance list inserts the machinery in your control that makes connection points work. Whenever you want to fire an event to the container, all you need to do is call one of the functions in the proxy. For example, a good time to fire these events is from within the control's OnTimer() method, firing a DiceRolled() event whenever the timer stops, firing a SnakeEyes() event whenever both die faces have the value 1, and firing a Doubles() event when both die faces are equal:

Cmyatldiceob::OnTimer(UINT msg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)

{

    if(m_nTimesRolled > m_nTimesToRoll)

    {

        m_nTimesRolled = 0;

        KillTimer(1);

        Fire_DiceRolled(m_nFirstDieValue, m_nSecondDieValue);

        if(m_nFirstDieValue == m_nSecondDieValue)

            Fire_Doubles(m_nFirstDieValue);

        if(m_nFirstDieValue == 1 && m_nSecondDieValue == 1)

            Fire_SnakeEyes();

    } else {

        m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;

        m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;

        FireViewChange();

        m_nTimesRolled++;

    }

        bHandled = TRUE;

        return 0;

}

Finally, notice the connection map contains entries for the control's connection points:

BEGIN_CONNECTION_POINT_MAP(Cmyatldiceob)

    CONNECTION_POINT_ENTRY(DIID__IMyatldiceobjEvents)

    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)

END_CONNECTION_POINT_MAP()

The control uses this map to hand back connection points as the client requests them.

 

 

Using the Control

 

So, how do you use the control once you've written it? The beauty of COM is that as long as the client and the object agree on their shared interfaces, they don't need to know anything else about each other. All the interfaces implemented within the dice control are well understood by a number of programming environments. You've already seen how to use ActiveX Controls within an MFC-based dialog box. The control you just wrote will work fine within an MFC-based dialog box, just use the Add To Project menu option under the Project menu. Select Registered ActiveX Controls and insert the Myatldiceob component into your project. Visual C++ will read the dice control's type information and insert all the necessary COM glue to make the dialog box and the control talk together. This includes all the OLE embedding interfaces as well as the connection and event interfaces. In addition, you could just as easily use this control from within a Visual Basic form. When working on a Visual Basic project, select References from the Project menu and insert the dice control into the Visual Basic project.

 

Conclusion

 

ActiveX Controls are one of the most widely used applications of COM in the real world today. To summarize, ActiveX controls are just COM objects that happen to implement a number of standard interfaces that environments like Visual C++ and Visual Basic understand how to use. These interfaces deal with rendering, persistence, and events, allowing you to drop these components into the aforementioned programming environments and use them right away.

In the past, MFC was the only practical way to implement ActiveX Controls. However, these days ATL provides a reasonable way of implementing ActiveX Controls, provided you're willing to follow ATL's rules. For example, if you buy into the ATL architecture for writing controls, you'll have to dip down into Windows and start working with window handles and device context handles in their raw forms. However, the tradeoff is often worthwhile, because ATL provides more flexibility when developing ActiveX controls. For example, dual interfaces are free when using ATL, whereas they're a real pain to implement in MFC.

 

 

----------------------End---------------------

 

 

 

 

 

 

 

 

 

 

 

 

Further reading and digging:

  1. DCOM at MSDN.

  2. COM+ at MSDN.

  3. COM at MSDN.

  4. Win32 process, thread and synchronization story can be found starting from Module R.

  5. MSDN MFC 7.0 class library online documentation.

  6. MSDN MFC 9.0 class library online documentation - latest version.

  7. MSDN Library

  8. Windows data type.

  9. Win32 programming Tutorial.

  10. The best of C/C++, MFC, Windows and other related books.

  11. Unicode and Multibyte character set: Story and program examples.

 

 


 

| Tenouk C & C++ | MFC Home | ATL and ActiveX Controls 6 | Another ATL Tutorial 1 | Download | Site Index |