| A couple of months ago I read an article that compared writing ActiveXÂ® controls in MFC to writing them in ATL. I know you can also write ActiveX controls in Visual BasicÂ®. Can you tell me how writing ActiveX controls in Visual Basic is different from writing them in C++?|
Parker Wiebe That's a great question. I've never seen any information comparing one approach to the other. Of course, in the end the client doesn't care, right? This is COM and as long as the client and the object agree on the interfaces to use, the two should be able to integrate easily. However, each environment has some differences when it comes to implementations and specific features. This month I'll compare creating ActiveX controls using Visual Basic with creating ActiveX controls using C++ (and the C++ control frameworks, MFC and ATL).
The sample control I'll use is a pair of dice that generates random numbers between 2 and 12. Just load the control into some application, then double-click the dice. They'll roll a predetermined number of times then report the results to the client as events. Along the way, I'll focus on how the controls themselves differ when they're written in Visual Basic and C++.
ActiveX controls are COM objects generally intended to function as little UI gadgets inside a WindowsÂ®-based application. They have some sort of rendering capability and persistent state, and they report events back to the client application. All this is accomplished through a series of standard COM interfaces. When an ActiveX control container finds a COM object that implements the necessary interfaces, the two pieces of code can hook up to do some useful things.
In the old days, there was only one kind of controlâ"an OLE control. These beasts borrowed heavily from the older linking and embedding protocol prevalent during the mid-90s. In 1996, these user interface gadgets became known as ActiveX controls, and the features that constituted an OLE control (rendering, events, persistence, and so on) became optionalâ"though the control is fairly useless without these features.
The main tools used for producing ActiveX controls are Visual Basic and Visual C++Â®, and MFC and ATL are the Visual C++ frameworks for implementing controls. All three control frameworks come with code generators so you don't need to create all the code by hand. However, the code produced by each code generator is slightly different. Here's a rundown of the process of control creation with each tool.
The first difference you encounter when comparing control creation with Visual Basic to ATL and MFC is the kinds of controls supported. MFC and Visual Basic are similar in that they create full controls right off the bat, letting you do only minor tweaking to the resulting control. A full control is one in which the entire OLE linking and embedding protocol is implemented, including design-time support. Using Visual Basic and MFC, there's no easy way to pick and choose the specific interfaces you want supportedâ"they're all turned on by default. All three environments support full controls, licensing, windowless controls, and transparent controls. These features are available via various checkboxes and radio buttons in the wizards or by typing the code in yourself. Figure 1 shows the differences among MFC, ATL, and Visual Basic in terms of adding these advanced features to your control.
There are a couple of other noticeable differences between Visual Basic and the Visual C++ frameworks. First, Visual Basic and ATL both support composite controlsâ"ActiveX controls whose presentation spaces are composed of other standard Windows controls. Composite controls aren't explicitly supported in MFC, but they can be hacked out if you have the time and patience. Additionally, an option available in ATL but unavailable in MFC and Visual Basic is the capability to use HTML to represent the control's user interface (this is the HTML control in the ATL ObjectWizard).
Once the control is created and registered, you've got a COM class on your system capable of being loaded and used as an ActiveX control. That means the class implements the standard set of COM interfaces that clients expect to see in a any COM object that dares to call itself a control. Figure 2 lists the interfaces implemented by controls written in Visual Basic, MFC, and ATL.
Figure 2 shows that the core control functionality to support controls is available in all three frameworks. For example, at the very least, a COM object has to support IOleControl, IOleObject, IOleInPlaceObject, IViewObject, IPersistStreamInit, IProvideClassInfo, and IOleInPlaceActiveObject. If you don't include these interfaces on your COM object, the client won't be able to use it as a control. Unless you've spent some time rummaging around the linking and embedding protocol, some of these interfaces may not look familiar. Don't worry, these are really just the boilerplate interfaces that are necessary to support the UI embedding protocol.
Beyond the usual COM interfaces for controls, Visual Basic, MFC, and ATL support other features to varying degrees. For example, Visual Basic and ATL (but not MFC) implement ISupportErrorInfo for enabling COM exceptions (ISupportErrorInfo is available to the ATL-based control via a checkbox on the ObjectWizard). Of the three frameworks, only MFC supports IPersistMemory.
IPersistMemory is similar to IPersistStreamInit, except that the caller provides a fixed-size memory block as opposed to a pointer to the IStream interface. Only Visual Basic supports IServiceProvider and IParseDisplayName. IServiceProvider lets clients query for a service (a higher-level protocol than a simple interface) identified by a GUID. Services are often used when implementing ActiveX Designers. Visual Basic (but neither MFC nor ATL) supports IParseDisplayName, which is generally used for support monikers. Both MFC and ATL (but not Visual Basic) implement IViewObjectEx, which extends IViewObject and IViewObject2 to support drawing optimizationsâ"particularly for preventing flicker.
When you create a control in Visual Basic (by clicking File | New Project and selecting the ActiveX Control option), Visual Basic pumps out the source code for an ActiveX control housed inside a DLL. The DLL is a full COM DLL with the requisite entry points, including DllCanUnloadNow, DllGetClassObject, DllRegisterServer, and DllUnregisterServer. Visual Basic adds an object of type UserControl to your project as well. The UserControl embodies the code for the control and includes implementations of the interfaces I just mentioned.
The nice thing about Visual Basic is that the grungy code is really well-hidden behind well-established event handlers. For example, all controls have initialization, loading, and termination event handlers that correspond to the underlying control protocol. These five basic events handled by the controls include generic initialization, property initialization, property loading, property saving, and control termination.
The UserControl's Initialize handler is called when the client calls CoCreateInstance to create the control. The UserControl's InitProperties handler is called during the client's call to IPersistStreamInit::InitNew. The control's properties are loaded through the ReadProperties handler, which is invoked when the client calls either IPersistPropertyBag::Load, IPersistStreamInit::Load, or IPersistStorage::Load. The UserControl's property-saving code is wrapped up in the WriteProperties handler, which is called during a client's call to either IPersistPropertyBag::Save, IPersistStreamInit::Save, or IPersistStorage::Save. Finally, the UserControl's Terminate handler is called when the last interface pointer to the control is released by the client.
I'll look at each of these handlers in a bit more detail and compare them to MFC and ATL handlers.
C++ has constructors and destructors into which your initialization and termination code is supposed to go. Because ATL includes a special declaration for reducing intermediate vtable bloat (ATL_NO_VTABLE), ATL classes include a virtual FinalConstruct method where any control initialization should go. In Visual Basic, the place to initialize the control is within the UserControl's Initialize event handler.
The sample Dice control uses picture bitmaps to display the die faces and some internal housekeeping variables. These bitmaps are loaded and the other data members are initialized during the Initialize event, as shown in Figure 3. The dice control works by managing a timer. When you double-click the control within the client, the Dice control starts a timer, responding to the timer messages by generating a new pair of random numbers and showing the new die faces. You should note that the initialization code also initializes the timer.
The Dice control includes two picture controls in Visual Basic, one representing each die face. The bitmaps are implemented as resources in the control and simply loaded into arrays during initialization. There's one array of die face bitmaps for each color supported by the controlâ"red, white, and blue. In addition to loading the die faces, the Initialize handler sets the internal roll count to zero, turns off the timer, initializes the timer interval, initializes the random number generator, comes up with random numbers to start with, and shows those die faces.
The code that you'd normally put into a destructor goes in the UserControl's Terminate event.
Controls are visual components, so they need to render themselves. There are two occasions when an ActiveX control will draw itself: when responding to the WM_PAINT message, and when the client calls IViewObject::Draw. MFC and ATL each have OnDraw methods with device contexts prepared by the framework as parameters. The device context is either a real device context representing the screen or a metafile on which to make a snapshot of the last control state.
In a Visual Basic control, the drawing happens during the UserControl's Paint event. The Dice control simply loads the appropriate bitmaps into the picture controls on the form, as shown in Figure 4. Here's one place where Visual Basic has the advantage over the C++ frameworks in terms of reducing complexity. Showing bitmaps within the Visual C++ frameworks involves creating a memory device context (using CreateCompatibleDC), loading the bitmaps, selecting the bitmap into the device context, then BITBLTing the bitmap into the real device context. The picture control does all that for you within Visual Basic. In addition to rendering bitmaps on the screen, you can render many other things using the standard Visual Basic graphics API, including lines, shapes, and text.
In MFC you develop an incoming dispatch interface via ClassWizard. In MFC your control class carries around a Dispatch map that maps incoming DISPIDs to internal member functions. In ATL, ClassView in Visual C++ lets you add member functions to the interfaces attached to your class. In Visual Basic, all you have to do is make your methods public and they'll show up as incoming methods to the client.
For example, the Dice control exposes a function named RollDice that simply starts the timer going. RollDice is public, so it'll appear in the object's incoming interface.
As you add methods (and properties), Visual Basic concocts a dual incoming interface.
Public Sub RollDice()
Rem start the timer
DiceTimer.Enabled = True
In addition to incoming methods, most controls include a set of properties. For example, the Dice control properties include the color of the dice, how many times to roll the dice, and whether to sound the speaker as the dice roll. In MFC, you add properties via the ClassWizard. As with methods, the ClassWizard adds members to your C++ class and updates the dispatch map. In ATL, you add the data members to your C++ class, then expose them via methods in your interface. In Visual Basic, you add data members to your UserControl in the top of the file, then expose the properties as public properties. Figure 5 shows the code for declaring the data members and exposing them as public properties.
When your control is loaded, clients have the ability to change the properties by accessing them through the control's main dispatch interface. However, it's often very useful to set up some sort of user interface so developers using your control can access the properties easily at design time. This access is usually obtained through property pages.
Figure 6 shows an ActiveX control's property page being used from within Visual C++. Most clients (like Visual StudioÂ® itself) use property pages in this way. They first ask the control for a list of property page GUIDs through the ISpecifyPropertyPages interface. Then the client calls CoCreateInstance on each property page and installs each in its own dialog frame. That dialog frame gets a copy of the control's main incoming interface for talking to the control (so the dialog frame is able to change the properties within the control).
|Figure 6 Property Page in Visual C++ |
When you create a control in MFC, the ControlWizard automatically gives you a property page and you can use ClassWizard to add more property pages. When you code up the property page dialog box with ClassWizard, ClassWizard adds the code to transfer data from the property page controls into the ActiveX control itself. The ATL ObjectWizard includes a wizard for creating property pages. However, with ATL, you need to write some Petzold-style SDK code to move the data from the property page controls, and then you need to write the code to put the properties into the ActiveX control.
After you've exposed all your properties as shown in Figure 5, the easiest way to create a property page is to use the property page Wizard in Visual Basic. The Wizard reads the type information about your properties and writes the property page dialog box, complete with code for moving data between the dialog box controls and the ActiveX control itself.
Once the client of the control has all the properties set up correctly through the property page, it would be a pity to lose all that configuration information. For that reason, ActiveX controls support property persistence though a variety of interfaces. As a general rule, COM persistence works through pairs of interfaces. Clients acquire some medium like a stream, a storage, or a property bag. These media are represented by the standard IStream, IStorage, and IPropertyBag interfaces. IStream represents a binary stream; IStorage represents structured storage; and IPropertyBag represents name-value pairs. These interfaces usually embody functions to read and write data to and from the real media. Then while establishing a connection with the control, the client asks for the corresponding persistence interface from the control. For example, objects supporting persistence to a stream implement IPersistStreamInit. Objects supporting persistence to storage implement IPersistStorage, while objects supporting persistence to property bags implement IPersistPropertyBag.
MFC supports control property persistence through a single virtual function in your control's C++ classâ"DoPropExchange. In MFC you simply override this DoPropExchange and move your properties to and from the media via a helper class named CPropExchange. MFC takes care of details such as the direction of the data.
The ATL persistence mechanism uses a property map. Just put the properties you want to be saved and loaded in the map. Then make sure to include the persistent interfaces you want to support in your inheritance list and the COM map (QueryInterface support in ATL). For example, ATL includes the classes IPersistStreamImpl, IPersistStorageImpl, and IPersistPropertyBagImpl, which take care of implementing the persistence interfaces.
In Visual Basic, you simply write your UserControl's ReadProperties and WriteProperties handlers:
This code handles property persistence regardless of the medium supplied by the client. That is, calls to IPersistPropertyBag::Load, IPersistStreamInit::Load, and IPersistStorage::Load are all mapped to the ReadProperties handler. All calls made to IPersistPropertyBag::Save, IPersistStreamInit::Save, and IPersistStorage::Save are mapped to the UserControl's WriteProperties handler.
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
' Read the properties out of a property bag
bSound = PropBag.ReadProperty("Sound", True)
strDiceColor = PropBag.ReadProperty("DiceColor", "Blue")
nTimesToRoll = PropBag.ReadProperty("TimesToRoll", 15)
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
' Write the properties to a property bag
PropBag.WriteProperty "Sound", bSound, True
PropBag.WriteProperty "DiceColor", strDiceColor, "Blue"
PropBag.WriteProperty "TimesToRoll", nTimesToRoll, 15
In addition to supporting incoming methods and properties, ActiveX controls also support ambient properties. Ambient properties are owned by the client and shared with the object. For example, imagine you're writing a control and you want the control's background color to be the same as the client's background color. The background color is known as an ambient property.
MFC and ATL support ambient properties through member variables and member notification functions in their control classes (COleControl in MFC, and CComControl in ATL). The Visual Basic UserControl has a handler named AmbientChanged that's called every time a container's ambient property is changed. Visual Basic will tell you which property changed and you can then update your control accordingly.
ActiveX controls also support events. Events are notifications back to the client that something interesting has happened. The control expects the client to implement the event functions. For example, the Dice control fires events after the dice have been rolled, and whenever doubles or snake eyes are rolled.
In MFC, you define the default event set through the ClassWizard. For example, if you add an event named DiceRolled, ClassWizard adds a member function named FireDiceRolled to your control class. FireDiceRolled acts as a proxy, calling back to the client event handlers.
In ATL, you define the event interfaces within the project's IDL, build the type library, and then ask the ClassView to write a proxy for you. For example, once you describe the DiceRolled function in IDL and build the type library, ClassView will build a proxy class for you that implements the connection point and includes a member function named Fire_DiceRolled for calling back to the client. ClassView takes this proxy class and attaches it to your control class via inheritance. Then you can just call the function Fire_DiceRolled whenever you want to tell the client that the dice have finished rolling.
Visual Basic also has a syntax for declaring events. Here's how to define the RollDice, Doubles, and Snake Eyes events using Visual Basic syntax:
Whenever you want to tell the client that the dice have been rolled, just call the appropriate event function. Figure 7 shows how to implement the Dice control's timer to fire an event after the dice have finished rolling.
' Events for the client to implement
Public Event Doubles(ByVal Value As Long)
Public Event SnakeEyes()
Public Event DiceRolled(ByVal FirstDie As Long,
ByVal SecondDie As Long)
While MFC, ATL, and Visual Basic all represent viable ActiveX control frameworks, they each have different implementation considerations. In general, Visual Basic remains an effective tool for creating controls with the advantage of hiding complexity at the cost of losing some flexibility. MFC and ATL are a bit more flexible because you're coding effectively at the SDK level, however you're not insulated from the intricacies of C++.