ActiveX Control Container Programming

MFC and ClassWizard support ActiveX controls both in dialogs and as "child windows." To use ActiveX controls, you must understand how a control grants access to properties, and you must understand the interactions between your DDX code and those property values.

Property Access

The ActiveX control developer designates certain properties for access at design time. Those properties are specified in the property pages that the control displays in the dialog editor when you right-click on a control and choose Properties. The Calendar control's main property page looks like the one shown next.

Click to view at full size.

When you click on the All tab, you will see a list of all the design- time-accessible properties, which might include a few properties not found on the Control tab. The Calendar control's All page looks like this.

Click to view at full size.

All the control's properties, including the design-time properties, are accessible at runtime. Some properties, however, might be designated as read-only.

ClassWizard's C++ Wrapper Classes for ActiveX Controls

When you insert an ActiveX control into a project, ClassWizard generates a C++ wrapper class, derived from CWnd, that is tailored to your control's methods and properties. The class has member functions for all properties and methods, and it has constructors that you can use to dynamically create an instance of the control. (ClassWizard also generates wrapper classes for objects used by the control.) Following are a few typical member functions from the file Calendar.cpp that ClassWizard generates for the Calendar control:

unsigned long CCalendar::GetBackColor()
{
    unsigned long result;
    InvokeHelper(DISPID_BACKCOLOR, DISPATCH_PROPERTYGET,
                 VT_I4, (void*)&result, NULL);
    return result;
}

void CCalendar::SetBackColor(unsigned long newValue)
{
    static BYTE parms[] =
        VTS_I4;
    InvokeHelper(DISPID_BACKCOLOR, DISPATCH_PROPERTYPUT,
                 VT_EMPTY, NULL, parms, newValue);
}

short CCalendar::GetDay()
{
    short result;
    InvokeHelper(0x11, DISPATCH_PROPERTYGET, VT_I2,
                 (void*)&result, NULL);
    return result;
}

void CCalendar::SetDay(short nNewValue)
{
    static BYTE parms[] =
        VTS_I2;
    InvokeHelper(0x11, DISPATCH_PROPERTYPUT, VT_EMPTY,
                 NULL, parms, nNewValue);
}

COleFont CCalendar::GetDayFont()
{
    LPDISPATCH pDispatch;
    InvokeHelper(0x1, DISPATCH_PROPERTYGET, VT_DISPATCH,
                 (void*)&pDispatch, NULL);
    return COleFont(pDispatch);
}

void CCalendar::SetDayFont(LPDISPATCH newValue)
{
    static BYTE parms[] =
        VTS_DISPATCH;
    InvokeHelper(0x1, DISPATCH_PROPERTYPUT, VT_EMPTY,
                 NULL, parms, newValue);
}

VARIANT CCalendar::GetValue()
{
    VARIANT result;
    InvokeHelper(0xc, DISPATCH_PROPERTYGET, VT_VARIANT,
                 (void*)&result, NULL);
    return result;
}

void CCalendar::SetValue(const VARIANT& newValue)
{
    static BYTE parms[] =
        VTS_VARIANT;
    InvokeHelper(0xc, DISPATCH_PROPERTYPUT, VT_EMPTY,
                 NULL, parms, &newValue);
}

void CCalendar::NextDay()
{
    InvokeHelper(0x16, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
}

void CCalendar::NextMonth()
{
    InvokeHelper(0x17, DISPATCH_METHOD, VT_EMPTY, NULL, NULL);
}

You don't have to concern yourself too much with the code inside these functions, but you can match up the first parameter of each InvokeHelper function with the dispatch ID for the corresponding property or method in the Calendar control property list. As you can see, properties always have separate Set and Get functions. To call a method, you simply call the corresponding function. For example, to call the NextDay method from a dialog class function, you write code such as this:

m_calendar.NextDay();

In this case, m_calendar is an object of class CCalendar, the wrapper class for the Calendar control.

AppWizard Support for ActiveX Controls

When the AppWizard ActiveX Controls option is checked (the default), AppWizard inserts the following line in your application class InitInstance member function:

AfxEnableControlContainer();

It also inserts the following line in the project's StdAfx.h file:

#include <afxdisp.h>

If you decide to add ActiveX controls to an existing project that doesn't include the two lines above, you can simply add the lines.

ClassWizard and the Container Dialog

Once you've used the dialog editor to generate a dialog template, you already know that you can use ClassWizard to generate a C++ class for the dialog window. If your template contains one or more ActiveX controls, you can use ClassWizard to add data members and event handler functions.

Dialog Class Data Members vs. Wrapper Class Usage

What kind of data members can you add to the dialog for an ActiveX control? If you want to set a control property before you call DoModal for the dialog, you can add a dialog data member for that property. If you want to change properties inside the dialog member functions, you must take another approach: you add a data member that is an object of the wrapper class for the ActiveX control.

Now is a good time to review the MFC DDX logic. Look back at the Cincinnati dialog in Chapter 6. The CDialog::OnInitDialog function calls CWnd::UpdateData(FALSE) to read the dialog class data members, and the CDialog::OnOK function calls UpdateData(TRUE) to write the members. Suppose you added a data member for each ActiveX control property and you needed to get the Value property value in a button handler. If you called UpdateData(FALSE) in the button handler, it would read all the property values from all the dialog's controls—clearly a waste of time. It's more effective to avoid using a data member and to call the wrapper class Get function instead. To call that function, you must first tell ClassWizard to add a wrapper class object data member.

Suppose you have a Calendar wrapper class CCalendar and you have an m_calendar data member in your dialog class. If you want to get the Value property, you do it like this:

COleVariant var = m_calendar.GetValue();

The VARIANT type and COleVariant class are described in Chapter 25.

Now consider another case: you want to set the day to the 5th of the month before the control is displayed. To do this by hand, add a dialog class data member m_sCalDay that corresponds to the control's short integer Day property. Then add the following line to the DoDataExchange function:

DDX_OCShort(pDX, ID_CALENDAR1, 0x11, m_sCalDay);

The third parameter is the Day property's integer index (its DispID), which you can find in the GetDay and SetDay functions generated by ClassWizard for the control. Here's how you construct and display the dialog:

CMyDialog dlg;
dlg.m_sCalDay = 5;
dlg.DoModal();

The DDX code takes care of setting the property value from the data member before the control is displayed. No other programming is needed. As you would expect, the DDX code sets the data member from the property value when the user clicks the OK button.

Even when ClassWizard correctly detects a control's properties, it can't always generate data members for all of them. In particular, no DDX functions exist for VARIANT properties like the Calendar's Value property. You'll have to use the wrapper class for these properties.

Mapping ActiveX Control Events

ClassWizard lets you map ActiveX control events the same way you map Windows messages and command messages from controls. If a dialog class contains one or more ActiveX controls, ClassWizard adds and maintains an event sink map that connects mapped events to their handler functions. It works something like a message map. You can see the code in Figure 8-2.

ActiveX controls have the annoying habit of firing events before your program is ready for them. If your event handler uses windows or pointers to C++ objects, it should verify the validity of those entities prior to using them.

Locking ActiveX Controls in Memory

Normally, an ActiveX control remains mapped in your process as long as its parent dialog is active. That means it must be reloaded each time the user opens a modal dialog. The reloads are usually quicker than the initial load because of disk caching, but you can lock the control into memory for better performance. To do so, add the following line in the overridden OnInitDialog function after the base class call:

AfxOleLockControl(m_calendar.GetClsid());

The ActiveX control remains mapped until your program exits or until you call the AfxOleUnlockControl function.