The ABCs of MFC ActiveX Controls

 

Paul Johns
Developer Trainer

October 22, 1996

Click here to download the sample files associated with this article from the Downloads Center.

Introduction

ActiveX™ controls (formerly known as OLE controls) are hot stuff, with more than 1,000 controls currently available. They can run in a wide variety of containers—Visual Basic®, Visual C++®, Microsoft® Access, and, as we all know, Microsoft Internet Explorer 3.0. (They also can be used by Delphi and Netscape Navigator—and perhaps other containers.)

ActiveX controls are also cool because they are what I call "scalable." Their functionality canbe very simple, such as a timer control, or very complex, such as a data-bound grid, spreadsheet, or word processor control. Most controls fall somewhere in between.

This scalability is important because you can use ActiveX controls as reusable software components in bigger applications. For instance, this article shows you how to write a stoplight control. I can hear you thinking now, "Well, that's interesting, I suppose, and cute—but how useful?"

Okay, what would happen if we wrote a few other controls—say a road control, a vehicle control, and a master controller control? Remember, ActiveX controls feature two-way communication with their containers (in this case a Web page), so the controls can interact with each other via scripting on the page! With those components, how easy would it be to put together traffic simulations of any intersection or set of intersections you wanted?

One of the cooler Java™ applets out there is a traffic simulation written by Kelly Liu. It's at http://seagal.lanl.gov:8090/~liuke/traffic/traffic.html. It's multithreaded, active, and it runs great, especially with the Internet Explorer 3.0 way fast just-in-time compiler.

But as cool as the Java applet is, it's limited because it's monolithic. One big happy applet. Even the buttons are part of the applet. And the only way to do something that the programmer didn't plan for is—you guessed it—to modify the source code and recompile.

Now, back to ActiveX. The stoplight control we're going to write here is only a very small piece of such a simulation. But it is a start—and the stoplight component we write here could be used as-is in a larger simulation. By the same token, all ActiveX controls can be used (and reused) as components in larger applications.

I hope you'll agree that ActiveX controls are cool. And now I'll bet you're asking, "How can I get a piece of this ActiveX action?"

Ways to Write an ActiveX Control

Right now, there are four ways to write an ActiveX control.

  • Microsoft Foundation Classes (MFC)
  • ActiveX Template Library
  • BaseCtrl framework
  • Visual J++™ (COM objects only)

(More ways to write ActiveX controls will be available soon. A later version of Microsoft Visual J++ will support the writing of complex ActiveX controls, as will the next version of Visual Basic. So you'll have tools and frameworks for every taste and need—except perhaps COBOL.)

Using the ActiveX Template Library requires intimate knowledge of how OLE controls communicate with their containers—and implementation of over a dozen OLE interfaces for controls with user interfaces. The BaseCtrl framework is much simpler, but still requires more knowledge of OLE than does MFC. Visual J++ makes it very easy to write COM objects, but it's best used for pretty simple objects. We'll be doing articles about those other methods for writing controls in the future.

Using MFC for ActiveX Controls

For most folks, we recommend using MFC because MFC controls are easy to write. You focus on your control's behavior, not the intricacies of OLE interfaces. And, with the new features of MFC 4.2, you can write controls that perform better and implement the cool new OCX 96 features. (You may want to stick with version 4.1 though; since the MFC 4.1 DLLs ship with Internet Explorer, all Internet Explorer 3.0 users will already have these DLLs properly installed on their system.)

But every silver lining has its cloud—and there are two big MFC clouds.

The first is that MFC controls, while not huge, are not tiny. If you're developing controls that don't need the full functionality of an OLE control, you might want to check out other options—perhaps after prototyping in MFC.

The second cloud is bigger and darker, but might not be in your sky. To run an ActiveX control written with MFC, the correct version of the MFC and C Runtime DLLs must be installed on the user's system. These DLLs are over a megabyte total—quite large to be downloading at 14.4Kbps! The good news is that Internet Explorer 3.0 ships with version 4.1 of the MFC DLLs, so most users will have them installed already. Even if you use the 4.1 DLLs, be prepared to provide and install the DLLs for users running your control in clients other than Microsoft Internet Explorer. Also, you'll have to provide the MFC 4.2 DLLs to all users if you need MFC 4.2 features in your control.

What's this Article About?

In this article, we're going to show you how to write a simple yet complete ActiveX control using MFC. We're using Visual C++ version 4.1, but the code should work with any compiler that supports MFC version 4.0 or later. Your Wizard support may vary, however. We show all the code that the Wizards generate and modify so you can follow along regardless of your tools.

We aren't going to go into advanced topics—no discussions of how to asynchronously download video clips over the Internet, nor transparent controls—and only minimal discussion about over-the-Internet installation, code signing, and marking your control as safe for initializing and scripting. For now, just the basics.

As we mentioned before, this control is a little on-screen stoplight. To keep the filename under eight characters without spaces (do this—it makes your life simpler!), I called the control StopLite.

When the control is embedded in a Web page, it looks like this:

I've used JavaScript™ to hook up the buttons and label control to the StopLite control; I could as easily have used Visual Basic® Scripting Edition (VBScript).

If you're using Internet Explorer 3.0 or another ActiveX-activated browser, you can click the buttons to change the light. The Next Light button invokes the Next method, which changes the light to the next color (in the sequence red, green, yellow, red . . .). The color buttons change the light to the specified color, while the Off and Test buttons turn the lights all off and all on, respectively.

The StopLite control also fires events as it changes. These events change the label just underneath the button from "Welcome to PaulJo's StopLite page!" to other appropriate messages.

One great feature of ActiveX controls is that they can be used in containers other than Web pages. Here's a Visual Basic app that uses the StopLite control:

In both the Visual Basic and the HTML uses of this control, the buttons, control, and label are hooked together using Visual Basic code.

How Is this Article Organized?

We're going to show you how to write this control. But first, I want to give a you a little bit of background about the architecture of MFC controls. Then we'll look at the code that AppWizard generates. Finally, we'll modify that skeleton to create the complete StopLite control.

What Does an MFC Control Look Like?

MFC Classes for a Control

MFC controls are considerably simpler than MFC applications. The simplest MFC control has only three classes: a control module class derived from COleControlModule (which in turn is derived from CWinApp), a control window class derived from COleControl (which in turn is derived from CWnd), and a property page class derived from COlePropertyPage (which in turn is derived from CDialog). Assuming that the name of the control is StopLite, the name of the control module class is CStopLiteApp, the name of the control class is CStopLiteCtrl, and the name of the property page class is CStopLitePropPage.

CStopLiteApp is very similar to the CWinApp-derived class that's at the core of your MFC applications. Only one object of this class will be in your control module, no matter how many controls might be in this module.

CStopLiteCtrl is the class in which you'll do 99.44 percent of your work. It has functions that implement all of the properties and methods, and fire all the events. It also is responsible for drawing the control and responding to Windows messages.

If your control module (.OCX) contains more than one control, you'll have an additional class for each control. Each class will be named CXxxxCtrl, where Xxxx is the name of the control encapsulated by that class.

CStopLitePropPage is very similar to a dialog box class. It's used to implement the code needed to connect the dialog box template you'll write for your property page to the properties in your control.

Creating the Control with AppWizard

Which Version of MFC Should I Use?

Because ActiveX controls written using MFC require the appropriate MFC DLLs to be present and registered on the user's system, you have to ensure sure that every user has the correct DLLs properly installed.

In general, you need to provide a setup script that sets up your control and all the DLLs it needs. This setup script should not overwrite DLLs that are of a later version than the ones it installs. It also might need to register some or all of the DLLs it installs. We'll have articles about installation, code signing issues, and marking controls as safe for scripting and initializing soon. For now, check your development environment's documentation for installation requirements and the MSDN Library for information on installations, code signing, and marking controls as safe.

To reduce the amount of code that needs to be downloaded before your control can be used, you may want develop your control so that it takes advantage of DLLs already present on your users' systems. For instance, Internet Explorer 3.0 installation always installs the version 4.1 MFC DLLs, as does Windows NT 4.0.

If you don't need any of the new features in later versions of MFC, stick with MFC (and Visual C++) version 4.1 so that most of your users won't have to download those big DLLs. But not all users will have the DLLs, so you'll also have to come up with an optional scheme for users to get them.

Because the StopLite control doesn't use any version 4.2 features, I stuck with Visual C++ version 4.1 to make life simpler. I had to reinstall Visual C++ version 4.1, but that was better than requiring everyone to download the version 4.2 DLLs. (There are directions included with Visual C++ 4.2 that tell how to install both version 4.1 and version 4.2 on the same system.)

Creating the Skeleton

The first step in creating any MFC project, including a control project, is to use Visual C++'s AppWizard to create the skeleton code. I've used Visual C++ 4.1 for this control, but the directions should work with any version 4.0 or later. I've tested these directions with versions 4.1 and 4.2.

To get started, select "New" on the File menu, then select "Project Workspace." This will allow you to select a type of project. Select "OLE ControlWizard" to create an ActiveX control. Fill in the name of the project and the directory in which you want the control placed. (In case you're following along, I've called this project "StopLite"—note the capitalization and spelling.) Keep the filename eight characters or less. Although Visual C++ generally deals with long filenames correctly, some other tools have trouble with them, especially if the names contain spaces.

Clicking "Create" will start the OLE ControlWizard. While it looks as though you have two pages of options, there are really three—you access the third page by clicking the "Advanced" button on the second page. We're going to use all the default options, so don't change anything. Just take a look at the options and click "Finish" when you're done. (We'll get into some of these other options, and more, in later articles.)

After you click "OK" on the summary page, the OLE ControlWizard will generate your new project. For more information on the OLE ControlWizard, look up "OLE ControlWizard" in Visual C++'s Books Online.

This project is ready to go. You can build it now by selecting "Build StopLite.OCX" from the Build menu or by pressing the F7 key. If you need more assistance with AppWizard, consult Visual C++'s Books Online.

Running the Skeleton

Once you've built the control, it's easy to test it in the OLE Control Test Container that comes with Visual C++. Select "OLE Control Test Container" from the Tools menu, pick "Insert OLE Control" from the Edit menu (or click the Insert button), and select your control ("StopLite") in the list box. Because we haven't modified the drawing code, we get the ControlWizard-provided code, which draws an ellipse that fills the control area. You can see that the control automatically scales the ellipse by resizing the control.

The control container also allows us to view and change the control's properties (View.Properties—we don't yet have any, but we will . . .), call its methods (Edit.Invoke Methods—ControlWizard wrote an AboutBox method for us. Try it!), and view a log of events fired (View.Event Log—we don't fire any events yet). You can also invoke the control's (blank) property page (Edit.Properties).

Finally, you can view and change the container's ambient properties (Edit.Set Ambient Properties). However, we don't yet have the code in place to access those ambient properties, so changing them will have no effect on the control.

There are toolbar buttons for all of these functions.

For more advanced testing, the control container has a wide variety of options you can use to exercise the control.

A Tour of the Code

Now that we've played with the control a little bit, let's examine the code that the OLE ControlWizard generated for us. To get a copy of the code, you can either run Visual C++'s OLE ControlWizard yourself as described above or click to download the original source. Or, view the code online by shift-clicking to display the index in a new browser window.

Differences between Control Wizard 4.1 and 4.2 Code

There are two significant differences between code generated by Control Wizard 4.1 and 4.2. Either of these differences will keep your project from building if you're moving 4.2 code back to 4.1.

The first difference is that 4.2 adds the line

#include <idispids.h>

near the beginning of your .ODL file. IDISPIDS.H is only available with Visual C++ version 4.2, so comment this line out if you're moving the code back to 4.1.

The second difference is in the call to AfxOleUnregisterTypeLib. This difference is described below in the section that talks about CStopLiteApp.

Files (and Classes)

ControlWizard has generated a bunch of files for us—a .RC file, an .ODL file, a .DEF file, and four pairs of .CPP and .H files—the usual STDAFX.H/STDAFX.CPP, plus a pair for each of the classes ControlWizard creates.

The .RC and .DEF (StopLite.DEF) files are the standard resource and linker definition files. The .ODL (StopLite.ODL) file contains type information for OLE. You can look at this file, but you needn't edit it. ClassWizard will automatically modify it as you add properties, methods, and events.

The STDAFX.CPP and STDAFX.H files are the usual files Visual C++ generates to enable creation and maintenance of the precompiled header (*.PCH) file. STDAFX.H includes MFC (and, indirectly, Windows) headers; STDAFX.CPP, which contains an #include of STDAFX.H, exists only so the precompiled type information will go to STDAFX.OBJ.

The control itself consists of three classes: a control module (or application) class called CStopLiteApp, a property page class called CStopLitePropPage, and the main control window class called CStopLiteCtrl. Before continuing, you can print out at least the three major .CPP files so you can look at the code while you read.

CStopLiteApp (StopLite.H/StopLite.CPP)

This class is analogous to the CWinApp-derived class that's at the heart of regular MFC applications—it's even derived, via COleControlModule, from CWinApp. There will be only one COleControlModule object in your .OCX, even if your .OCX supports multiple controls. You rarely need to modify this class, and for this example, you won't need to modify it at all. (You might need to modify it if you have multiple controls in the same module that need to communicate with each other.)

In addition to the InitInstance and ExitInstance member functions for module initialization and cleanup, these files also contain the DllRegisterServer and DllUnregisterServer global functions you need to have in a "self-registering" control and some read-only global variables: two WORDs, which contain the control's major and minor version numbers, and the GUID for the control's type library.

This file contains the only significant difference between the code generated with Visual C++ version 4.1 and version 4.2. In version 4.1, the DllUnregisterServer function contains the call:

if (!AfxOleUnregisterTypeLib(_tlid))

In version 4.2, the function adds the version number to the call, as is supported by an update to the AfxOleUnregisterTypeLib function:

 if (!AfxOleUnregisterTypeLib(_tlid, _wVerMajor, _wVerMinor))

If you've generated your project with version 4.2, you'll have to change the line above to the version 4.1 call in order to compile under 4.1.

CStopLitePropPage (StopLitePpg.H/StopLitePpg.CPP)

This class is the code for the property page that we'll use to view and set the control's properties. It's actually the code for an individual property page, not for the property page tabbed container, which is called a property sheet.

CStopLitePropPage is derived from COlePropertyPage, which is derived from CDialog. Therefore, CStopLitePropPage behaves much like a standard MFC dialog box. It has the usual message map and DoDataExchange function that all MFC dialog boxes use. (Some new data exchange functions for exchanging data with the control's properties that begin with DDP_ are used in addition to the DDX_ functions in regular modal dialog boxes.)

The control's property page has a few additional pieces that a regular dialog box doesn't have. First, it's declared for dynamic and OLE creation via the IMPLEMENT_DYNCREATE and IMPLEMENT_OLECREATE_EX macros (and their corresponding DECLARE_ variants in the header file). Note that the IMPLEMENT_OLECREATE_EX macro also takes and uses a GUID for the ID of the property page.

Lastly, the property page has a function called CStopLitePropPage::CStopLitePropPageFactory::UpdateRegistry, which registers and unregisters the property page with OLE. This function will be called by MFC when the control is being initialized and destroyed. As the syntax indicates, this function is a member of a class nested inside of CStopLitePropPage.

CStopLiteCtrl (StopLiteCtl.H/StopLiteCtl.CPP)

This class is the most important class in your control. It's where all the action takes place. Except for the property page code, all the code that ClassWizard adds and you edit will go into this class. Note that if this .OCX implemented more than one control, there would be more than one control class. Since this file is large, let's describe the parts of it as they appear in StopLiteCtl.CPP.

First, CStopLiteCtrl has a message map—after all, it's derived from CWnd, which is a command target. In addition, it has a couple of other maps. The dispatch map contains entries for all the properties and methods you implement, while the event map contains entries for all the events you fire. ClassWizard will maintain all of these maps for you.

Next, we have a data structure that contains information on our property page(s), followed by some macros to implement OLE creation and our type library. After this are GUIDs for the two dispatch interfaces used by this control: IID_DStopLite for its properties and methods, and IID_DStopLiteEvents for the events that will be fired to the container.

After that is a rather important variable: _dwStopLiteOleMisc. This variable, which is passed to AfxOLERegisterControlClass later on, contains flags that tell OLE how to create this control and what kind of control it is. See the documentation on the OLEMISC enumeration for information about what these flags mean.

You will need to change CStopLiteCtrl::CStopLiteCtrlFactory::UpdateRegistry, the next function, if you want to mark your control as safe for scripting and/or initialization. (You could also modify DllRegisterServer, but it's a little more convenient to change UpdateRegistry because the control's class ID is readily available.) To mark the control as safe for scripting and initialization, call helper functions that register a component category and register the control as belonging to a category. Because all the of the control's registry entries are deleted when the control is unregistered, you don't have to do anything special to unregister the control. (You should not unregister the categories—other components installed later might need them!) I also added the above-mentioned helper functions (in HELPERS. CPP and HELPERS.H), copied directly from the ActiveX SDK.

So UpdateRegistry just registers and unregisters the control's OLE class. Be sure to read and heed the warning about writing your code correctly so it can run as a multi-threaded apartment model object. All of the details are in MFC Technote 64; for now, just avoid any non-constant global or static member variables. (We won't be using any—they're not really necessary.) It's very important for controls used in Internet applications to be compatible with the apartment threading model—otherwise, performance will suffer greatly if your Web page has more than one control from your control module.

Next is a nearly-empty constructor and an empty destructor that you can add to if needed. You do not need to initialize members that correspond to persistent properties; these will be initialized from persistent data (or default values, if the persistent data is not present) when the control is started.

CStopLiteCtrl is derived from COleControl, which in turn is derived from CWnd. Therefore, it can do basically anything that any window can—draw itself, respond to messages, and so on. However, rather than drawing in an OnPaint function, OLE controls draw in a function called OnDraw. We'll change OnDraw soon to make it draw a stoplight rather than an ellipse. OnDraw differs from CView::OnDraw and from CWnd::OnPaint in two ways. First, you are passed a rectangle representing the extent of the control. You must not draw outside this rectangle. Second, you must erase the control's background in OnDraw rather than relying on a different function to do it for you.

Another function that will look strange yet familiar is DoPropExchange, which is used when saving the control's properties to the container or loading them from the container. The best analogy to an MFC application is that DoPropExchange acts like the document class's Serialize function; however, DoPropExchange is structured more like a dialog box's DoDataExchange (although the exchange functions start with "PX_" rather than "DDX_").

Next to last is OnResetState, which is called when the control should reset its state to the default initial state. Since the default implementation sets property values to the values specified in your DoDataExchange function, you often won't need to change this code.

Last, and perhaps least, is OnAbout, which displays your control's About box.

Specification of the StopLite Control

Next, we'll go through the modifications we made to write this control. Basically, the control will work as follows:

Properties

StopLite implements only one property, Color. Its possible values and their meanings are:

StopLite also uses the BackColor and ForeColor stock properties and the BackColor ambient (container) property (this is the container's background color).

Methods

StopLite has three methods: AboutBox, which displays the About box; Next, which changes the light to the next state (red, green, yellow, red . . .); and the stock Refresh method.

Events

StopLite fires a bunch of events when the light changes state: Off, Testing, Stop, Go, and Caution. It also fires the stock Click event when either mouse button is clicked.

Drawing

StopLite will draw itself as a vertical row of three lights. Each light will be either on or off; except in test mode, at most one light will be on at a time. The lights will be circular regardless of the width of the control, but the size of the lights will be as large as possible given the height of the control. A bezel will be drawn around the lights.

Windows Messages

StopLite responds to left-button clicks (WM_LBUTTONDOWN) by calling the Next method. This gives you a way to call the Next method even when the control is embedded in a container, such as WORD, that doesn't support calling methods.

Marked as Safe, Signed

StopLite is signed to allow users to install the control, even if their browser security is set to high. It also assures them that the control was actually signed by the entity described in the signing certificate and that the control has not been modified since it was signed.

StopLite is also marked as safe for scripting and safe for initializing, since there are no scripting commands nor data values that could cause it to damage a user's system. This allows pages that display StopLite controls to come up immediately without warning dialog boxes (if security is set to medium) or failures (if security is set to high).

How I Wrote StopLite

I actually wrote the control in a different order than I'm presenting below. First, I modified OnDraw so that the control drew something besides an ellipse! Next, I implemented the first property, Color. That necessitated modifying the drawing code to draw the lights in the correct color. Once I had that working, I implemented the Next method and the OnLButtonDown mouse handler so I could see the light change without fiddling with a property page in the test container. (Most satisfying!)

At that point, I created my first Web page for the control using the ActiveX Control Pad, which makes adding controls and your choice of VBScript or JavaScript to Web pages a snap!

Then I added the remainder of the properties, events, and methods.

Finally, I got tired of all of the annoying warning dialogs every time I went to a Web page that contained my control, so I signed StopLite and marked it as safe for scripting and safe for initializing. I learned how to do this using the ActiveX SDK documentation.

But that's a difficult sequence in which to explain the control's code—especially since you don't really want me to blather on about rewriting the OnDraw function fifteen times. So I've captured what I've learned and organized it in a more readable fashion.

Of all of the files in the project, only the StopLite.DEF file didn't change.

Properties

Think of properties as exposing the variables inside a control. In the StopLite control, we have three properties: the BackColor and ForeColor stock properties, and a custom property called Color, which represents the state of the stoplight (off, red, green, yellow, testing). In addition, StopLite uses the container's background color as an "ambient property."

Although properties act as if you're exposing a variable, that's just the programming model. Getting and setting a property can do any operations you choose. So one important way for a container to manipulate its controls is by initializing and changing its properties.

When you insert a control into a container in design mode, you have a chance to set its initial properties. The control passes any changes back to the container, which is responsible for saving the properties the control asks it to. (The control reads and writes its properties in DoPropExchange.) In this way, the state of the control is persistent, and we say that the control "implements persistence." It's the container design program's responsibility to save these properties when it saves the container's representation.

When the container is run and creates the control, it must provide the control with these initial property values.

In Visual Basic, the properties are saved as part of the form's .FRM file. In HTML pages, the properties are saved as <PARAM> tags inside the <OBJECT> tag that specifies how to create the control.

There are several categories and sub-categories of properties. They can either belong to (be implemented by) the control or belong to the container. Control properties are further broken down into custom properties (implemented by you) and stock properties (implemented by MFC). Container properties are called ambient properties; they are further categorized as standard ambient properties, whose meanings are defined by OLE, and extended properties, which are defined by a specific container. Extended properties are more properly thought of as properties of the extended control, which is a control inside the container that encapsulates and extends your control.

Of the four, custom control properties are most commonly used, so we'll discuss them first.

Control Properties

There are two types of control properties: Custom properties are implemented in code you write, while stock properties are implemented by MFC and accessed with COleControl member functions.

Custom properties

Custom properties are implemented by the code in your control. You will typically have a variable in your control class to hold these properties, although you can implement them via Get/Set methods to work in any way you choose.

ClassWizard offers two choices when implementing a custom property: as a pair of Get/Set methods, or as a member variable. In general, using a pair of Get/Set methods is more flexible because you can validate changes to the property and because you can represent the data in any way you choose. Also, if you want your property to be either read-only or write-only, it must be implemented with Get/Set methods.

You can specify that one, and at most one, property in your control is the "default" property. In StopLite, Color is the default property. This means that in your container, you can write:

   StopLite1 = 2;

rather than:

    StopLite1.Color = 2;

It's a minor convenience, but a nice one—most useful when the control is strongly associated with a single value, such as an edit control. The OLE Automation tab in ClassWizard has a checkbox for this. Select the property you want to make the default and check Default Property.

Note that the default property shows up in the container's property list as _Color. You should not use this property—use Color instead. This extra property is an artifact of the way MFC implements default properties.

Implemented as Get/Set methods

In general, this is the method I recommend for implementing properties, because it's more flexible. It allows you to store the property in any way you choose, and it allows you to prevent the properties from changing to values you don't allow. It also allows read-only and write-only properties.

Custom properties implemented using Get/Set methods can be both read and written. When you're using ClassWizard to create the property, it's easy to make it read-only or write-only—just delete the name of the Get method to make it write-only, or delete the name of the Set method to make it read-only. (But don't delete both, okay?)

The Color property in StopLite is implemented as a pair of Get/Set methods. Use the Add Property button on ClassWizard's OLE Automation tab and type "Color" in the External Name drop-down box. Select short as the type, then click on Get/Set methods. Check the function names, then click OK. To make Color the default property, select it while on the OLE Automation tab and click the Default Property checkbox. If you need more assistance with ClassWizard, consult Visual C++'s Books Online.

When you've done all this, ClassWizard will modify your dispatch map to include:

   DISP_PROPERTY_EX(CStopLiteCtrl, "Color", GetColor, SetColor, VT_I2)
   DISP_DEFVALUE(CStopLiteCtrl, "Color")

It will also modify your .ODL file (typelib source) to include the following:

   [id(1)] short Color;
   [id(0)] short _Color;

The fact that the second of the declarations has an ID of 0 is what makes Color the default property. (MFC takes care of the difference between Color and _Color.)

ClassWizard also generates skeletons for the GetColor and SetColor methods. I added a member variable m_color (using a right-click on CStopLiteCtrl in ClassView—yea!) and added code to the functions as follows:

short CStopLiteCtrl::GetColor() 
{
   return m_color;
}
void CStopLiteCtrl::SetColor(short nNewValue)
// set color, redraw; throw error if parameter bad
{
   if (nNewValue >= SL_NONE && nNewValue <= SL_TEST)
      m_color = nNewValue;
   InvalidateControl();
   FireRightEvent();
   SetModifiedFlag();
   else {
      ThrowError(CTL_E_ILLEGALFUNCTIONCALL, 
         "Color parameter out of range");
   }
}

GetColor is simple, but SetColor requires a little explanation. First, we check to see if the value being used is valid. If it is, we set m_color, then update the control. (InvalidateControl redraws, FireRightEvent fires the appropriate event, and SetModifiedFlag lets MFC know that a property has changed.)

If the value passed is not valid, we throw an error using the ThrowError member function. This function is only used in methods, including Get/Set methods. In other situations, use FireError to fire the stock Error method. (Note that SetColor will not be called when the control is initialized—only when the Color member is changed.)

Implemented as a member variable

You can also ask ClassWizard to implement a property as a member variable, without Get/Set functions. (Actually, there are Get/Set functions, but they're implemented by MFC so you don't have to write them.)

Although this is simpler, it has three disadvantages:

  • You cannot validate the value being set.
  • Your member variable must be of the exact same type as the property.
  • You cannot make the property read-only or write-only.

If the disadvantages above aren't a problem in your situation, you can implement a property as a member variable. For instance, if the type of a read/write property is BOOL, it makes sense to implement that property as a member variable.

By default, ClassWizard creates a change notification function, which will be called when the property changes. If we had implemented Color as a member variable, we would have called InvalidateControl, FireRightEvent, and SetModifiedFlag from this function.

Implementing a property as a member variable is very similar to implementing it as Get/Set methods, except that you click on Member variable in the last step of the procedure above. If you don't want a notification function, clear the name in ClassWizard before you click OK.

Making custom properties persistent

ClassWizard will automatically add the necessary code to DoPropExchange to save your properties at design time so they can be reloaded at run time.

In StopLite, ClassWizard and I added the following code:

   PX_Short(pPX, "Color", m_color, SL_TEST); 
               // default to test state (all on)

The last parameter to PX_Short is the property's default value, which I added, specifying it as SL_TEST.

We also need to validate the initialization data to ensure that we're safe for initializing. Just after the line above, I added:

               // check for load of bad value, fix
   if (pPX->IsLoading()) {
      if (m_color < SL_NONE || m_color > SL_TEST)  {
         m_color = SL_TEST;
      }
   }

It is possible for a container to pass a bad value into the control, so it's important to check it both here and in set functions.

Stock Properties

In addition to custom control properties that you implement, a set of stock properties is implemented by MFC. To use them, just add them to your control with ClassWizard—the code is already there to implement the properties. (MFC automatically provides persistence for stock properties.) Check out the list of stock properties by looking up Stock Methods/Properties in the class member list for COleControl.

To access the stock properties from your control, use predefined functions defined as members of COleControl. Most of the functions have names of the form GetXxxx and SetXxxx, where Xxxx is the name of the stock property. There are also a set of stock property notification change functions with names of the form OnXxxxChanged. These have reasonable default implementations, but you can override them if necessary.

StopLite uses two stock properties: BackColor and ForeColor. An important thing to remember about stock color properties is that they are stored as an OLE_COLOR, not as a COLORREF. Windows usually uses COLORREFs, so you'll need to call TranslateColor to translate from an OLE_COLOR to a COLORREF. (There doesn't appear to be a function to perform the inverse translation.)

To add these stock properties, use the Add Property button on ClassWizard's OLE Automation tab and select BackColor (ForeColor the second time . . .) in the External Name drop-down box.

When you're done, ClassWizard will modify your dispatch map to include:

   DISP_STOCKPROP_BACKCOLOR()
   DISP_STOCKPROP_FORECOLOR()

It will also modify your .ODL file (typelib source) to include the following:

   [id(DISPID_BACKCOLOR), bindable, requestedit] OLE_COLOR BackColor;
   [id(DISPID_FORECOLOR), bindable, requestedit] OLE_COLOR ForeColor;

That's all we need to do. We can now call GetBackColor and GetForeColor to get the colors to paint our control. As mentioned above, these stock properties are automatically persistent.

Ambient (Container) Properties

Ambient properties are properties of the control's container, not the control. They are therefore implemented by the container, not by the control. All ambient properties are read-only. You do not need to do anything to access these properties except call the appropriate functions.

Standard ambient properties

There is a group of ambient properties for which dispatch IDs are predefined by OLE. You can access most of these properties by calling member functions of COleControl. To access the few properties for which there is no function, you can call GetAmbientProperty.

StopLite uses the ambient BackColor property to paint the area outside of the control's bezel so it will blend into the container's background. The code to do this is in OnDraw, and it's very simple:

   CBrush brAmbientBack(TranslateColor(AmbientBackColor()));
   pdc->FillRect(rcBounds, &brAmbientBack);

The call to AmbientBackColor reads the ambient background color. Since this is another OLE_COLOR, it needs to be translated before we can create the brush for painting the background.

Some of the commonly used ambient properties include background and foreground colors, font, locale ID, and various properties associated with activation. Adam Denning's OLE Controls Inside Out (Microsoft Press, 1995) has a wonderful discussion of ambient properties.

Extended ambient properties

Control containers are also free to implement any ambient properties they like. If you know the property's dispatch ID and type, you can access it by calling GetAmbientProperty.

Ambient property change notifications

When ambient properties change, the control will be notified. If you want to know about changes, override COleControl::OnAmbientPropertyChange. I did this in StopLite so that I could repaint if the background color changed. I used ClassWizard to write the code by adding a handler for OnAmbientPropertyChange. (This is on the Message Map tab.) The code I wrote to handle the notification checks to see which notification I've received and forces the control to repaint if the ambient background has changed. Note that even if more than one ambient property changes at a time, only one call to OnAmbientPropertyChange will be made. That call will pass DISPID_UNKNOWN as the DISPID. I check for that situation and force a repaint just to be safe.

void CStopLiteCtrl::OnAmbientPropertyChange(DISPID dispid) 
{
   // Repaint if ambient background changed or if several changed
   if (dispid == DISPID_AMBIENT_BACKCOLOR ||
      dispid == DISPID_UNKNOWN)
   {
      InvalidateControl();
   }
   // pass on to base class
   COleControl::OnAmbientPropertyChange(dispid);
}

Methods

Methods are function calls into the control—how the container tells the control to do something. The StopLite control supports two methods: Next and AboutBox.

Stock Methods

MFC provides implementation for a couple of stock methods, which you can easily add to your project using ClassWizard. You don't even have to write any code. The StopLite control implements the Refresh stock method, which forces the control to redraw itself.

To add the stock Refresh method to your control, use the Add Method button on ClassWizard's OLE Automation tab and select Refresh in the External Name drop-down box. ClassWizard will modify your dispatch map to include:

DISP_STOCKFUNC_REFRESH( )
It will also modify your .ODL file (typelib source) to include:
   [id(DISPID_REFRESH)] void Refresh();

If you execute this method in most containers, you won't see any change in the control; it will simply repaint itself as it was. However, this method can be handy if the container knows it's drawn on top of the control and wants the control to redraw itself.

In addition to Refresh, MFC supplies the DoClick stock method, which forces the control to act as if it were clicked.

Custom Methods

Most of the methods your control uses will be custom methods. Stock methods are implemented by MFC; custom methods are implemented by you. In addition to AboutBox, which was written by ControlWizard, the StopLite control implements only one custom method, Next. Next causes the stoplight to change to the next state in its cycle (red, green, yellow, red . . .).

To add the Next method to your control, use the Add Method button on ClassWizard's OLE Automation tab and type Next in the External Name drop-down. ClassWizard will modify your dispatch map to include:

   DISP_FUNCTION(CStopLiteCtrl, "Next", Next, VT_EMPTY, VTS_NONE)

VT_EMPTY means that the return type of the Next method is void, and VTS_NONE means that the Next method takes no parameters. If the method had a return value, its return type constant (VT_xxx) would replace VT_EMPTY. If it took parameters, the types of the parameters would be represented by a comma-separated list of parameter type constants (VTS_xxx) instead of VTS_NONE. The list of constants is in the MFC documentation for DISP_FUNCTION.

ClassWizard will also modify your .ODL file (typelib source) to include:

   [id(2)] void Next();

Next, we'll write the code for the method. If the stoplight is currently off, testing, at the end of the cycle, or in some other state, we'll set it to the first state in the cycle, which happens to be red. If it's already in the cycle, we'll set its state to the next in the cycle. Since we've changed the internal state of the stoplight, we must also redraw it by calling InvalidateControl, fire the appropriate event by calling FireRightEvent, and let MFC know that the state has changed by calling SetModifiedFlag.

Here's the code for Next:

void CStopLiteCtrl::Next() 
// changes stoplight to next state, redraws
{
   if (m_color >= SL_LAST || m_color < SL_FIRST)  {
      m_color = SL_FIRST;
   }
   else m_color++;
   InvalidateControl();
   FireRightEvent();
   SetModifiedFlag();
}

Events

Events are the notifications your control sends to its container when something of interest has happened. The control does this by "firing" an event, which the container can handle with its own code.

Stock Events

MFC provides about a dozen stock events, mainly having to do with mouse clicks and key presses. Two events worth noting are the Error event, which we'll discuss in a bit, and the ReadyStateChanged event, which your control can fire to notify the container that its ready state has changed (for instance, from Loading to Interactive to Complete). This can be useful if your control needs to download a lot of data before it's ready to use.

StopLite implements one stock event, Click. To add this event, click the Add Event button on ClassWizard's OLE Events tab and select Click in the External Name drop-down box.

When you're done, ClassWizard will modify your event map to include:

   EVENT_STOCK_CLICK()

It will also modify your .ODL file (typelib source) to include:

   [id(DISPID_CLICK)] void Click();

No code is added or necessary since MFC implements the FireClick function.

Custom Events

Many of the events your control fires will be custom events. Custom events can pass parameters to provide more information about the situation that caused the event to be fired, although the StopLite control doesn't take advantage of that.

Rather, the StopLite control implements a separate event for each of its states: Stop, Caution, Go, Testing, and Off. Note that the events are not named for the color of the light, but rather for what I expect that the container might do with the event. For instance, the container could call the Stop method on all the cars on the road when it receives the Stop event from the StopLite control.

To add these events to your control, use the Add Event button on ClassWizard's OLE Events tab and type the name of each event in the External Name drop-down box. For these events, you won't need any parameters.

When you're done, ClassWizard will modify your event map to include:

   EVENT_CUSTOM("Stop", FireStop, VTS_NONE)
   EVENT_CUSTOM("Caution", FireCaution, VTS_NONE)
   EVENT_CUSTOM("Go", FireGo, VTS_NONE)
   EVENT_CUSTOM("Testing", FireTesting, VTS_NONE)
   EVENT_CUSTOM("Off", FireOff, VTS_NONE)

VTS_NONE in the macro invocations above means that the events have no parameters. If the events had parameters, constants describing their types would be listed in order starting after the function name. The list of constants is in the MFC documentation for EVENT_CUSTOM.

It will also modify your .ODL file (typelib source) to include:

   [id(1)] void Stop();
   [id(2)] void Caution();
   [id(3)] void Go();
   [id(4)] void Testing();
   [id(5)] void Off();

Finally, ClassWizard also adds the code you need to implement these events to the class' declaration in its header file:

   void FireStop()
      {FireEvent(eventidStop,EVENT_PARAM(VTS_NONE));}
   void FireCaution()
      {FireEvent(eventidCaution,EVENT_PARAM(VTS_NONE));}
   void FireGo()
      {FireEvent(eventidGo,EVENT_PARAM(VTS_NONE));}
   void FireTesting()
      {FireEvent(eventidTesting,EVENT_PARAM(VTS_NONE));}
   void FireOff()
      {FireEvent(eventidOff,EVENT_PARAM(VTS_NONE));}

To make it easier to fire events, I wrote a helper function called FireRightEvent. Call it after the light changes. It fires the appropriate event based on the stoplight's new state. Here's the code:

void CStopLiteCtrl::FireRightEvent()
// called whenever the stoplight state changes to fire the
//   appropriate event—must call AFTER m_color set to new value!
// Use the source browser to make sure you call each time 
//   m_color changed!
{
   switch (m_color) {
      case SL_RED: FireStop();
         break;
      case SL_YELLOW: FireCaution();
         break;
      case SL_GREEN: FireGo();
         break;
      case SL_NONE: FireOff();
         break;
      case SL_TEST: FireTesting();
         break;
   }
}

Making the Control Draw

Compared with other types of Windows programs, the drawing code for ActiveX controls must meet some special requirements.

First, your control must draw on demand. Your control will be asked to draw itself whenever the container needs it to. The container will pass it a device context (DC) and two rectangles—a bounding rectangle and a rectangle that encloses all the invalid regions. (If you're new to MFC and/or Windows, read up on device contexts.)

You must redraw at least the pixels enclosed by the invalid rectangle but may not draw any pixels outside the bounding rectangle. You can optimize your drawing code by not drawing pixels outside the invalid rectangle.

Note that the bounding rectangle's upper left corner is not guaranteed to be at (0, 0)—it usually will be when drawing a control that's in the active state, but not, for instance, when drawing a control that's in the inactive state.

Second, you must paint your background. Unlike painting a window or drawing a view, you must paint your background in the control's OnDraw function. Background painting is not done automatically, nor is it done in some other function.

Third, you may not assume anything about the DC you've been passed, so you should select the pens, brushes, fonts, and colors you need when you draw your control. Some containers will pass your control a metafile DC in some situations, especially when printing. In order to work properly when drawing into a metafile, you must restrict your function calls to those supported in metafiles. See the MFC Encyclopedia article "OLE Controls: Painting and OLE Control" for more information. Finally, you must always select the old drawing objects back into the DC before your drawing function exits.

Lastly, the container, not the control, determines the control's size and position. Because the control doesn't determine the size and position of your control's visible area, you must either scale your output to fit in the window you're provided or supply scrolling—or both. The StopLite control scales drawing according to the height of the control and centers it across the width. A control such as a grid will always draw using the same scaling but will provide scrolling if the area to be drawn is larger than the area the container gives you.

The requirements are even more complicated for transparent and other windowless controls, but that's beyond the scope of this article.

Drawing the StopLite Control

The OnDraw function for the StopLite control does three tasks: drawing the background, drawing the stoplight's bezel, and drawing the lights.

Here's the code for OnDraw:

void CStopLiteCtrl::OnDraw(
         CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)
{
   // 1. erase background using container's background color
   CBrush brAmbientBack(TranslateColor(AmbientBackColor()));
   pdc->FillRect(rcBounds, &brAmbientBack);
   
   // 2. draw bezel using control's stock properties 
   //   BackColor and ForeColor
      // calculate size based on 40% of height
   CRect rcBezel(rcBounds);
   int nHeight = rcBounds.Height();
   int nWidth = rcBounds.Width();
   int nBezelWidth = nHeight * 40 / 100;
   if (nBezelWidth > nWidth) 
      nBezelWidth = nWidth;   // not more then width!
   int nDeflateBezel = (nWidth - nBezelWidth) / 2;
   rcBezel.DeflateRect(nDeflateBezel, 0);
      // create and select brush and pen
   CBrush brBack(TranslateColor(GetBackColor()));
   CBrush * pbrOld = pdc->SelectObject(&brBack);
   CPen pnFore(PS_SOLID, 2, TranslateColor(GetForeColor()));
   CPen * ppnOld = pdc->SelectObject(&pnFore);
      // draw
   pdc->Rectangle(rcBezel);
      // select old brush, but not old pen
   pdc->SelectObject(pbrOld);
   // 3. draw lights using stock ForeColor, already selected
      // translate enum code to bits for red, green, yellow
   int nLights = TranslateLights();
      // percentages are percentage of height
      // draw red light on top, 6% down, 27% diameter
   DrawLight(pdc, rcBounds, 6, 27, 
      (nLights & SLBIT_RED) ? SLCOLOR_RED : SLCOLOR_OFF);
      // yellow light in middle, 37% down, 27% diameter
   DrawLight(pdc, rcBounds, 37, 27, 
      (nLights & SLBIT_YELLOW) ? SLCOLOR_YELLOW : SLCOLOR_OFF);
      // green light on bottom, 68% down, 27% diameter
   DrawLight(pdc, rcBounds, 68, 27,
      (nLights & SLBIT_GREEN) ? SLCOLOR_GREEN : SLCOLOR_OFF);
   pdc->SelectObject(ppnOld);
}

1. Drawing the background

This is the simplest of the three tasks: all we do is create a brush using the container's ambient background color, then use the FillRect function to draw a rectangle filling the entire control with that brush. FillRect automatically takes care of selecting and deselecting the brush.

2. Drawing the bezel

This is a little trickier because we have to scale the output rather than just paint the entire control. Specifically, we'd like the bezel to be a little wider than the lights, but not the full width of the control. First, we set up a rectangle that will be the size of the bezel (when we're done with it). We calculate the bezel size to be 40 percent of the height and then calculate how much to deflate the left and right sides of the rectangle. (Deflating it keeps it nicely centered.) Note that if the bounding width is less than the bezel width, the bezel width is set to the bounding width. This keeps us from drawing outside the bounding rectangle.

Once we have a rectangle for the bezel, we create a brush and select it into the DC, saving the pointer to the current brush returned by SelectObject so we can select it back in later. We do the same routine with the pen. In both cases, the color is selected from a stock property. Next, we draw the rectangle and select the old brush (but not the pen—we're still going to use it) back into the DC.

3. Drawing the lights

This is the trickiest of all. To draw the lights, I wrote a couple of helper functions—TranslateLights and DrawLight—which I'll discuss below. Note that I wrote the code for clarity, not top speed. If I were more concerned with speed, I'd be sure to calculate the various intermediate values as infrequently as possible.

Visual C++ includes a cool feature for adding member functions and member data: Just right-click on the class name in ClassView and select Add Function or Add Variable. When you do this, Visual C++ writes the header and a blank function skeleton for you. It even remembers to include the class name in the function definition. (I never remember this!)

The main idea is to call TranslateLights to get the bits representing which lights are on into the variable nLights, then call DrawLights for each light, passing the DC, the bounding rectangle, the distance to draw the light from the top (expressed as a percentage of the height), the diameter (as a percentage of the height), and the color to draw the light. SL_RED, SL_GREEN, and SL_YELLOW are defined as the RGB values for each color; SL_OFF is defined as dark gray. I measure drawing distances as percentages to make figuring out where to draw things easier.

Finally, we select the old brush (saved while drawing the bezel) back into the DC.

Here are the definitions of the SL_* constants. Note that I've put them inside the StopLiteCtrl class, so they're "local" to the class:

// stoplight state (values of m_color/Color property)
enum { SL_NONE = 0,
      SL_FIRST = 1,   // first light in cycle
      SL_RED = 1, SL_GREEN = 2, SL_YELLOW = 3, // cycle order
      SL_LAST = 3,   // last light in cycle
      SL_TEST = 4 };   // test MUST be last (see SetColor)
   
// bits corresponding to the three lights; used for drawing
enum { SLBIT_RED = 1, SLBIT_GREEN = 2, SLBIT_YELLOW = 4,  };
// possible colors for light: off, red, green, yellow
enum { SLCOLOR_OFF = RGB(63, 63, 63),   // dark gray
      SLCOLOR_RED = RGB(255, 0, 0),
      SLCOLOR_GREEN = RGB(0, 255, 0), 
      SLCOLOR_YELLOW = RGB(255, 255, 0) };

TranslateLights

TranslateLights takes the Color property and sets the appropriate bits of the return value in a switch statement.

Here's the code for TranslateLights:

int CStopLiteCtrl::TranslateLights()
// sets appropriate bits for stoplight state
{
   int nLights = SLBIT_RED;   // safe default
   switch (m_color) {
   case SL_NONE: nLights = 0;
      break;
   case SL_RED: nLights = SLBIT_RED;
      break;
   case SL_GREEN: nLights = SLBIT_GREEN;
      break;
   case SL_YELLOW: nLights = SLBIT_YELLOW;
      break;
   case SL_TEST: nLights = SLBIT_RED | SLBIT_YELLOW | SLBIT_GREEN;
      break;
   }
   return nLights;
}

DrawLight

DrawLight is somewhat complicated. First, we calculate the width and height of the control's bounding rectangle, then the diameter of the light in pixels. If the diameter is larger than the width of the bounding rectangle, we set it to the width of the bounding rectangle to ensure that we don't draw outside of the bounding rectangle.

We then calculate the left and top edges of the circle's bounding square, and construct a CRect object of that size and position. (We ensure that the CRect is entirely inside the bounding rectangle by using the IntersectRect function.)

Next, we create a brush of the appropriate color and select it into the DC, then draw the circle using the Ellipse function. Finally, we select the old brush back into the DC.

Here's the code for DrawLight:

void CStopLiteCtrl::DrawLight(
// draws an individual light centered in the control at
//   vertical position specified
      CDC* pdc,            // DC in which to draw
      const CRect& rcBounds,   // control's rectangle
      int nPercentDown,      // top position as % of height
      int nPercentDiameter,   // diameter as % of height
      COLORREF crColor      // color to fill light   
   )
{
   // calculate diameter in drawing units
   int nHeight = rcBounds.Height();
   int nWidth = rcBounds.Width();
   int nDiameter = nHeight * nPercentDiameter / 100;
   if (nDiameter > nWidth) 
      nDiameter = nWidth;   // but not greater than width!
   // create light's bounding rect
   int nLeftEdge = (rcBounds.left + rcBounds.right - nDiameter) / 2;
   int nTopEdge = rcBounds.top + nHeight * nPercentDown / 100;
   CRect rcLight(   nLeftEdge,
               nTopEdge,
               nLeftEdge + nDiameter,
               nTopEdge + nDiameter);
   // make absolutely sure we're within bounds—
   // distort circle if necessary!
   rcLight.IntersectRect(rcLight, rcBounds);
   // create brush, draw, select old brush
   CBrush brColor(crColor);
   CBrush * brOld = pdc->SelectObject(&brColor);
   pdc->Ellipse(rcLight);
   pdc->SelectObject(brOld);
}

Responding to Windows Messages

Handling Windows messages is very much like handling messages in any MFC application—you just add message handlers to your window, which in this case is derived from COleControl.

The StopLite control doesn't need any keyboard messages, but your control might. If you need to get messages for the enter key or the escape key, or if you need to use accelerators, I highly recommend the section on keyboard issues in Denning's OLE Controls Inside Out or "Developing OLE Custom Controls" by Scott Randall, available on the Library Archive (Conference and Seminar Papers; Tech Ed, March 1994; Visual C++).

The StopLite control does respond to one Windows message: It selects the next state in sequence when you left-click on it.

To respond to left button clicks, use ClassWizard to add a handler for the WM_LBUTTONDOWN message, then call the Next function from within that handler before you call the base class' function.

ClassWizard adds the appropriate declaration to the header, then adds the following entry to the message map:

   ON_WM_LBUTTONDOWN()

I modified the function it added so that it called the Next method. When I was done, the code looked like this:

void CStopLiteCtrl::OnLButtonDown(UINT nFlags, CPoint point) 
// call Next method on left click, allow Click stock event to be fired
{
   Next();
   
   COleControl::OnLButtonDown(nFlags, point);
}

Property Pages

ActiveX controls can support property pages to make modifying the control's properties at design time much easier. The default page generated by AppWizard is blank except for a static text control that contains a message reminding you to finish the property page.

Doing the property page is even simpler than doing a dialog box in MFC, because ClassWizard will write the code to transfer data between your property page class' members and the properties. First, edit the dialog template (in our case, it's IDD_PROPPAGE_STOPLITE) to create the controls you'd like for your property UI. Our case has only one important property (Color) and the light can have only one Color value at a time, so I used a set of option buttons, or radio buttons, as the UI.

When using option buttons, you must set the tab order correctly, so the buttons are read properly, and be sure that only the first option button (Off) has the Group checkbox checked on its property page. The screen shot above shows that the tab order is correct.

It is also important that the buttons be in the same tab order as the values they represent in the Color property. That means I don't have to translate the values going in and out of the control.

Once I had the dialog box template set up correctly, adding the code was a snap—or, more precisely, a double-click with the control key held down. All I did was Ctrl+double-click on the Off button, and ClassWizard came up ready to add the code I needed to handle this property page. In the Add Member Variable box, all I needed to do was name the member variable (m_color in my case) and tell ClassWizard what property to attach it to (Color). ClassWizard generated all the right code, adding a declaration for the member variable to the header file:

   int m_color;

initializing it in the constructor:

   m_color = 0;

and adding lines to the DoDataExchange function so the value in the option buttons would be transferred back and forth correctly to the control's property:

   DDP_Radio(pDX, IDC_OFF, m_color, _T("Color") );
   DDX_Radio(pDX, IDC_OFF, m_color);

I didn't have to write a line of code. Even if you're not using Visual C++, getting a property page in exchange for four lines of code is a pretty good deal.

Notifying Your Container of Errors

In some situations, you'd like to communicate the errors to the container. There are two ways of notifying the container of errors, depending on whether the code that detected the error is in a method call (including Get/Set methods) or not (such as the code that processes a Windows message).

From Within a Method Call

Within a method, call COleControl::ThrowError. StopLite does this in SetColor if the color requested is invalid:

   if (nNewValue >= SL_NONE && nNewValue <= SL_TEST)  {
      // ...set color and redraw, etc.
   }
   else {
      ThrowError(CTL_E_ILLEGALFUNCTIONCALL, 
         "Color parameter out of range");
   }

In Other Situations

Call COleControl::FireError. To do this, you must add the Error stock event to your control.

A Brief Note About Security Issues

Internet Explorer 3.0 implements two mechanisms to ensure that users can load and run controls without harming their systems. First, it checks the signature of any code it installs. If the signature is missing or invalid, it will refuse to install the code (when security is set to high, the default) or at least notify you and allow you to decide (when security is set to medium). When security is set to none, Internet Explorer 3.0 doesn't check signatures.

Second, if your Web page attempts to initialize and/or script the control, Internet Explorer 3.0 checks to see if it's marked as safe for initializing and/or safe for scripting. If the control is not marked safe for what Internet Explorer 3.0 wants to do, the browser will ignore the control (when security is set to high) or display a dialog box (when security is set to medium).

To get around all of the dialog boxes and to run at all on Internet Explorer 3.0 when security is set to high, you have to sign your control and mark it as safe.

Signing your code only takes about 3.5K, which adds up to one second at 28.8Kbps, so it won't significantly slow Web page download times.

Code Signing

Code signing is the process of adding data to the control so that Internet Explorer 3.0 can ascertain two things:

  • The control has not been modified since it was signed.
  • The claimed author is actually the author.

Signing a control requires an electronic "certificate" provided by a "certificate authority" (CA). When you apply for a certificate, the certificate authority does some checking to determine that you are who you say you are, then issues the certificate. You can use this certificate with some utilities in the ActiveX SDK to sign your control. Note that you must guard this certificate and the private key carefully. If someone else obtains them, they can sign their code in your name.

You can find more information on code signing in the MSDN Library and in the ActiveX SDK.

I did not sign the StopLite control myself, as Microsoft has a central signing procedure for signing code. I just followed their procedure, and they signed the code for me. If you work for a company of any size, your company will probably have a centralized signing procedure.

Marking Your Control as Safe

Now that we know from whom the control came, how do we know it's safe to use? Remember that ActiveX controls, unlike Java applets, can access the full power of your machine—for good or evil.

If it's not possible for your control to cause harm to a user's machine no matter what, you can mark it as safe. It is your responsibility to ensure that any control you mark is actually safe—Internet Explorer 3.0 has no way to ensure that a control marked as safe is actually safe.

There are two categories of safety: safe for scripting and safe for initializing. Safe for scripting means that no matter what scripts do to manipulate your control, it will not harm the user's machine. Safe for initializing means that no matter what data is passed to your control from the Web page, it will not harm the user's machine. StopLite is both safe for scripting and safe for initialization and is marked accordingly.

There are two methods for letting Internet Explorer 3.0 know that your control is safe:

  • Manipulate the registry to include component categories for safety and add an entry saying that your control is safe.
  • Implement the IObjectSafety COM interface.

For more complete information on marking controls as safe, check out my article Signing and Marking ActiveX Controls.

Unfortunately, there is currently no method for signing Web pages. This would be handy for using potentially unsafe controls because the Web page author could assert that the page doesn't contain scripting or initialization that would cause an otherwise potentially unsafe control to damage a user's machine. Such a scheme would also require a way to ensure that the right controls are installed.

Manipulating the Registry

The simpler method for marking a control safe for scripting and initialization is to add the necessary entries to the registry. I chose this method for StopLite.

Although it is possible to do this by hand or with a .REG file, self-registering controls should do this automatically when they register themselves. This is especially true for controls that will be downloaded over the Internet.

When the control unregisters itself, it will automatically remove all of its registry entries. Therefore, we don't need to do anything special to unregister the control. (We don't remove the categories because other controls might be using them.)

Here is the code I wrote to mark the control as safe for scripting and initializing. First I create the component categories, then I mark this particular control as safe in that category. This code might fail if it's being run on a system that doesn't have Internet Explorer 3.0 installed; because such a failure is okay (for instance, the user might be using the control in a Visual Basic application), we don't halt initialization for the control if this code fails.

   // mark as safe for scripting—failure OK
         HRESULT hr = CreateComponentCategory(CATID_SafeForScripting,
            L"Controls that are safely scriptable");
         if (SUCCEEDED(hr))
            // only register if category exists
            RegisterCLSIDInCategory(m_clsid, CATID_SafeForScripting);
            // don't care if this call fails
         // mark as safe for data initialization
         hr = CreateComponentCategory(CATID_SafeForInitializing,
            L"Controls safely initializable from persistent data");
         if (SUCCEEDED(hr))
            // only register if category exists
            RegisterCLSIDInCategory(m_clsid, CATID_SafeForInitializing);
            // don't care if this call fails

This code requires that the helper functions CreateComponentCategory and RegisterCLSIDInCategory be defined. I copied definitions from the ActiveX SDK. The definitions are in Helpers.H and Helpers.CPP. (You can use these files as-is in your projects.) In STOPLITECTL.CPP, I also had to include a couple files:

// for marking the StopLite safe for scripting, initializing
#include "helpers.h"
#include <objsafe.h>

The file HELPERS.H also includes COMCAT.H which, like OBJSAFE.H, is in the ActiveX SDK.

If my control were not written using MFC, I'd do the marking in DllRegisterServer.

Implementing IObjectSafety

The other way to "mark" a control as safe is to implement the IObjectSafety COM interface. We won't be covering it in this article, but it is covered in Signing and Marking ActiveX Controls and in the ActiveX SDK.

Although slightly more complicated, implementing IObjectSafety has some advantages in that the interface allows the container either to ask whether the control is safe using GetInterfaceSafetyOptions or to request that it make itself safe using SetInterfaceSafetyOptions. (GetInterfaceSafetyOptions also allows you to ask the control what safety options it supports.) This flexibility allows you to write your control so that it can operate in either safe or unsafe mode. If the container requests that you be safe for scripting and/or initialization, your control can change modes and behave accordingly. If the container doesn't care about safety, your control can run in unsafe mode and exploit the full power of the machine.

Using Visual C++ and Test Container to Debug the Control

At various points in the development process, you might want to be able to examine variables in your control, set breakpoints, and single-step the code.

This is quite easy. First, build the control for Debug. Then, provide the name of an executable that will host the control while debugging. I usually use the test container, which is at C:\MSDEV\BIN\TSTCON32.EXE in a standard Visual C++ installation. To change the container .EXE, use the "Settings" command on the Build menu and select the Debug tab. (You can debug in any container, although I find the test container most convenient.)

Once you have things set up, just select Debug.Go from the Build menu. The test container will start. As you insert the control in the test container, you'll get a number of messages you can safely ignore (see below).

At this point, if you haven't already, you can set breakpoints in your code, examine variables, single-step, and so forth—just as if you were debugging an application.

If you're using another development system, check your documentation for debugging directions.

Debugging Output You Can Safely Ignore

When you're debugging a control, you may get a series of messages in Visual C++'s debugging window. Although it's not documented anywhere, some of these messages can be safely ignored. For instance, when I debug the StopLite control in the test container using Visual C++ version 4.2, I get the following:

Loaded symbols for 'C:\WINNT35\system32\MFC42.DLL'
LDR: WARNING ! MAJOR PERFORMANCE LOSS in TSTCON32.EXE
LDR: Dll MFC42D.DLL base 5f400000 relocated due to collision with
C:\WINNT35\System32\MFC42.DLL
Loaded symbols for 'C:\WINNT35\system32\MFC42D.DLL'
Loaded symbols for 'C:\WINNT35\system32\MSVCRTD.DLL'
Loaded symbols for 'C:\WINNT35\system32\MFCO42D.DLL'
Warning: constructing COleException, scode = DISP_E_MEMBERNOTFOUND ($80020003).
First-chance exception in TSTCON32.EXE (MFCO42D.DLL): 0xE06D7363: Microsoft C++          Exception.

The first line is just notification that the debugging symbols for the release version MFC DLL were loaded when the test container, which uses the release version of the MFC DLL, was loaded.

The second and third lines (which begin with "LDR:") warn that the debugging version of the MFC DLL (used by the StopLite control) had to be relocated in my address space because it conflicted with the release version of the same DLL (which is being used by the test container). Although this means that it took a little longer to load the MFC DLL, it isn't a big deal because it only happens when you're debugging in the test container. You can ignore these messages. (Since Visual Basic apps don't use the MFC DLL at all, you won't get this message if you use a Visual Basic app as your test container.)

The next three lines are notifications that debugging symbols have been loaded.

The last two lines are notification that an exception has been thrown because the test container didn't provide a Color property to be read. This is okay, because we've provided a default value for the property. You won't get this error if you use a container that provides the property, such as a Visual Basic application.

In Visual C++ version 4.1, the only line I got was a warning about the exception for properties that couldn't be loaded from the container.

First-chance exception in TSTCON32.EXE (MFC40.DLL): 0xE06D7363: Microsoft C++ Exception. 

Depending on how your container handles threads, you may also get occasional messages about threads exiting. You don't need to worry about these, either.

Creating a Web Page Using the ActiveX Control Pad

Now that we have a control, let's use it in a Web page!

The ActiveX Control Pad is the easiest way to create Web pages that contain ActiveX controls and scripting. And the price is right: It's free at http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnaxctrl/html/cpad.asp! The ActiveX Control Pad is mainly intended to create .ALX files for the HTML Layout Control, which allows precise placement and layering of controls. However, the ActiveX Control Pad also works great for authoring Web pages with controls and scripting, so that's how we'll use it.

Inserting a StopLite Object

When you start the ActiveX Control Pad, it brings up an empty page. The first step is to insert a control using the Insert ActiveX Control command on the Edit menu. You can set the size and initial properties using the default property sheet, or call up your custom property sheet by right-clicking on the control and selecting the bottom "Properties . . ." choice. Once you've done this, close the Edit ActiveX Control Window.

You'll see that the ActiveX Control Pad has inserted an object tag for you—and placed an icon in the margin. Any time you'd like to edit the control's size or properties, just click on that icon.

Inserting Other Controls

Next, we'd like to insert some buttons. I don't want to use an OLE control for the buttons, so I just type an HTML <INPUT> tag in place: specifically, <INPUT TYPE=BUTTON NAME="NEXT" VALUE="Next Light">.

On the next line, I'd like an IELabel control, so I insert a paragraph tag (<P>), then use Insert ActiveX Control again to insert the label control ("Label Object" in my control list).

Scripting with Script Wizard

Now, let's do a little scripting. One of the best features of the ActiveX Control Pad is the Script Wizard, which helps you write code in your choice of VBScript or JavaScript. Just click on the Script Wizard toolbar button to start.

This is really intuitive: just select the event you'd like to handle, select the object you'd like to do something to, and tell it what to do. We'll do two events: the click of the Next button and the Stop event fired by the StopLite control.

First the Next button. Expand Next in the left pane, then click on OnClick. What we'd like to do is call the StopLite's Next method, so expand StopLite1 in the right pane, then double-click on Next. That's it. You can save your page and load it into Internet Explorer 3.0 now if you like—or handle the Stop event.

To handle the StopLite's Stop event, follow basically the same procedure: select StopLite1. Stop, double-click on IELabel1. Caption, then type (into the dialog box that pops up) the text you'd like to see.

Viewing the Page

We're done for now. You can duplicate (or better!) my page on your own knowing what you now know. Save your work (I usually save it on my desktop) and test it in Internet Explorer 3.0!

Note that you'll get warning dialogs if your control isn't signed and/or marked as safe. And, if Internet Explorer's security is set to high, you won't be able to view the control at all. (For development purposes, it's often better to set the security to medium. We don't recommend setting it to none.)

Conclusion

We've created a simple control in MFC and embedded it in a Web page. We've looked at drawing the control, handling Windows messages, and implementing properties, methods, and events. But there are some things we haven't done.

First, we could add more functionality to the control—the capability for blinking lights (even a blinking green light, as used in Vancouver, Canada), or turn arrows. We could even modify the sequence of lights to include allowing the yellow and red lights to be on at the same time just before the light changes to green, as they do in the United Kingdom. (Note that the drawing code design makes such a modification easy and safe.) Or we could write some of the other controls needed as components in a traffic simulation.

Then there are OCX 96 enhancements: making the control windowless for improved performance, transparent controls, and so on. Implementing dual-automation interfaces for better performance would be nice. Or we could just set up the control for licensing to users and designers.

We could also package up the control and its DLLs so that Internet Explorer 3.0 could download and run it automatically. We'll have an article about that subject soon.

The most important step is to sign the control by attaching a certificate to it so that people who download it will know who wrote it—and so Internet Explorer 3.0 will use it without a dire warning message. Check out Signing and Marking ActiveX Controls for more information.

Where to Learn More

How can you learn more? Good information on new control features can be found in the ActiveX SDK and on the MSDN Library site. Another great source, as I've mentioned previously, is Adam Denning's OLE Controls Inside Out. And the Visual C++ 4.2 MFC Encyclopedia is great, as is the rest of the doc set.

Enjoy! And start writing ActiveX controls for fun and profit!