ActiveX Controls Framework: Sample Code for Authoring Non-MFC Controls

Updated: April 1996

Marc Wandschneider
Microsoft Corporation

Note   This document is an early release of the final specification. It is meant to specify and accompany software that is still in development. Some of the information in this documentation may be inaccurate or may not be an accurate representation of the functionality of the final specification or software. Microsoft assumes no responsibility for any damages that might occur either directly or indirectly from these inaccuracies. Microsoft may have trademarks, copyrights, patents or pending patent applications, or other intellectual property rights covering subject matter in this document. The furnishing of this document does not give you a license to these trademarks, copyrights, patents, or other intellectual property rights.

Contents:

1.  Introduction
2.  Creating an ActiveX Control
3.  Working with Your ActiveX Control
4.  Persistence
5.  Property Pages
6.  String Manipulation
7.  Localization
8.  Miscellaneous

1. Introduction

The ActiveX™ Controls Framework is a sample code base from which one can author new ActiveX controls for use in existing containers, such as Microsoft® Visual Basic®, Microsoft Access®, and Microsoft Visual FoxPro™, or in future containers of ActiveX controls.

This Framework differs from the Microsoft Visual C++® Control Developer's Kit (CDK) in many ways. Most notably, it's intended to be considerably more "bare bones." Only minimal functionality is provided to the developer. Little in the way of default handlers for various Windows® messages, ActiveX events, and similar items, has been provided. The ability to add them is there, but the code is not. The Framework's code base has also been designed primarily for performance and reduced code size as much as possible. Whenever a choice between ease of use and performance arose, the latter was typically chosen.

The code base is extremely extensible, however, and all the source code is there—if something doesn't do what you want it to, you can access the source code and make it happen.

1.1 Target Audience

This Framework targets slightly more advanced developers than the Microsoft Visual C++ Control Developer's Kit. Specifically, they will be required to understand some of the fundamentals of ActiveX automation and dual interfaces. Developers will have to be able to understand and modify an .ODL file on their own. In addition, they will be required to understand and be able to work with ActiveX persistence interfaces, most notably IStream and IPersistStream. However, if it is not desired, developers will not be required to have much knowledge of ActiveX embedding interfaces.

Developers who do not have specific performance requirements, those who are not familiar with many of the pertinent ActiveX™ technologies, or those who work primarily with the Microsoft Foundation Classes (MFC) will find the Microsoft Foundation Classes Control Developer's Kit far more suited to their needs.

1.2 Structure of the Framework

The directory structure of the ActiveX controls Framework is as follows:

    Controls +
         |- \AutoSample    +
         |         |- \Debug
         |         |- \Release
         |
         |- \Button    +
         |         |- \Debug
         |         |- \Release
         |
         |- \Circle    +
         |         |- \Debug
         |         |- \Release
         |
         |- \FontColor    +
         |         |- \Debug
         |         |- \Release
         |
         |- \Framewrk    +
         |         |- \Debug
         |         |- \Release
         |
         |- \Include
         |
         |- \Invisible    +
         |         |- \Debug
         |         |- \Release
         |
         |- \Localize    +
         |         |- \Debug
         |         |- \Release
         |         |- \French
         |
         |- \Template
         |
         |- \Wizards

The Controls\Framewrk and Controls\Include directories contain the core code for writing an ActiveX control. The Controls\Include directory contains the headers from which most controls will get their information, and the Controls\Framewrk directory contains the core functionality (in the COleControl class) that compiles into a library (.LIB) form.

In addition, the Controls\Wizards and Controls\Template directories contain the files necessary to generate a control. In the Controls\Wizards directory is a Microsoft Visual Basic 4.0 project, CtlWiz.Vbp. Running this project under Visual Basic 4.0 will generate a skeleton control that will compile right away.

Note   This wizard basically assumes that you're going to place the control in the Controls directory—most of the paths generated are relative and will look for the Controls\Framewrk directory in ...\Framewrk.

A few samples are provided with the Framework:

1.3 Target Environment

This Framework was developed assuming that you have the Microsoft Visual C++ 4.0 toolset in your path. The MAKECTL.INC and TOOLS.INC files in the Controls\Include directory use various tools from that toolset. In addition, the various wizards assume you have UUIDGEN.EXE, which is included in the Microsoft Win32® Software Development Kit (SDK) as well as in Visual C++ 4.0. If you are using a different environment, it should be fairly easy to change the variables to work with the Visual C++ 4.0 toolset instead. If you have only the Visual C++ 2.x tools and headers in your path, you will get various compile errors.

All of the makefiles and build processes are command-line based. Various people have reported, however, that integrating these files and processes into their favorite environments is an easy task.

Note   Under Windows 95, Visual C++ will not, by default, register its environment variables to set up for command-line builds. To enable command-line building under Windows 95, you'll either have to chdir C:\MSDEV\BIN and type "vcvars32 x86" to set up your environment variables, or include the above file in your AUTOEXEC.BAT file. Occasionally, you will get "Out of Environment Space" messages when doing this. In the Properties dialog box for the Command Prompt, you can increase the size of the environment from "Auto" to some number like 1024 to take care of the problem. None of this should happen when developing under Windows NT™ Workstation.

The DWINVERS.H file in Controls\Framewrk contains version and copyright information that should be updated each time you run your builds. The Framework does not do this for you.

2. Creating an ActiveX Control

Creating an ActiveX control using this Framework isn't a terribly difficult task. The provided wizards will do most of the work for you, but we'll briefly go into the process here. Using the sample code as a template should fill in any holes.

The Framework implements the core functionality in a few C++ classes, notably CAutomationObject, COleControl (which inherits from CAutomationObject), and CPropertyPage. All objects inherit from CUnknownObject, which provides the support for aggregation.

So, to write an ActiveX control, you need to declare a new object that inherits from COleControl. In addition, you'll need to inherit from some sort of ActiveX automation interface that describes the properties and methods for your control, for example, IMyControl. This interface description is generated by MKTYPLIB and will be put in an output file created by MKTYPLIB (the wizard sets up an environment in which this file is named MYCONTROLINTERFACES.h). COleControl has a number of virtual methods that are declared as pure, which you must implement in your control class. These include WindowProc, LoadBinaryState, LoadTextState, SaveBinaryState, SaveTextState, OnDraw, and RegisterClassData.

To write a property page, you declare a new object that inherits from CPropertyPage. This object must implement a DialogProc. You can also implement automation objects and collections by declaring a new object that inherits from CAutomationObject.

Because an ActiveX control is an in-process ActiveX server, you also need one file to describe all your objects, whether they are controls, automation objects, or property pages. This file should contain a table of all objects and details about each object. In addition, the file should have information on what sort of localization your server would like to use, and what sort of licensing support you'd like to have.

Finally, you'll need a resource file, an .ODL file to describe your interfaces and event interfaces, a .DEF file for your linking information, and a file to define all the GUIDs that have been declared.

Although it is possible to use a sample control as a template for all this information, using the wizard to create a new control remains the easiest method.

2.1 Using the Control Wizard

The Control wizard is a simple Visual Basic 4.0 program that can be used to create a new ActiveX control project. It is not the most robust program, and will usually just abort if something odd comes up, but it will save you a considerable amount of time in the beginning.

We will now walk through a sample to create a control. We will assume that the Framework lies in C:\Controls, and we will create a subclassed scrollbar control.

First, start up Visual Basic 4.0, and run CTLWIZ.VBP. The Control Wizard will ask for the name of your new control. For this example, we will use "SuperScroll." Select the option to subclass a Windows control, and use satellite localization, because foreign markets are important to us. Do not select the option to avoid long filenames.

On the next screen, from the combo box, select the SCROLLBAR window class. After that, specify C:\Controls\SuperScroll as the directory for your project. (Please note that the end project generated by this wizard will build correctly only if you choose to put it in a subdirectory of your Framework root, for example, C:\Controls.) Finally, you will be asked for the location of the Template files. Enter C:\Controls\Template.

The Control wizard will then generate your project, assuming that UUIDGEN.EXE is somewhere in your path. This is not the speediest of processes—on a 486/66 megahertz machine, it takes about 30 seconds.

2.2 Building the Control

To build your control, you first need to generate the libraries for the Framework files. The libraries are not automatically generated; this allows you to modify the libraries if desired. To do this, simply go to the Controls\Framewrk directory, and type "make dep all" in each of the Debug and Release subdirectories. You will need to recompile the Framework files only if you change a file in the Controls\Include or Controls\Framewrk directories.

Once the Framework files have been built, go back to your control's \Debug and/or \Release subdirectories, and type "make dep all" there. This will build your control.

2.3 Working with the In-Process Server

Your ActiveX control, any property pages, and automation objects are all ActiveX COM objects in an in-process server. These objects must be declared in a global table, g_ObjectInfo, which is found in the main in-process server file. Each object is declared with a wrapper, either CONTROLOBJECT, PROPERTYPAGE, or AUTOMATIONOBJECT. The name of the object is entered as an argument to the macro. In the header file where you declare the COM object, you'll need to use DEFINE_PROPERTYPAGEOBJECT, DEFINE_CONTROLOBJECT, or DEFINE_AUTOMATIONOBJECT to declare the object for use in the global table. If you are declaring objects that aren't creatable from a class factory, they still need to be declared in the global table, but the Creation function specified in the structure should be NULL.

There is some additional information that must be put in the file for the in-process server. The LIBID of the type library (.TLB) must be put in the global variable g_pLibid. You must indicate what sort of localization your control supports by using the variables g_fSatelliteLocalization and g_lcidLocale. See Section 7, "Localization", for more information.

You can put code in five of the routines in your in-process server file. The first two, InitializeLibrary() and UninitializeLibrary(), are called when the DLL is first loaded into and unloaded from memory. This is a good place to do any initialization that can't be delayed. It is worth noting that for performance reasons, delaying the initialization as long as possible is often a good idea. The CheckForLicense function lets the control decide if it is licensed to run. The global variables g_wszLicenseKey and g_wszLicenseLocation describe the license key and its location in the registry. If you don't wish to bother with licensing, you can leave all of the above untouched.

The RegisterData and UnregisterData routines are called from DllRegisterServer and DllUnregisterServer, and can be used to register and clean up additional information in the registry.

Finally, two small pieces of code are included in the in-process server file so that your project does not have to link with any of the C run-time libraries. This typically results in smaller DLL size, and can help with performance. If you want to link with the C run-time libraries or the CRTDLL libraries, you can remove these pieces of code from the in-process server file.

3. Working with Your ActiveX Control

Once you have your control up and running, you'll want to start extending its functionality. The first thing to note is that your control is, in many ways, similar to a window in Microsoft Windows. You have an HWND, you have a WindowProc, and you have to paint the client area yourself using Windows drawing APIs and handles to device contexts (DCs).

3.1 Structure of a Control

Every control in this Framework must implement a core set of methods, based on creation semantics and methods that COleControl does not provide. You may also wish to override and provide implementations for several routines. The following describes of many of these methods and routines:

In addition, the following methods/routines can be called by the ActiveX control, and often prove to be extremely useful:

3.2 Painting a Control

The OnDraw routine is called whenever the control needs to paint. Sometimes the call is from IViewObject2::Draw (as in design mode), and other times it comes from being sent a WM_PAINT message (as handled in ControlWindowProc).

Your control is given a DC, a rectangle to describe where to paint, a rectangle for describing a metafile, and an Information Context (IC, passed in as an HDC) that describes the device. If the device is a metafile, the control must perform slightly different work. However, if the device is a raster display, the control is typically painting to the screen.

Your control must be careful not to make any assumptions about the DC, except that it will be in MM_TEXT mapping mode. Often, there will be no default pens, brushes, fonts, or colors selected into the DC. Your control will have to do this work. This will typically manifest itself by having your control look slightly different in design and run modes.

3.3 Handling Messages in a Control

Your control has a method called WindowProc, which is called whenever a message is sent to your control. Your control should respond to messages in the desired fashion here. Again, try to reduce the amount of work that is done in the WM_CREATE message handler, and see if it can be put in BeforeCreateWindow().

For certain types of messages, such as keyboard messages for arrow keys and other special keys, OnSpecialKey is called instead of WindowProc. The code in OnSpecialKey should look for WM_KEYDOWN/UP, WM_CHAR, and other messages, and deal with them appropriately.

A certain class of messages typically involves notifying a window about happenings that are usually sent to a window's parent. These include WM_COMMAND, WM_NOTIFY, WM_CTLCOLOR, and others. These messages will be reflected by the host to your control in the form of OCM_COMMAND, OCM_NOTIFY, OCM_CTLCOLOR, and so on. Your controls should look for these messages instead of WM_COMMAND, and the other WM_ messages. Please see OLECTL.H for other OCM_ messages that the control might be interested in.

3.4 Adding a Property

One of the more important parts of a control is its set of properties. When you create a control with the wizard, you have no properties by default. Adding them is a relatively straightforward and simple process.

First, you need to modify the primary dispatch interface for your control in the .ODL file. Let's say you have a control called SuperScroll, and you'd like to add a LargeChange method to the control. You'd add the following to the ISuperScroll interface description in the .ODL:

[id(DISPID_LARGECHANGE), propget, helpstring("The largechange property")]
        HRESULT LargeChange([out, retval] long *plLargeChange);
    [id(DISPID_LARGECHANGE), propput]
        HRESULT LargeChange([in] long lLargeChange);

DISPID_LARGECHANGE is something that you define in DISPIDS.H. You then regenerate the type library (.TLB) file by typing:

make SuperScroll.TLB

More importantly, this regenerates SUPERSCROLLINTERFACES.H. You can then cut and paste the following two lines from SUPERSCROLLINTERFACES.H:

STDMETHOD(get_LargeChange)(THIS_ long FAR* plLargeChange) PURE;
    STDMETHOD(put_LargeChange)(THIS_ long lLargeChange) PURE;

Take these two lines and add them to your class description for CSuperScroll, and make sure that you remove the PURE declarators at the end; for example:

STDMETHOD(get_LargeChange)(long FAR* plLargeChange);
    STDMETHOD(put_LargeChange)(long lLargeChange);

You can now implement these methods in your control file to implement your property.

Please note that OLECTL.H defines a few standard DISPIDs for you. Whenever you want to declare a property, take a look in this header first to see if there is a standard DISPID for it.

3.5 Adding a Method

Adding a method is much like adding a property to your control: You define a DISPID for the method, then you add the method to the primary interface for your control. Controls generated with the Control wizard will already have an About method defined for them. Let's say we'd like to define a method called MooCow, with three parameters, the last of which is optional. Here is one such method:

[id(DISPID_MOOCOW), helpstring("Makes your Cow moo")]
        HRESULT MooCow([in] long lSeconds, [in] boolean fLowPitch,
                [in, optional] VARIANT vPitch);

Again, as with properties, you regenerate the type library, and then paste the declaration into your control header (without the PURE declarator) and implement it.

3.6 Adding an Event

In many situations, a control will want to fire an event. For example, when a control gets a WM_?BUTTONDOWN message, it often makes sense to fire a MouseDown event. This turns out to be surprisingly easy.

First, the event must be defined in an EVENTINFO structure. There are many ways to do this, including declaring a new global variable for each event type, or using an array. We will talk about the latter, since it's a little neater. Let's say you want to have KeyDown, KeyUp, and KeyPushed events. Here's how you might declare them:

typedef enum {
        MyCtlEvent_KeyDown = 0,
        MyCtlEvent_KeyUp = 1,
        MyCtlEvent_KeyPushed = 2
    } MYCTLEVENTS;

    VARTYPE rgI2 [] = { VT_I2 };

    EVENTINFO m_rgMyCtlEvents [] = {
        { DISPID_KEYDOWN, 1, rgI2 },
        { DISPID_KEYUP, 1, rgI2 },
        { DISPID_KEYPUSHED, 1, rgI2 }
    };

The EVENTINFO structure has three members: the DISPID of the event, the count of arguments in the event, and a pointer to an array of VARTYPEs that describe the types of parameters to the event. Please note that there are many DISPIDs defined for you in OLECTL.H. Before adding an event, check OLECTL.H to see whether there's already a DISPID for the event.

To fire the added events from code, just call the following:

FireEvent(&(m_rgMyCtlEvents[MyCtlEvent_KeyDown]), sKeyValue);

3.7 Using Standard ActiveX Types

For many controls, you will find it useful to declare properties of types provided by ActiveX, such as Font, Picture, and Color. Many hosts will detect properties of these types and put up convenient browsers for the user to select values for these types.

To declare a property of these types, you must first make sure the .ODL includes the following at the top:

importlib(STDTYPE_TLB);

Then, to declare a specific property of type Font, Picture, or Color, you would do something similar to the following:

[id(DISPID_FONT), propget]
        HRESULT Font([out, retval] IFontDisp **ppFont);
    [id(DISPID_FONT), propputref]
        HRESULT Font([in] IFontDisp *pFont);

    [id(DISPID_MOUSEICON), propget]
        HRESULT MouseIcon([out, retval] IPictureDisp **ppMouseIcon);
    [id(DISPID_MOUSEICON), propput]
        HRESULT MouseIcon([in] IPictureDisp *pMouseIcon);

    [id(DISPID_FORECOLOR), propget]
        HRESULT ForeColor([out, retval] OLE_COLOR *pocForeColor);
    [id(DISPID_FORECOLOR), propputref]
        HRESULT ForeColor([in] OLE_COLOR ocForeColor);

For the get_ and put_ methods for these types, the control will get a property as declared above. For fonts and pictures, the control will probably want to QueryInterface these for IFont and IPicture, respectively. See the FontColor control sample (downloadable from the Framework abstract) for an example of how this is done.

The Microsoft Development Library CD contains some very good descriptions of how to use these fonts in your application. For an example, see the topics "Standard Font Type," "Standard Picture Type," and "Standard Color Type" in the OLE Control Developer’s Kit. The following is a short summary:

Note   The Framework uses dual/vtable-bound automation interfaces, and uses ActiveX automation functionality to support IDispatch methods on the automation objects. A known problem in ActiveX automation causes unexpected failures and/or crashes when using the provided ITypeInfo::Invoke on properties that are declared to be of types that are imported from a type library. (For example, any font, picture, or color property has this problem.) The way to get around this problem is to override Invoke() and to look for the DISPIDs of the control's properties that are of this type. In this case, the control can quickly dispatch the call to the appropriate member function. The FontColor sample does just this. See its implementation of IDispatch::Invoke for sample workaround code. This problem will not exist in future versions of ActiveX automation, but until then, the workaround is necessary—and fortunately not terribly difficult.

3.8 Throwing an Exception

Every once in a while, during an operation, your control will find itself rather upset with the state of the union, and will wish to communicate this to the user via an exception. In any of the control's ActiveX automation methods or property operators, the control can call the Exception method when exiting, and the method will set up the appropriate information to trigger the error.

For example:

CMyControl::put_Eek(long lEek)
    {
        if (lEek == 10)
            return Exception(MYCTL_E_IHATETHENUMBER10, IDS_ERR_IHATETHENUMBER10, 0);
        m_lEek = lEek;
        return S_OK;
    }

The Exception routine has three arguments:

4. Persistence

One of the most important things your control will do is save out and restore its persistent state. This will typically be done in one of two ways: Through PropertyBags for text persistence, and through Streams for binary persistence. If you use Streams, performance is absolutely critical to make your control load quickly.

The ActiveX Controls Framework requires that your control implement four member functions to support persistence. The control is required to implement LoadTextState, LoadBinaryState, SaveTextState, and SaveBinaryState. If you are positive that the control will never be in a host that uses IPersistPropertyBag, you can ignore the two text interfaces, but we don't recommend this, because these interfaces are sufficiently straightforward to use easily.

4.1 Text Persistence

Text persistence in the Framework is done via IPersistPropertyBag and IPropertyBag. All ActiveX controls have an implementation of IPersistPropertyBag, and are given pointers to PropertyBag objects to do their work.

Kraig Brockschmidt's book Inside OLE (2d ed.) (Development Library, Books and Periodicals) provides a description of the IPropertyBag interface. (Editor's note: Since this book's publication date, OLE has evolved to become a key component of the ActiveX™ technologies described in this article.) Effectively, the developer will use two routines: Read and Write. In both cases, the developer will pass in a VARIANT. In the Read case, the property, if it was saved out, will be put in the VARIANT. If the property couldn't be found (it was never saved out), the default value for that property should be used, and an error should probably not be returned. For Write, a VARIANT with the data is passed in. To persist out a collection or an object (such as a Font or Picture object), the developer can pass in a VT_UNKNOWN object, and the PropertyBag will then QueryInterface that object for IPersistPropertyBag or IPersistStream and persist it. This actually proves effective for persisting collections—they can just support IPersistPropertyBag.

All of the samples (except the Localize and Circle samples) show how to persist out properties using PropertyBags. For controls with many properties, it often makes sense to use some sort of table-driven persistence to reduce code size and bug potential.

4.2 Binary Persistence

Binary persistence turns out to be critical when implementing an ActiveX control. Control loading speed can be severely hampered by a poorly written LoadBinaryState routine, so it's important to plan how to keep this routine fast. The binary persistence code is used by most hosts all the time, and by other hosts when including the control in a generated executable file.

In both routines, the control is handed a pointer to an IStream object. The key to loading speed here is to reduce the number of operations on the stream. For the SaveBinaryState routine, this is slightly less critical.

Typically, a control will want to save the following information (often in the order given):

You'll most likely want to start the control's binary persistent state with some sort of header structure that includes a "magic number" that the control can check for sanity when the control is loading. You'll also want to include a version number so that future versions of your control can deal with older versions; and finally, you will often want the control to write out the number of bytes of data that were written.

Samples in the Framework that have a binary persistent state use the following structure:

#define STREAMHDR_MAGIC    0x12345678

    typedef struct {
        DWORD dwMagic;
        DWORD dwVersion;
        DWORD cbSize;
    } STREAMHDR;

The SaveBinaryState routine saves this information, and the LoadBinaryState routine looks for it.

One way to write out all the fixed-size information (and therefore load it efficiently) is to do it all in one chunk—if all of the control's fixed persistent state is in a structure in the control object, you can write out the structure in the persistence code. Controls generated by the Control wizard will have a structure called MYCTLNAMECTLSTATE, into which you can opt to put the control's fixed-state data. Then, when saving, the control can do the following:

hr = pStream->Write(&m_state, sizeof(m_state), NULL);

For loading, the procedure becomes as simple as:

    hr = pStream->Read(&(m_state), sizeof(m_state), NULL);

Extremely efficient and easy.

For fonts and pictures, binary persistence is slightly more complicated. Effectively, the control must QueryInterface those objects for IPersistStream, then call the Load or Save routine with the stream that the control has been given. Typically, this can be done after the control has written out all other information. The FontColor control sample is a perfect example.

If you follow the above suggestions for persistent-state structure, the control's load routine will look something like this:

IPersistStream *pps;
    STREAMHDR sh;
    HRESULT   hr;

    // First read in the streamhdr, and make sure we like what we're getting.

    hr = pStream->Read(&sh, sizeof(sh), NULL);
    RETURN_ON_FAILURE(hr);

    // Sanity check

    if (sh.dwMagic != STREAMHDR_MAGIC || sh.cbSize != sizeof(m_state))
        return E_UNEXPECTED;

    // Read in the control state information.

    hr = pStream->Read(&(m_state), sizeof(m_state), NULL);
    RETURN_ON_FAILURE(hr);

    // Now read in the font!

    OleCreateFontIndirect(&_fdDefault, IID_IFont, (void **)&m_pFont);
    RETURN_ON_NULLALLOC(m_pFont);

    // QueryInterface it for IPersistStream and load it.

    hr = m_pFont->QueryInterface(IID_IPersistStream, (void **)&pps);
    RETURN_ON_FAILURE(hr);

    hr = pps->Load(pStream);
    pps->Release();

    return hr;

This method proves to be acceptably fast and robust.

5. Property Pages

Most ActiveX controls will find property pages an invaluable addition to their design-time functionality. Fortunately, implementing property pages is relatively straightforward. To do so, you merely need to declare an object that inherits from CPropertyPage.

5.1 Working with a Property Page

The control's property page is declared in the header file using the DEFINE_PROPERTYPAGE macro, which puts it into the g_ObjectInfo table. The Framework supports the creation of the property page object, but you are required to implement the static Create() function (which the Control wizard generates for you).

The property page is created much like a standard Microsoft Windows dialog box is—you use your favorite resource editor to create a DIALOG resource, then just cut and paste it into your control's resource (.RC) file.

The most important method you'll have to implement will be the DialogProc method, which is where all the work will take place. In addition to the regular Windows messages that one would expect in a DialogProc, there are a few additional ones that developers working with this Framework will expect:

Please see one of the sample controls (downloadable from the Framework abstract) for exact details on these messages.

5.2 Navigating Through Associated Objects

Your property pages will operate on one or more controls. When initializing, you will typically get some values from the first control you are given. You can use the FirstControl() method to get the object pointer for this control. You can then QueryInterface it for your primary dispatch interface to get properties with which to populate the page.

When told to apply the values (PPM_APPLY), you'll want to apply them to all objects, which means you'll want to loop using FirstControl() and NextControl() as follows:

for (pUnk = FirstControl(&dwCookie) ; pUnk; pUnk = NextControl(&dwCookie)) {
        hr = pUnk->QueryInterface(IID_IButton, (void **)&pButton);
        if (FAILED(hr)) continue;

        GetDlgItemText(hwnd, IDC_CAPTION, szTmp, 128);
        bstr = BSTRFROMANSI(szTmp);
        ASSERT(bstr, "Maggots!");
        pButton->put_Caption(bstr);
        SysFreeString(bstr);
        pButton->Release();
    }

Please note that the return values of FirstControl() and NextControl() don't need to be Release()ed.

5.3 Marking Your Page as Dirty

It is important to correctly mark the control's property page as dirty at the appropriate times. This must be done manually. Typically, the control will do this in response to a Windows notification message, such as EN_CHANGE or BN_CLICKED. When the control wishes to mark its page as dirty, the MakeDirty() routine should be called. This will cause the Apply button to be enabled, if it was previously disabled, and will tell the host that the state should be saved before destroying the page.

The following code causes the page to mark itself as dirty when the user changes text in a text box on the property page:

case WM_COMMAND:
        switch (LOWORD(wParam)) {
            case IDC_CAPTION:
                if (HIWORD(wParam) == EN_CHANGE)
                    MakeDirty();
                break;
        }
        break;

6. String Manipulation

The ActiveX Controls Framework provides a robust system of macros for manipulating strings in virtually all of the ways you'll encounter while working with an ActiveX control.

6.1 Types of Strings

32-bit systems include many different types of strings. Understanding these types is important when working with ActiveX, because there is great potential for memory leaks and bugs associated with strings.

There are two fundamental types of strings—multibyte strings (which can be ANSI or double-byte) and Unicode® strings. For multibyte strings, you almost always work with some sort of char * pointer (LPSTR, LPCSTR). As for Unicode strings, a few types are commonly used, most notably WCHAR * (LPWSTR, LPWCSTR), BSTR, and OLESTR strings.

LPWSTR pointers are just that—a pointer to a wide string. An LPOLESTR pointer is much the same, with some ActiveX rules added to it. An OLESTR is merely a wide string, but when it's an out-parameter to a function, it should be allocated using the host's IMalloc allocator (that is, CoTaskMemAlloc).

A BSTR is a string with a little more structure—specifically, a length prefix. To work with BSTRs, you need to use special APIs designed exclusively for them, notably SysAllocString, SysFreeString, and SysStringLen. (There are a few others; see the OLE 2 Programmer’s Reference, Volume II, Microsoft Press, 1994 for more details.

These data types are fully interchangeable as far as comparisons and copies go, but they are not interchangeable in terms of allocation and freeing—for instance, it is not acceptable to call SysFreeString on an OLESTR or LPWSTR string.

Both BSTRs and OLESTRs as in-parameters to functions should not be freed (per standard ActiveX COM conventions). By the same token, BSTRs and OLESTRs as out-parameters should be expected to be freed, and thus should be allocated appropriately.

6.2 Working with Strings

Now, the problem is your controls will be working with multibyte strings, except when you work with ActiveX. Therefore, there will be scenarios where you'll either be given a wide string and need the multibyte version of it, or you'll have a multibyte string and need a wide string for it.

To solve these problems, the ActiveX Controls Framework includes the following macros:

    MAKE_WIDEPTR_FROMANSI(newstringname, convertme)
    MAKE_ANSIPTR_FROMWIDE(newstringname, convertme)

    BSTRFROMANSI(ansistr)
    OLESTRFROMANSI(ansistr)
    BSTRFROMRESID(resourceid)
    OLESTRFROMRESID(resourceid)
    COPYOLESTR(copyme)
    COPYBSTR(copyme)

The first two macros will take a string of a given type and a name, and create a variable of the new name (do not declare a variable of this name yourself), and then convert the other string into the new variable. This cannot be used as an rvalue in C/C++ expressions, nor can it be an lvalue—it basically needs to sit on a line by itself.

The last set of macros do almost all the remaining interesting work. You can get BSTRs or IMalloced OLESTRs from an ANSI string, or copy OLESTRs and BSTRs. The only additional functions of real interest are those that take a WORD that is a resource ID, load in a string from your localization DLL (or the main DLL, if you don't do satellite localization), and make either a BSTR or an OLESTR out of it. This proves useful in a few places where you need a localized string.

Remember that although these macros were designed with a certain amount of speed in mind, converting strings is not a cheap operation, and therefore control writers should try to be moderately conservative about the number of string conversions they perform.

7. Localization

The ActiveX Controls Framework also provides support for robust localization of your control; most notably, property pages and other resources you will find interesting to localize.

The scheme is as follows: The resources for the default language (typically English) are in the main in-process server's resources. For each additional language supported, a satellite DLL contains the resources for that language. The name for this satellite DLL is generated by taking the "base" name for the DLL from a string resource in the main in-process server file, and then adding on a three-letter language code generated by the GetLocaleInfo function with the LCTYPE LOCALE_SABBREVLANGNAME.

The code will first look for the specific language that is set up in the global variable g_lcidLocale. This should be initialized by all servers. For ActiveX controls and property pages, you'll want to get this variable from the host. For ActiveX automation servers, you'll want to set this up some other way, such as using the system language. If the satellite DLL for the specific language is not found, the Framework will look for the primary language ID with SUBLANG_DEFAULT instead. If that satellite is not found, a handle to the default resources will be returned.

Please note that fully localized type libraries are not supported, because most hosts will ignore these and use the default library.

7.1 Setting Up for Localization

If you want to support satellite localization in your in-process server, you need to set up a few things:

Please see the Localize sample included with the Framework (downloadable from the Framework abstract) for an example of how this works, along with an actual localized satellite DLL (in the French subdirectory). The satellite DLL should contain all localized resources that interest you.

8. Miscellaneous

8.1 Recommended Reading

8.2 Host-Specific Notes

Notes, things to consider, or known problems with specific hosts for ActiveX controls: