Building ActiveX Controls for Microsoft Windows CE Using the Active Template Library

Windows CE 2.1

Shyam Pather
Microsoft Corporation

September 1999

Summary: An introductory guide to creating ActiveX controls for Microsoft Windows CE using the Active Template Library (ATL). (51 printed pages)

Contents

Introduction
Part 1: The Active Template Library
   ATL Overview
   Example: A Simple COM Object Using ATL
   Summary of Part 1
Part 2: Using ATL to Write ActiveX Controls
   ActiveX Control Concepts
Suggestions for Further Reading
Conclusion
References

Introduction

Handheld devices running Microsoft's Windows CE operating system are becoming an essential part of the high-tech lifestyle. Because these devices are still relatively new, there are few applications targeted to them, leaving the market wide open for new developers.

The design of Handheld PCs imposes constraints on application developers: These devices are designed to complement, rather than compete with, desktop PCs, and therefore typically offer much less memory and processing power than do their desk-bound counterparts. This means that applications for handheld devices need to be small and efficient.

Why Write ActiveX Controls for Windows CE?

Microsoft Visual Basic®, with its intuitive graphical model and rapid development cycle, is an ideal language for developing Handheld PC applications. Visual Basic works best for small to medium sized applications, precisely the sort of application most suited to handheld devices.

One drawback to using Visual Basic is the limited number of controls (UI gadgets and objects representing system services) available to developers. But with the advent of COM, OLE, and ActiveX, Microsoft has essentially made Visual Basic extensible. One can now author complete new controls that Visual Basic applications (as well as applications written in other languages) can use. By writing ActiveX controls, developers can lend to Visual Basic all the power and flexibility typically available only in lower-level languages such as C++. That is the focus of this article: How to write re-usable ActiveX controls that can be plugged into Visual Basic applications to implement features not otherwise available.

Topics Covered in This Article

The pillar on which ActiveX technologies rest is COM, the Component Object Model. This article does not cover COM fundamentals; other books and papers do a much better job of this than I could in the short space available here. If you are not already familiar with COM, please see the section "Suggestions for Further Reading" at the end of this article for book titles that will be helpful to you.

Windows CE ActiveX controls must be written in Visual C++®, though they can be used in Visual Basic applications when completed. A good grounding in C++ is definitely a prerequisite for reading this article because the examples make heavy use of some advanced features of C++, such as multiple inheritance and templates.

Visual C++ offers several "Wizards" that greatly reduce the work required to put together a working ActiveX control. I will illustrate the use of these wizards in this article's examples. The wizards generate code based either on Microsoft Foundation Classes (MFC) or Active Template Library (ATL). We will be using the ATL versions exclusively (this decision will be discussed later).

Part 1 of this article will cover some ATL basics. Since this article is meant as an introductory guide, not an exhaustive reference, I will concentrate on how to use ATL, rather than on the details of what ATL does behind the scenes. The example at the end of Part 1 will show how to write a simple COM component, but it will not be a full-fledged ActiveX control.

Part 2 will discuss the principles of ActiveX controls, and then walk through an example that will culminate in a working control that we can use in Visual Basic applications. Finally, I will mention some books that discuss the more advanced COM, ActiveX and ATL concepts that are beyond the scope of this article.

What You Will Need

To build the example objects and controls in this article, you will need Microsoft Visual C++ version 6.0 or later (the screenshots used in this article were taken from version 6.0) with the Microsoft Windows CE Toolkit for Visual C++ 6.0 installed. To build the sample clients, you will need Visual Basic version 6.0 or later, with the Microsoft Windows CE Toolkit for Visual Basic 6.0. A Windows CE-based device is not required, since the examples will run on the emulators provided with the Windows CE toolkits. I recommend making frequent use of the SDK reference documentation supplied with the toolkits. In this article, I will describe what certain interfaces and methods do and how to use them, but the SDK reference is the place to look for their exact definitions.

Part 1: The Active Template Library

The Active Template Library (ATL) is a collection of template classes (sometimes known as parameterized classes). These are classes that will never be instantiated directly, but rather, as the name suggests, serve as templates for classes that will be directly instantiated. Template classes are defined in terms of one or more parameters that are given specific values (or specialized) at compile time.

C++ Templates Refresher

Consider the following trivial class that implements an array of integers (this class is over-simplified for illustration purposes and does not contain features, such as error checking, that one would expect in a real-world class):

class CMyTrivialArray {
public:
   int get_element(int index) {
      return m_array[index];
   }

   void put_element(int index, int value) {
      m_array[index] = value;
   }

private:
   int   m_array[10];
};

This class is tightly tied to the int data type. It could not be used to represent an array of floats, for example. If we wanted an array of floats, we could implement another class, declared exactly like CMyTrivialArray but with every occurrence of int changed to float. That approach would become unwieldy if we needed arrays for several different data types, since each would require its own class. A better approach would be to define the array class as a template class, as shown below:

template <class T>
class CMyTrivialArray {
public:
   T get_element(int index) {
      return m_array[index];
   }

   void put_element(int index, T value) {
      m_array[index] = value;
   }

private:
   T m_array[10];
};

Notice the template parameter (class T) before the class declaration. This tells the compiler that the class will be written in terms of a generic type T that will be specified at compile time. To create an array of integers, a program could use the following declaration:

CMyTrivialArray<int> myIntArray;

The data type, int, specified for the class T parameter, tells the compiler to create an instance of the class with all occurrences of T in the class declaration replaced with int. This results in an object that represents an array of integers, equivalent to an instance of the original CMyTrivialArray class. The difference is that, unlike the original class, this CMyTrivialArray is specified as template, meaning that we can create an array of floats just as easily:

CMyTrivialArray<float> myFloatArray;

The Active Template Library uses templates as simple as this one, as well as more complicated forms.

ATL Overview

The classes in the Active Template Library can be broadly divided into three categories: classes that implement COM functionality (for example, component registration, QueryInterface(), and class factories), classes that act as thin wrappers to the Win32® windowing API, and utility classes (for example, smart pointers).

Implementing COM components involves a lot of drudgework just to get off the ground. Among other things, one must implement code to register COM objects in the system registry, provide class factories for these objects, and implement the IUnknown interface, before getting to the interesting parts. The ATL classes in the first category described above are designed to do most of this drudgework for you. These classes (and associated macros) implement component registration, the three IUnknown methods (QueryInterface(), AddRef(), and Release()), as well as class factories. As a COM developer using ATL, you simply have to derive your classes from these ATL classes in order to use the functionality they implement.

The large number of classes in the ATL, and the long, confusing lists of template parameters that some of these take, can make deciding which ones to use when developing a new COM class difficult. Fortunately, the Visual C++ ATL COM AppWizard saves you from having to pick and choose from the ATL classes yourself. A few simple clicks through the wizard dialogs lets you select the options you want, and the wizard then generates the code for your class, which will inherit from the right ATL classes.

The classes in the second ATL category simplify the GUI development by wrapping calls to the underlying Win32 windowing API. This is similar to the way that MFC aids GUI development. Since this article focuses on ActiveX control development, I will not devote attention to the windowing classes (the examples do not use them).

The ATL utility classes (the third category alluded to above) exist simply to make things easier for COM developers. For example, the smart pointer classes are used to represent pointers to COM interfaces. These classes add value over direct C++ pointers since they manage reference counting automatically. That is, they increment reference counts on interface pointers when they are created, and decrement them when they are deleted or go out of scope. In addition, ATL provides wrapper classes for VARIANTs that overload the assignment operator to perform automatic conversion between common data types.

Since ActiveX controls are basically just COM objects that implement a specific set of interfaces, it makes sense to use ATL to develop them. Instead of spending time on the COM drudgework, we can concentrate on the more interesting parts of control development. We could also have used MFC to develop ActiveX controls, but since our goal is to write controls that run on small handheld devices, there are two advantages of ATL that we cannot ignore. First, ATL is small. Controls written with ATL do not require the presence of large run-time DLLs as MFC-based controls do. This is important on handheld devices with limited storage. Also, ATL is efficient. It makes use of compiler optimizations wherever possible and thus results in tight code, well suited to handheld devices featuring small memories and less powerful processors than desktop PCs.

Example: A Simple COM Object Using ATL

I believe the best way to learn ATL is to look at example code. So, in this section, we will use the Visual C++ AppWizard and Object Wizard to create a simple COM object using ATL. We will examine the code generated by the wizard and I will explain the ATL concepts as we encounter them. Our COM object will be called SimpleCalculator and, as its name suggests, will be capable of performing simple calculations. It will support two interfaces, IArithmetic, whose methods will do simple arithmetic operations, and IGeometry, an interface with methods to perform simple geometric calculations. Figure 1, below, illustrates the component and its interfaces.

Figure 1. The SimpleCalculator component and its interfaces

Creating the Project with the AppWizard

To begin, start Visual C++ and create a new project by selecting New... from the File menu. This should open a dialog like the one shown in Figure 2, allowing you to select the type of project to create. If you don't see the Windows CE projects listed (they all begin with "WCE"), you probably have not installed the Windows CE Toolkit.

Selecting the project type

Select WCE ATL COM AppWizard as the project type, enter "SimpleCalculator" as the project name, and check only the Win32 (WCE x86em) item in the Platforms list box. This will cause the AppWizard to generate a project that can be built to run on the x86 Windows CE Emulators that are supplied with the Windows CE Toolkit. Emulators are supplied for three types of devices, but we will run this article's examples on the Handheld PC Emulator.

Figure 2. Visual C++ new project dialog

Clicking OK will lead you to the AppWizard, which has only one page. Leave the Support MFC checkbox unchecked (since we will not need MFC support) and click Finish. This will bring up a dialog containing the information about the created project. Click OK to dismiss this dialog.

Examining the generated code

The AppWizard will have generated several files that are listed under the FileView tab in the Visual C++ integrated development environment (IDE). These files contain enough code to build the DLL that will contain our component. In fact, if you click on the Build icon on the toolbar, the files will be compiled and linked, generating the target DLL. By default, after building, the DLL will be downloaded to the target Windows CE device, which is the emulator in this case. Visual C++ will start the emulator, establish a connection with it, download the DLL and register its components in the emulator's registry.

At this point, the DLL does not contain any components. The only functions that exist are the global DLL entry points that all COM servers are required to export. These can be found in the file SimpleCalculator.cpp.

This file contains the declaration of a global variable named _Module, of type CComModule. CComModule is an ATL class (one of the few that is not a template class) that implements the functions of a COM server. The _Module object is initialized in the DllMain() function with the name of the server's object map. The object map is an ATL construct, declared by means of the ATL macros BEGIN_OBJECT_MAP() and END_OBJECT_MAP(). It serves as a list of the objects contained in the server and the _Module object uses it to register and unregister objects and to create class factories. Since we have not added any objects to our project yet, the object map in SimpleCalculator.cpp is empty. We will revisit the object map as we add objects to our project.

The other wizard-generated file of interest is SimpleCalculator.idl. The code in this file is written in the Interface Definition Language (IDL). IDL is used to describe COM interfaces and type libraries. In this article, I will not cover IDL syntax; if you are unfamiliar with IDL, consult the references at the end of this article and the IDL documentation on MSDN. Alternatively, I've found that the best way to learn IDL is by looking at the IDL code for existing interfaces. There is a lot of IDL code supplied with Visual C++ that makes good fodder for learning. If you look at SimpleCalculator.idl, you'll notice that it is empty, except for the declaration of our project's type library.

Using the ATL Object Wizard

The next step is to actually create our component by inserting a new object into the project. To do this, go to the Insert menu and choose New ATL Object.... This brings up the first page of the ATL Object Wizard. The wizard can generate several kinds of objects, but ours will just be a Simple Object. In the Category pane, make sure Objects is selected, and then choose Simple Object from the Objects pane. Clicking Next brings up the page in which we can name the object. Enter "SimpleCalc" in the Short Name entry field, and the wizard should fill in class, file, interface, and type library the names for you. The dialog should look like Figure 3, below.

Figure 3. The Names tab in the Object Wizard

The Attributes tab in this dialog allows you to specify other characteristics of the object, but for now we will leave those with their default values. Click the OK button to finish the wizard.

Examining the Wizard-generated files

The Object Wizard will have generated several new files. Under the ClassView tab in the IDE, we can see that a new class, CSimpleCalc, has been added. This is the class that will implement our component, and its default interface, ISimpleCalc, is also listed in ClassView. Examining the file SimpleCalculator.cpp now shows that an entry for our new object has been made in the object map. Also, the IDL file now contains the definition for the IsimpleCalc, a default interface generated by the wizard. This interface has no methods or properties (because we have not yet specified any). Instead of adding properties and methods to this default interface, we will add two new interfaces, IArithmetic and IGeometry. This exercise will best illustrate the mechanics of how ATL deals with interfaces.

The CSimpleCalc class is declared in the file SimpleCalc.h and implemented in SimpleCalc.cpp. The wizard-generated code from SimpleCalc.h is shown below:

// SimpleCalc.h : Declaration of the CSimpleCalc

#ifndef __SIMPLECALC_H_
#define __SIMPLECALC_H_

#include "resource.h"       // main symbols

/////////////////////////////////////////////////////////////////////////////
// CSimpleCalc
class ATL_NO_VTABLE CSimpleCalc : 
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CSimpleCalc, &CLSID_SimpleCalc>,
   public IDispatchImpl<ISimpleCalc, &IID_ISimpleCalc, &LIBID_SIMPLECALCULATORLib>
{
public:
   CSimpleCalc()
   {
   }

DECLARE_REGISTRY_RESOURCEID(IDR_SIMPLECALC)

DECLARE_PROTECT_FINAL_CONSTRUCT()

BEGIN_COM_MAP(CSimpleCalc)
   COM_INTERFACE_ENTRY(ISimpleCalc)
   COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()

// ISimpleCalc
public:
};

#endif //__SIMPLECALC_H_

CSimpleCalc uses C++ multiple inheritance to derive from three base classes supplied by ATL. From each of these classes it inherits functions that implement certain COM features. I will describe each of these base classes, as well as the other macros that the wizard has inserted. This is intended as background material and I have not delved into the exact implementation details. If you are interested, the references cited at the end of this article do a more than adequate job of digging into the finer points. I have chosen to skip these here because this is the code that does the drudgework for you: for most COM objects that you write, you will never have to worry about it.

The first class from which our class inherits is CComObjectRootEx, a template class that takes an ATL class representing the object's threading model as its template parameter. This class provides the internal implementation of the IUnknown methods, AddRef() and Release(), as well as functions to lock and unlock the object (used to synchronize access to the object by multiple threads). The lock and unlock functions are dependent on the threading model for the object (passed in as a template parameter to CComObjectRootEx) and in the case of the single thread model our class uses, these functions simply do nothing.

The CComCoClass base class from which CSimpleCalc inherits provides it with the necessary machinery to create a class factory as well as to create instances. The template parameters are used to create typedefs within the class to identify two other classes: one that will create the class factory, and one that represents the class factory itself. The CComModule class that we saw in SimpleCalculator.cpp earlier uses a static member function (a member function that can be called before any instances of the class exist) of the first class to create the class factory for our object. The second typedef is used to implement the CreateInstance() static member function, used to create instances of our class directly, without going through the class factory. CComCoClass also provides the implementation necessary for our component to support error objects, a topic beyond the scope of this article.

Our class also inherits from IDispatchImpl, a class that implements the standard IDispatch automation interface for our object. We will discuss this interface and this base class in Part 2 of this article.

The macro Declare_Registry_ResourceID(IDR_SIMPLECALC) in our class header expands to declare a static member function called UpdateRegistry() which calls the UpdateRegistryFromResource() method of _Module, passing it the resource ID, IDR_SIMPLECALC, as an argument. This resource ID identifies a resource in our project that refers to SimpleCalc.rgs file generated for us by the Object Wizard. This file is a registry script that registers and unregisters our component in the system registry. When the UpdateRegistry() method in our class is called, this registry script is run and our component is registered.

The object map

Having taken a brief look at the classes from which our CSimpleCalc class inherits, we can look more closely at the object map in SimpleCalculator.cpp. The Begin_Object_Map() and End_Object_Map() macros effectively declare an array of structures. Each Object_Entry macro expands to add an entry to this array. This entry contains pointers to static member functions of our class that the _Module object will call to perform various operations. These static member functions are provided by the ATL base classes from which we derived our class. For example, one of the pointers points to the UpdateRegistry method in our class. If we had other objects in our project, the Object_Entry() macros for the implementation classes of those objects would contain pointers to functions in those classes, including their UpdateRegistry functions.

As an example of how the object map is used, consider the implementation of DllRegisterServer in our DLL. The purpose of this function is to register each of the components in the DLL in the system registry. To do this, the _Module object would simply have to walk through the object map, find each entry's function pointer for updating the registry and call it.

The COM map

The other "map" that appears in the wizard-generated code is the COM map that appears in the declaration of our SimpleCalc class. This map simply lists the interfaces that our component supports and is used by the implementation of QueryInterface(). You might have noticed that I did not mention the implementation of QueryInterface() in the discussion of any of the base classes in this section. This is because QueryInterface() is not implemented in any of the ATL base classes from which our class inherits. Rather, QueryInterface() is implemented by the CComObject class, from which CSimpleCalc does not inherit. The expansion of the End_COM_Map() macro results in QueryInterface(), AddRef(), and Release() being declared as pure virtual member functions in CSimpleCalc. The presence of these pure virtual member functions makes CSimpleCalc an abstract base class that cannot be instantiated directly.

Instead, to create SimpleCalc objects, class factories instantiate CComObject<CSimpleCalc>. The CComObject class is declared as follows (this code is taken from the ATL header file ATLCOM.H, supplied with Visual C++):

template <class Base>
class CComObject : public Base
{
   ...
   ...
};

This causes CComObject to inherit from the class passed in as its template parameter. Therefore, instantiating CComObject<CSimpleCalc> results in an object of class CComObject that is derived from CSimpleCalc. The resulting object has all the methods implemented in CSimpleCalc, plus QueryInterface, AddRef(), and Release() which are implemented in CComObject. Thus, it is a complete COM object.

Adding Interfaces

Since the Object Wizard has created the base COM functionality, our task is to implement the interesting parts of our component. We can begin by adding the IArithmetic interface.

Defining the interface

First, we have to describe the interface using IDL. Edit the project IDL file (SimpleCalculator.idl) so that it contains the definition of IArithmetic as shown below.

// SimpleCalculator.idl : IDL source for SimpleCalculator.dll
//

// This file will be processed by the MIDL tool to
// produce the type library (SimpleCalculator.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";
   [
      object,
      uuid(BB91F2CB-11AE-11D3-A01B-00C04F682859),
      dual,
      helpstring("ISimpleCalc Interface"),
      pointer_default(unique)
   ]
   interface ISimpleCalc : IDispatch
   {
   };

   [
      object,
      uuid(EE7EFCCB-11C4-11D3-A01B-00C04F682859),
      helpstring("IArithmetic Interface"),
      pointer_default(unique)
   ]
   interface IArithmetic : IUnknown
   {   
      [id(1), helpstring("method Add")] 
      HRESULT Add([in] int X, [in] int Y, 
         [out, retval] int *ans);
      [id(2), helpstring("method Subtract")] 
      HRESULT Subtract([in] int X, [in] int Y, 
         [out, retval] int *ans);
   };

[
   uuid(FFF8DEB3-11A7-11D3-A01B-00C04F682859),
   version(1.0),
   helpstring("SimpleCalculator 1.0 Type Library")
]
library SIMPLECALCULATORLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   [
      uuid(E6BD3711-11AF-11D3-A01B-00C04F682859),
      helpstring("SimpleCalc Class")
   ]
   coclass SimpleCalc
   {
      [default] interface ISimpleCalc;
      interface IArithmetic;   
   };
};

The lines in bold are the ones that need to be added to the code generated by the wizard. I generated the GUID used to identify the new interface using the GUIDGEN.EXE utility that ships with Visual C++. This utility can be accessed through the Components and Controls dialog opened by selecting the Add to Project item on the Project menu.

Generating the interface

Building the project at this time results in the modified IDL being compiled and the new interface showing up in ClassView. The next step is to implement the interface in our class. Right-click on the CSimpleCalc class name in ClassView (make sure that you click on the CSimpleCalc class, not its constructor, CSimpleCalc()), and choose Implement Interface... from the context menu that appears. This results in an Implement Interface dialog that allows you to select the interface to implement. The list of interfaces shown is drawn from the project's type library, defined in the IDL file. IArithmetic should be the only interface that appears in this list: select it, and click OK.

Open the SimpleCalc.h file in which the CSimpleCalc class is declared; you will notice that this file has changed somewhat since we last saw it. Notice that it is now derived from IArithmetic, in addition to the other base classes we discussed earlier. Also, there is an entry for IArithmetic in the COM map, and the two IArithmetic methods are added to the class. We need to implement these, so change these functions so that they look like the code below.

// IArithmetic
   STDMETHOD(Add)(INT X, INT Y, INT * ans)
   {
      if (ans == NULL)
         return E_POINTER;
         
      *ans = X + Y;

      return S_OK;
   }

   STDMETHOD(Subtract)(INT X, INT Y, INT * ans)
   {
      if (ans == NULL)
         return E_POINTER;
      
      *ans = X - Y;

      return S_OK;
   }

The purpose of these functions is self-explanatory. Note, however, that these functions return HRESULTs indicating their success or failure status and each returns its answer by means of an out parameter (a pointer to a location in which to store the answer).

Adding an additional interface

At this point, it would be a good idea to build the component to make sure we have not made any mistakes. The only thing left to do is to add the second interface, IGeometry. To do this, edit the IDL file so that it looks like the code shown below. Again, the code marked in boldface is new and should be added to the existing code:

// SimpleCalculator.idl : IDL source for SimpleCalculator.dll
//

// This file will be processed by the MIDL tool to
// produce the type library (SimpleCalculator.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";
   [
      object,
      uuid(BB91F2CB-11AE-11D3-A01B-00C04F682859),
      dual,
      helpstring("ISimpleCalc Interface"),
      pointer_default(unique)
   ]
   interface ISimpleCalc : IDispatch
   {
   };

   [
      object,
      uuid(EE7EFCCB-11C4-11D3-A01B-00C04F682859),
      helpstring("IArithmetic Interface"),
      pointer_default(unique)
   ]
   interface IArithmetic : IUnknown
   {   
      [id(1), helpstring("method Add")] 
      HRESULT Add([in] int X, [in] int Y, 
         [out, retval] int *ans);
      [id(2), helpstring("method Subtract")] 
      HRESULT Subtract([in] int X, [in] int Y, 
         [out, retval] int *ans);
   };

   [
      object,
      uuid(44176A67-11CA-11D3-A01B-00C04F682859),
      helpstring("IGeometry Interface"),
      pointer_default(unique)
   ]
   interface IGeometry : IUnknown
   {   
      [id(1), helpstring("method Circumference")] 
      HRESULT Circumference([in] double radius, 
         [out, retval] double *ans);
      [id(2), helpstring("method Area")] 
      HRESULT Area([in] double radius, 
         [out, retval] double *ans);
   };

[
   uuid(FFF8DEB3-11A7-11D3-A01B-00C04F682859),
   version(1.0),
   helpstring("SimpleCalculator 1.0 Type Library")
]
library SIMPLECALCULATORLib
{
   importlib("stdole32.tlb");
   importlib("stdole2.tlb");

   [
      uuid(E6BD3711-11AF-11D3-A01B-00C04F682859),
      helpstring("SimpleCalc Class")
   ]
   coclass SimpleCalc
   {
      [default] interface ISimpleCalc;
      interface IArithmetic;
      interface IGeometry;
   };
};

Build the project again to compile the type library. This should cause the new interface, IGeometry, to show up in ClassView. Repeat the procedure used for IArithmetic, to implement IGeometry in CSimpleCalc, and edit the method implementations generated by the wizard to match the following code.

// IGeometry
   STDMETHOD(Circumference)(DOUBLE radius, DOUBLE * ans)
   {
      if (ans == NULL)
         return E_POINTER;
         
      // Circumference = 2 * pi * radius
      *ans = 2.0 * 3.14 * radius;
      return S_OK
   }

   STDMETHOD(Area)(DOUBLE radius, DOUBLE * ans)
   {
      if (ans == NULL)
         return E_POINTER;
      
      // Area = pi * (r^2)
      *ans = 3.14 * radius * radius;
      return S_OK;
   }

Registering the component

The SimpleCalculator component is now complete. After building it one final time, we can peek into the Windows CE registry on the emulated device to see that our component is registered (Visual C++ automatically registers the component on the target device after it is built).

To view the system registry of the target Windows CE device (the emulator, in our case), use the Windows CE Regedit tool supplied with the SDK. This should open the registry editor on the emulated device. Expand the HKCR branch (HKEY_CLASSES_ROOT) and you should see the registered classes, including our SimpleCalculator component, as shown in Figure 4.

Figure 4. Windows CE Registry showing registered components on the emulated device

Writing a Simple Test Client

Now that the component is complete, we can write a simple test client that puts it into action. The test client I will present here is very simple. It uses some rudimentary COM calls to create an instance of our component, query for the interfaces we defined, and use the methods on those interfaces. The code should be fairly self-explanatory.

Creating a new project

Create a new project (choose New... from the File menu, and click on the Projects tab). Choose WCE Application as the project type and enter the project name. For the project location, instead of choosing the default provided by the wizard, place the project in a subdirectory off the directory in which the SimpleCalculator project was located. This will make it easy to refer to the SimpleCalculator header files we just created by using relative paths. Again, leave Win32 (WCE x86em) as the only platform checked.

The next page in the wizard will allow you to choose the type of application. Select "A simple Windows CE application." This will cause the wizard to generate a skeleton application that will compile and run, but won't do anything (it will exit immediately).

Adding code to create and use the SimpleCalc object

Edit the file TestClient.cpp so that it looks like the listing below. Note the #include statements that pull in the files SimpleCalculator.h and SimpleCalculator_i.c. Both these files were generated by the IDL compiler and contain the definitions of our component's CLSID, as well as the IIDs of its interfaces.

// TestClient.cpp : Defines the entry point for the application.
//

#include "stdafx.h"

#include <atlbase.h>
#include <objbase.h>

#include "..\SimpleCalculator.h"
#include "..\SimpleCalculator_i.c"

int WINAPI WinMain(   HINSTANCE hInstance,
         HINSTANCE hPrevInstance,
         LPTSTR    lpCmdLine,
         int       nCmdShow)
{
   IUnknown *pUnk = NULL;
   IArithmetic *pArithmetic = NULL;
   IGeometry *pGeometry = NULL;
   HRESULT hr = S_OK;

   //
   // Initialize the COM library;
   //
   hr = CoInitializeEx(NULL, COINIT_MULTITHREADED);
   
   if (FAILED(hr)) {
      MessageBox(NULL, L"Failed to initialize COM library", 
               L"Error!", MB_ICONERROR);
      return -1;
   } 

   //
   // Create an instance of the SimpleCalculator object 
   // and grab an IUnknown interface.
   //
   hr = CoCreateInstance(
         CLSID_SimpleCalc, 
         NULL, 
         CLSCTX_INPROC_SERVER, 
         IID_IUnknown, 
         (void**)&pUnk);

   if (FAILED(hr)) {
      MessageBox(
         NULL, 
         L"Failed to create instance of " "SimpleCalculator", 
         L"Error!", 
         MB_ICONERROR);
      return -1;
   }

   //
   // Query the object for the IArithmetic interface.
   //
   hr = pUnk->QueryInterface(
      IID_IArithmetic, 
      (void **)&pArithmetic);

   pUnk->Release(); // Don't need this any more. 
   pUnk = NULL;

   if (FAILED(hr)) {
      MessageBox(
         NULL, 
         L"Failed to get IArithmetic interface", 
         L"Error!", 
         MB_ICONERROR);
      return -1;
   }

   //
   // Do some operations with the IArithmetic interface.
   //
   int ans1, ans2;

   hr = pArithmetic->Add(10, 5, &ans1);

   if (FAILED(hr)) {
      MessageBox(
         NULL, 
         L"Failed to execute Add() method correctly", 
         L"Error!", 
         MB_ICONERROR);
      pArithmetic->Release();
      return -1;
   }

   hr = pArithmetic->Subtract(9, 6, &ans2);

   if (FAILED(hr)) {
      MessageBox(
         NULL, 
         L"Failed to execute Subtract() "
         "method correctly", 
         L"Error!", 
         MB_ICONERROR);
      pArithmetic->Release();
      return -1;
   }

   WCHAR displayString[100];

   wsprintf(
      displayString, 
      L"10 + 5 == %d ... and ... 9 - 6 == %d", 
      ans1, 
      ans2);
   
   MessageBox(
      NULL, 
      displayString, 
      L"Results from IArithmetic Operations", 
      MB_OK);
   
   //
   // Query the object for the IGeometry interface.
   //
   
   hr = pArithmetic->QueryInterface(
         IID_IGeometry, 
         (void **)&pGeometry);

   pArithmetic->Release(); // Don't need this any more. 
   pArithmetic = NULL;

   if (FAILED(hr)) {
      MessageBox(
         NULL, 
         L"Failed to get IGeometry interface", 
         L"Error!", 
         MB_ICONERROR);
      return -1;
   }

   //
   // Do some operations with the IGeometry interface.
   //
   double circumference, area;

   hr = pGeometry->Circumference(5.0, &circumference);

   if (FAILED(hr)) {
      MessageBox(
         NULL, 
         L"Failed to execute Circumference() "
         "method correctly", 
         L"Error!", 
         MB_ICONERROR);
      pGeometry->Release();
      return -1;
   }

   hr = pGeometry->Area(5.0, &area);

   if (FAILED(hr)) {
      MessageBox(
         NULL, 
         L"Failed to execute Area() method correctly", 
         L"Error!", 
         MB_ICONERROR);
      pGeometry->Release();
      return -1;
   }

   // Have to use swprintf() since wsprintf() does 
// not do floats.

   swprintf(
      displayString, 
      L"Circle with radius 5.0 has circumference %.2f "
      "and area %.2f", 
      circumference, 
      area); 


   MessageBox(
      NULL, 
      displayString, 
      L"Results from IGeometry Operations", 
      MB_OK);

   pGeometry->Release();
   pGeometry = NULL;

   //
   // Uninitialize the COM library.
   //

   CoUninitialize();

   return 0;
}

This code first initializes the COM library, as all COM applications do, by calling CoInitializeEx(). It then creates an instance of the SimpleCalc object by calling CoCreateInstance() and passing it the CLSID of SimpleCalc (the CLSID_SimpleCalc variable is defined in the SimpleCalculator_i.c file that was generated by the IDL compiler and included in this source file). The CoCreateInstance() call returns a pointer to the IUnknown interface on the object. Using this interface pointer, the program does a QueryInterface() for the IArithmetic interface (the IID_IArithmetic variable is also defined in SimpleCalculator_i.c). Using this interface pointer, it exercises the IArithmetic methods and displays the results using a message box. Similarly, the program queries for IGeometry and exercises the methods on that interface. Finally, it calls CoUninitialize() before terminating.

Building this project will generate an executable called TestClient and will download it to the emulated device. You can run it by opening the My Handheld PC folder on the emulated device, and double clicking on the TestClient icon. If everything works correctly, this should produce two dialog boxes, one showing the results of the IArithmetic methods and the other showing the results of the IGeometry methods. While certainly not anything spectacular, this test client demonstrates that our component works as a proper COM object.

Summary of Part 1

In this first part of the article, I introduced some basic ATL concepts and illustrated how to use the Visual C++ wizards to generated skeleton code for ATL components. We developed a simple COM object using ATL and saw how to add interfaces on top of the wizard-generated code. The result was a full-fledged COM object that could be used in a COM application, as illustrated by the sample test client.

In Part 2, I will introduce ActiveX controls and show how to use ATL to implement them. ActiveX controls are nothing more than COM objects that implement a set of standard interfaces. ATL provides ready-to-use implementations of many of these interfaces, making control development a relatively painless task.

Part 2: Using ATL to Write ActiveX Controls

ActiveX controls are basically just COM objects that support a specific set of interfaces. Typically, they function as small user-interface "gadgets" that are placed within other containing windows. The windows that contain ActiveX controls are aptly referred to as "containers." ActiveX controls export properties and methods, which, through a special interface called IDispatch, are accessible to clients written in interpreted languages such as Visual Basic, or Web pages written in HTML. Thus, ActiveX controls can be incorporated into these types of clients as easily as standard Windows controls such as push buttons and list boxes.

Visual C++ and ATL make writing controls painless. The Object Wizard that we encountered in the previous article can generate skeleton code for a basic ATL ActiveX control. ATL takes care of much of the drudgework by supplying base classes that implement most of the standard interfaces that ActiveX controls need to support. This allows you to concentrate on implementing the parts specific to your control.

ActiveX Control Concepts

IDispatch and Dynamic Invocation

When a C++ client wishes to call a method on a particular interface, it first has to obtain a pointer to that interface (through QueryInterface() or some other function that returns an interface pointer). This pointer actually points to a piece of memory containing a virtual function table (or "vtable," as it is known in C++ terminology) containing the addresses of the various methods on the interface. The layout of this table is based on the layout of the interface in the header file in which it is declared.

When compiling a call to an interface method, the C++ compiler generates code that looks up the address of the method in the vtable, pushes the arguments to the method onto the stack and calls the method at the address found in the vtable. This requires prior knowledge of the vtable layout, which is provided by interface declaration (in some header file). This is known as "early binding" or "static invocation" since the method call is tied to a particular vtable layout at compile-time and cannot change at run time.

This technique works well for C++ clients, but not for clients written in interpreted languages such as Visual Basic. In interpreted languages, there are no header files that can provide the layout of an interface in advance. Without a way to determine its vtable layout at run time, interpreted clients cannot call the methods on an interface. The solution is to use a dispatch interface (or "dispinterface"). Simply put, a dispatch interface is one that inherits from the standard interface, IDispatch.

This standard interface contains methods that return information about the other methods on the interface, such as their names and the types of arguments they take, as well as a method used to invoke these other methods. Visual Basic, for example, can query an object for a dispinterface and use the IDispatch methods to learn about the other methods supported by the interface. It can then put together the arguments that will have to be passed to these methods, and invoke them through the IDispatch::Invoke() method. This is known as "dynamic invocation" or "late binding", since it occurs at run-time. Thankfully, ATL provides a generic implementation of IDispatch that frees us from having to implement the hairy details of invoking methods dynamically.

Controls, Containers, and Windows

ActiveX controls live within containers. A windowless control does not own its own window, but rather renders itself in the window owned by its container. In contrast, a windowed control does own its window, and an accompanying window procedure, but is still typically drawn as part of the container's window.

Windowless controls are smaller than their windowed counterparts and execute faster, since all the expensive window creation code is skipped. Windowless controls can also be transparent because they are simply drawn over the original contents of the container window. Unfortunately, certain Win32 API functions that require a window handle cannot be called by windowless controls. If a particular control needs to use any of these Win32 functions, it must be windowed. If none of these functions are needed, a windowless implementation is generally advised. Again, ATL takes care of the implementation details required to create a control as windowed or windowless.

Containers can talk to the controls they contain through the methods and properties exposed in the controls' dispinterfaces. However, it is sometimes also necessary for the control to talk back to the container. This is accomplished through the use of events. The control will expose a "connection point" representing its main event set. The container can then connect to this connection point, and the control will use this connection later on to notify the container of events that occur.

A control needs to export an interface called IConnectionPointContainer that describes the connection points it supports. Each connection point is essentially an interface that a connecting client needs to implement. To connect to a connection point, the client implements this interface, and passes a pointer to the interface back to the control. The control then invokes methods on this interface when it wants to communicate with the connected client. The connection interface is typically a dispinterface whose methods must be invoked by IDispatch::Invoke(). Implementing dynamic invocation is typically a tedious programming task, but the Visual C++ wizards can generate wrappers that do most of the hard work. This will be seen in the example that follows.

Control State and Persistence

Controls typically have state that can be altered by the container, as well as by external events. If the control is used in an application that allows the container state to be saved in some way, it is usually desirable for the state of the control to be saved (or made "persistent") also. Writing a control using ATL means that you don't have to do much to implement persistence for your control's properties. By using ATL property maps, this functionality can be automatically added to your control.

Property Pages

As mentioned earlier, controls can expose their properties through dispinterfaces, allowing them to be changed by interpreted code, such as that written in Visual Basic. However, there are times when it is useful to be able to adjust the properties of a control visually, rather than using code to do so. This is done through property pages supplied by the control. These pages are usually displayed by clients within a dialog, and present a visual interface to adjust the control's properties. The Visual C++ Object Wizard and ATL make it very easy to create property pages for controls. The example in the following section will create a simple property page that will illustrate how to do this.

Example: A Digital Clock Control

In this section, we'll walk through the creation of a real ActiveX control. The example will illustrate each of the facets of ActiveX controls just discussed. I have deliberately chosen a very simple control for this example so as not to confuse the general principles of control writing with specific implementation details.

Our control will be a simple digital clock. We'll add methods that can start and stop the clock, and properties that affect the way the clock displays itself. We will then write a simple test client in Visual Basic to show how easy it is to use our control.

Creating the project with the ATL COM AppWizard

As before, create a new "WCE ATL COM AppWizard" project in Visual C++ and call it "DigitalClock". The next page in the AppWizard presents a checkbox labeled Support MFC. We definitely do not want to use MFC (for the reasons discussed in Part 1), so we will leave this checkbox unchecked.

Finishing the wizard creates the code for the DLL entry points that looks much the same as the initial code in the SimpleCalculator project of the previous article. At this point, our project does not yet contain any objects.

The ATL Object Wizard

The next step is to use the Object Wizard to insert a new ActiveX control into the project. Go to the Insert menu and choose New ATL Object.... Instead of inserting a simple object, as we did the last time, select Controls in the left pane of the dialog that appears and then select Full Control in the right pane. The dialog should look like Figure 5.

Figure 5. The ATL Object Wizard dialog showing control objects

The next wizard page presents a dialog in which we can name the control. Enter "DigiClock" in the Short Name entry box, and let the wizard fill out the other names. Then go to the attributes tab and check the Support Connection Points checkbox (this will make our lives easier when we are ready to implement events). Finally, go to the Miscellaneous tab and check the Windowed Only checkbox (the reason for this will become apparent shortly), as shown in Figure 6. Click OK to complete the wizard.

Figure 6. Specifying miscellaneous properties in the ATL Object Wizard

The wizard will generate code for a class called CdigiClock, in which we will implement our control. The control has a default interface, IDigiClock, that does not contain any methods or properties at this time. Look at the file DigiClock.h, which contains the declaration of the CDigiClock class. Notice that the inheritance list is much longer than that of the CSimpleCalc class we used in the last example. This is because CDigiClock is not just a simple COM object as CSimpleCalc was; it is a full-fledged ActiveX control that has to support several standard interfaces.

CDigitalClock inherits and implements one or more of these required standard interfaces from each of these classes. The COM map lists the interfaces that our class supports; again, this is a much longer list than the one we saw in the case of a simple COM object.

I will not go through this inheritance list in great detail because most of these classes implement interfaces that essentially just hook up the "plumbing" required for an ActiveX control to start and communicate with its container. In most cases, you will not need to modify this. Consult the reference documentation cited at the end of this article for more information on these classes and the interfaces that they support.

Implementing the clock behavior

A good starting point would be to get the clock control to display the time. It will need some internal state variables in order to keep track of the current time: one variable each for the hour, minute, and second. We will add these as private data members (of type short) to the DigiClock class.

The OnDraw() method of the class gets called when the control needs to redraw itself, either when the window is first created, or when it comes into view after being hidden behind another window. The object wizard has implemented OnDraw() to display a default string. We will change it to display the time based on the three private state variables. The necessary modifications to DigiClock.h are shown below (for clarity's sake, I've cut out the parts of the wizard-generated code that are to remain the same, and marked the new code in boldface):

// DigiClock.h : Declaration of the CDigiClock

...


/////////////////////////////////////////////////////////////////////////////
// CDigiClock
class ATL_NO_VTABLE CDigiClock : 
   public CComObjectRootEx<CComSingleThreadModel>,
   public IDispatchImpl<IDigiClock, 
         &IID_IDigiClock, &LIBID_DIGITALCLOCKLib>,
   public CComControl<CDigiClock>,
   public IPersistStreamInitImpl<CDigiClock>,
   public IOleControlImpl<CDigiClock>,
   public IOleObjectImpl<CDigiClock>,
   public IOleInPlaceActiveObjectImpl<CDigiClock>,
   public IViewObjectExImpl<CDigiClock>,
   public IOleInPlaceObjectWindowlessImpl<CDigiClock>,
   public IConnectionPointContainerImpl<CDigiClock>,
   public IPersistStorageImpl<CDigiClock>,
   public ISpecifyPropertyPagesImpl<CDigiClock>,
   public IQuickActivateImpl<CDigiClock>,
   public IDataObjectImpl<CDigiClock>,
   public IProvideClassInfo2Impl<&CLSID_DigiClock,
         &DIID__IDigiClockEvents, &LIBID_DIGITALCLOCKLib>,
   public IPropertyNotifySinkCP<CDigiClock>,
   public CComCoClass<CDigiClock, &CLSID_DigiClock>
{
public:
   CDigiClock() : m_sHour(12), m_sMinute(0), m_sSecond(0)
   {
      m_bWindowOnly = TRUE;
   }
...

// IDigiClock
public:

   HRESULT OnDraw(ATL_DRAWINFO& di)
   {
      RECT& rc = *(RECT*)di.prcBounds;
      HBRUSH hBrush, hOldBrush;
      hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH);
      hOldBrush = (HBRUSH)SelectObject(di.hdcDraw, hBrush);
      Rectangle(
         di.hdcDraw, 
         rc.left, 
         rc.top, 
         rc.right, 
         rc.bottom);
      SelectObject(di.hdcDraw, hOldBrush);

      TCHAR       pszText[10];
   
      wsprintf(pszText, _T("%02d:%02d:%02d"),
             m_sHour, m_sMinute, m_sSecond);
      
      DrawText(
         di.hdcDraw, 
         pszText, 
         -1, 
         &rc, 
         DT_CENTER | DT_VCENTER | DT_SINGLELINE);

      return S_OK;
   }

private:
   // Internal state variables
   short   m_sHour, m_sMinute, m_sSecond;
};

#endif //__DIGICLOCK_H_

The constructor initializes the control's three internal state variables, and the OnDraw() method displays them in digital time format. The parameter passed to OnDraw() is an ATL structure that contains, among other things, a handle to the device context on which the control must draw itself. Our simple OnDraw() implementation simply displays a text string on the device context, but more elaborate controls can use any of the Windows CE GDI functions to render themselves as needed.

Since the control can now draw itself, we can put together a small test client to see the fruits of our labor. First, build the control, which should automatically register itself on the emulated device.

The next step is to use Visual Basic to construct a form that will serve as a container for our control. However, here we run into a small problem. The control we just built is registered on the emulated Handheld PC. The desktop PC on which we will design the form does not know anything about our control. Unfortunately, it's not possible to use the control's DLL to register it on the desktop PC directly because the DLL is compiled for the Handheld PC target, not for our desktop PC (running Windows 95, 98 or NT®). So we must build a separate version of our control to run on the desktop PC so that we can use Visual Basic to incorporate it into a test client.

Building a desktop version of the control

This is a clumsy process, but currently remains the only way to accomplish our goal. Fortunately, the guts of our control do not contain any Windows CE-specific code and therefore can be used to build a version for the desktop target without any changes.

To build the desktop version, fire up another instance of Visual C++ and create a new "ATL COM AppWizard" project (note that this is not a "WCE ATL COM AppWizard" project, because we are not targeting a Windows CE device this time). Give the project the same name as we did the last time ("DigitalClock") so that the files created have the same names, but edit the location so that Visual C++ does not try to put the project in the same location as the Windows CE version. (I've found it helpful to name the directory containing the new control "DesktopDigitalClock.") Figure 7 shows the New Project dialog used to create the desktop version of the control.

Figure 7. Creating a new project for the desktop version of the control

Accept the defaults on all the wizard screens that follow. Then add a Full Control to the project by going to the Insert menu and choosing New ATL Object... as we did before. Give the control the same name as before ("DigiClock"), and accept all the wizard defaults. This will generate skeleton source files very similar to the ones we started from when creating the Windows CE control. Do not build the desktop control at this point.

Now, using either the command prompt or the Windows Explorer, copy over all the .CPP, .H, .IDL, and .RGS files from the directory containing the Windows CE version of the control to the directory containing the desktop version, overwriting any existing files in the desktop directory (see Figure 8). This is why it was so important to give the project and the control the same names in both versions: The files end up with the same names, and therefore we can simply copy over all the source from the Window CE directory and not have to change any of the project settings.

Figure 8. Files to copy from the Windows CE version of the project to the desktop version

Make sure you do not copy the .DSP file from the Windows CE version to the desktop version, because this is where the information about the target type is stored, and we want this information to remain different between the two versions. Copying the .IDL and .RGS files over causes the control and its type library to be identified by the same GUIDs on the desktop as they are on the Windows CE Handheld device.

Now build the desktop version. This should cause the control to be registered on the desktop. As we make changes to the source of the Windows CE version, we will have to recopy the source files over to the desktop version's directory in order to keep the two versions matched.

Creating the Visual Basic client

Open Visual Basic and create a new Windows CE HPC Project (if you don't see any Windows CE projects listed, you probably have not installed the Windows CE Toolkit for Visual Basic). In the Project Properties dialog that appears, specify "ClockClient" as the project name and "\ClockClient.vb" in the Remote Path entry box. Make sure that Handheld PC Emulation is selected in the Run on Target combo-box. You should be presented with a blank form on which to place controls at this point.

First, we need to tell Visual Basic that the Digital Clock control we just registered is usable in Windows CE projects. To do this, go to the Windows CE menu and select Control Manager to open the Windows CE Control Manager. In the tree in the left pane of the Control Manager window, expand the branch labeled "H/PC Ver. 2.00" Select the Desktop Design Controls sub-branch to display the Windows CE controls available for design work on the desktop PC. From the Control menu, choose Add New Control... and find the DLL containing the desktop version of the clock control using the dialog that appears. This will mark the control as available for use in Handheld PC projects. The Control Manager display should look something like Figure 9 (the exact controls listed will vary depending on what you have installed on your system, but you should at least see our "DigiClock Class" listed).

Figure 9. The Control Manager listing available controls

Next, go back to the Visual Basic window and choose Components... from the Project menu. This will bring up a dialog allowing you to select the controls that will be used in this project. Find the check box labeled DigitalClock 1.0 Type Library and check it. Click OK to close this dialog. This will add a small icon representing our control to the Visual Basic controls toolbar. Click this icon, and then drag out a rectangular area on the form to insert an instance of our control. The form should look like Figure 10, showing our control and the effects of its rudimentary OnDraw() function.

Figure 10. A Visual Basic form featuring the digital clock control

Hit the Start button (or press F5) to start our application—this will download the code to the emulator and start the program. You should see a window on the emulated device that contains our control, as shown in Figure 11 below.

Figure 11. The test client running on the emulated Handheld PC

Making the clock tick

Currently, our control is rather uninteresting because it does not really do anything. In this section, we'll introduce some action by making the clock tick.

Our control has three internal state variables that it uses to store the time that it displays. We need to update the values of these state variables as time progresses, and change the display accordingly. We would want the clock to start ticking when the client tells it to do so. Therefore, we must expose a method in our control's default interface that the client can call to do this.

Switch back to Visual C++ (the instance containing the Windows CE version of the control), and find the IDigiClock interface (our control's default interface) in ClassView. Right-click on this interface and select Add Method... from the context menu that appears. The dialog that appears lets you specify a method that will be added to the IDL declaration of the interface. Enter "Start" for the method name, and leave the parameters blank because this method will not take any parameters.

If you examine the IDL file now, you will see that a method has been added to the IDigiClock interface. Also, a prototype for the method has been added to the CDigiClock class declaration, and the corresponding implementation (with an empty function body) has been added to DigiClock.cpp by the wizard. Edit this file and add the function body as follows:

STDMETHODIMP CDigiClock::Start()
{
   SetTimer(1, 1000);

   return S_OK;
}

This simply calls SetTimer(), a member of the ATL CWindow class from which our class is indirectly derived. SetTimer() wraps the Win32 SetTimer() API function, which sets a system timer and causes a WM_TIMER message to be delivered to our window after 1000 milliseconds (or 1 second). The SetTimer() function requires a window handle, and this is the reason we had to create our control as "Windowed Only" in the Object Wizard. If our control were windowless, it would not have been able to make use of system timers.

Having set the system timer, Windows CE will deliver a WM_TIMER message to our control's window after 1 second. Each time the control gets this message, it should increment the time stored in its state variables, redraw itself, and set the timer to fire again in another 1 second. To make this happen, we must define a message handler for the WM_TIMER message.

Responding to window messages

Make an entry in the control's message map (defined inside the class declaration in DigiCalc.h) so that the map looks as follows:

BEGIN_MSG_MAP(CDigiClock)
   CHAIN_MSG_MAP(CComControl<CDigiClock>)
   DEFAULT_REFLECTION_HANDLER()
   MESSAGE_HANDLER(WM_TIMER, OnTimer)
END_MSG_MAP()

The MESSAGE_HANDLER() entry in the message map essentially tells the ATL windowing classes to call the OnTimer() method when a WM_TIMER message arrives. The next step is to implement this method. Add the following inline code to the CDigiClock class declaration in DigiClock.h, just above the OnDraw() method:

LRESULT OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
   {
       m_sSecond++;

       if (m_sSecond > 59) {
          m_sSecond = 0;
         m_sMinute++;
      }
   
      if (m_sMinute > 59) {
         m_sMinute = 0;
         m_sHour++;
      }

      if (m_sHour > 23)
         m_sHour = 0;

      //
      // Set the timer to fire again.
      //
      SetTimer(1, 1000);

      //
      // Update the display.
      //
      FireViewChange();

      bHandled = TRUE;
      return 0;
   }

This code updates the state variable that stores seconds, and then adjusts the minute and hour variables accordingly. It also calls SetTimer() once more to cause the timer to fire again (and thus the OnTimer() method to be invoked again) after another second. After resetting the timer, this code calls FireViewChange(), a method implemented in the CComControl ATL class from which CDigiClock is derived. FireViewChange() informs the container that the control wishes to redraw itself.

Through a series of windows messages and handlers, our control's OnDraw() method, which displays the values in the state variables in time format, is invoked. The result of all this code is that the state variables are incremented once every second, and the control redraws itself to display the new time. This will give the appearance of a continually updating digital clock.

Before trying out our control's new functionality, we should add a method to stop the clock from ticking. To do this, add a method called Stop to the IDigiClock interface using ClassView as before, again specifying no parameters. Implement this method as follows:

STDMETHODIMP CDigiClock::Stop()
{
   KillTimer(1);

   return S_OK;
}

This method calls KillTimer(), which has the expected opposite effect of SetTimer(): It prevents a pending timer from firing.

Modifying the test client

Shut down the test client program in the emulated device if it is still running, and then rebuild the control. Before updating our test client to use the changes we just made to our control, we must update the desktop version of the control. To do this, simply recopy all the .IDL, .RGS, .H, and .CPP files from the directory containing the Windows CE version of the control to the directory containing the desktop version.

Save the Visual Basic project containing the test client and shut down Visual Basic before rebuilding the desktop version of the control. This is necessary because Visual Basic was using our control's DLL, which would prevent it from being overwritten when we linked the new version. Go back to Visual C++, rebuild the control, and then re-open the Visual Basic project containing the test client.

Expand the form containing the control, and add two buttons, one named StartButton and the other named StopButton, as shown in Figure 12.

Figure 12. Visual Basic test client form with Start and Stop buttons added

Adding event handlers

Set the Enabled property on the stop button to False. Then add the following code to handle the click events for the buttons:

Private Sub StartButton_Click()
    DigiClock1.Start
    StartButton.Enabled = False
    StopButton.Enabled = True
End Sub

Private Sub StopButton_Click()
    DigiClock1.Stop
    StartButton.Enabled = True
    StopButton.Enabled = False
End Sub

Note how this Visual Basic code can call the start and stop methods on our control even though these methods were implemented in C++. This is IDispatch at work. Our control's default interface, IDigiClock, inherits from IDispatch and that the class CDigiClock inherits from the ATL class IDispatchImpl, which implements the IDispatch methods. These allow Visual Basic to get descriptions of the methods and properties on the IDigiClock interface, and then call those methods through IDispatch::Invoke().

If you run this client and click on the Start button, the clock will start running. As expected, clicking the Stop button will stop the clock.

Adding more functionality

Currently, our control behaves more like a stopwatch than a clock. To rectify this, we can add another method that sets the state of the clock to match the current system time. Thereafter, starting the clock should keep it in time with the system clock.

Go back to the instance of Visual C++ in which we are developing the Windows CE version of the control and find the IDigiClock interface in ClassView. Right-click it and add another method called SynchToSystemTime, again taking no parameters. Fill out the code for this method as follows:

STDMETHODIMP CDigiClock::SynchToSystemTime()
{
    //
    // Initialize the state variables from the 
    // current system time.
    //
    SYSTEMTIME  sysTime;

    GetSystemTime(&sysTime);

    m_sHour = sysTime.wHour;
    m_sMinute = sysTime.wMinute;
    m_sSecond = sysTime.wSecond;

    return S_OK;
}

Before trying out this change, we should add a property to the control that affects the way it is displayed. Currently, the control displays time only in 24-hour format. However, an application that uses our control might want to display time in 12-hour format, using "AM" and "PM" to distinguish morning from afternoon. To accomplish this, we should add another state variable to hold the display format, and expose this format to clients through a property.

The state variable that holds the display format can have one of only two possible values, 12-hour or 24-hour. Therefore, we should create an enumerated type for this variable. To do this, edit the IDL file and add the code shown in bold below (again, for the sake of clarity I have left out parts of this file that are to remain unchanged). Since IDL does not allow anonymous enumerations, a name is given after the enum keyword (this differs slightly from the C++ syntax for declaring enumerations).

// DigitalClock.idl : IDL source for DigitalClock.dll
//

// This file will be processed by the MIDL tool to
// produce the type library (DigitalClock.tlb) and 
// marshalling code.

import "oaidl.idl";
import "ocidl.idl";
#include "olectl.h"
   
   typedef enum TimeFormat {
      TIMEFORMAT_12HOUR = 0,
      TIMEFORMAT_24HOUR = 1
   } TimeFormat;

   [
      object,
      uuid(09BCFD69-1314-11D3-A01B-00C04F682859),
      dual,
      helpstring("IDigiClock Interface"),
      pointer_default(unique)
   ]
   interface IDigiClock : IDispatch
   {
      [id(1), helpstring("method Start")] HRESULT Start();
      [id(2), helpstring("method Stop")] HRESULT Stop();
      [id(3), helpstring("method SynchToSystemTime")] 
            HRESULT SynchToSystemTime();
   };

   ...

We can then go to the CDigiClock class declaration and add the necessary state variable, of type TimeFormat, to the private: section.

private:
   // Internal state variables
   short   m_sHour, m_sMinute, m_sSecond;
   TimeFormat   m_TimeFormat;

In order for clients to set this property, it must be exposed by the control's default interface, IDigiClock. Right-click on IDigiClock in ClassView, and select Add Property.... In the Add Property to Interface dialog that appears, enter "TimeFormat" in the Property Type field, and enter "TimeDisplayFormat" as the property name. Leave the Parameters field blank, and click OK. This will add the property to the IDL description of our interface, and add methods to CDigiClock to get and set the value of this property. Implement these methods in DigiClock.cpp as follows:

STDMETHODIMP CDigiClock::get_TimeDisplayFormat(TimeFormat *pVal)
{
   if (pVal == NULL)
      return E_POINTER;

   *pVal = m_TimeFormat;

   return S_OK;
}

STDMETHODIMP CDigiClock::put_TimeDisplayFormat(TimeFormat newVal)
{
   m_TimeFormat = newVal;

   FireViewChange();

   return S_OK;
}

The first method is called to get the value of the property, so we simply return the value of our state variable using the pointer that is passed. The second method sets the value of the internal state variable, and calls FireViewChange() to cause the display to get updated accordingly. Also, the state variable should be initialized when the object is created, so edit the constructor as follows:

   CDigiClock() : m_sHour(12), m_sMinute(0), m_sSecond(0), 
                     m_TimeFormat(TIMEFORMAT_24HOUR)
   {
      m_bWindowOnly = TRUE;
   }

We've now added the code to get and set the desired value for the display format, but we do not yet use this value when we draw the control. Modify OnDraw() as follows:

HRESULT OnDraw(ATL_DRAWINFO& di)
{
   RECT& rc = *(RECT*)di.prcBounds;
   HBRUSH hBrush, hOldBrush;
   hBrush = (HBRUSH)GetStockObject(WHITE_BRUSH);
   hOldBrush = (HBRUSH)SelectObject(di.hdcDraw, hBrush);
   Rectangle(
      di.hdcDraw, 
      rc.left, 
      rc.top, 
      rc.right, 
      rc.bottom);
   SelectObject(di.hdcDraw, hOldBrush);
      
   if (m_TimeFormat == TIMEFORMAT_24HOUR) {
      // Display in 24-hour format.
      TCHAR       pszText[10];
   
      wsprintf(
         pszText, 
         _T("%02d:%02d:%02d"),
         m_sHour, m_sMinute, m_sSecond);
      
      DrawText(
         di.hdcDraw, 
         pszText, 
         -1, 
         &rc, 
         DT_CENTER | DT_VCENTER | DT_SINGLELINE);
   } else {         
      // Display in 12-hour format.         
      TCHAR       pszText[13];
      short       sHour = m_sHour;

      if (sHour >= 12) {
         if (sHour != 12)
            sHour -= 12;

         wsprintf(
         pszText, 
         _T("%02d:%02d:%02d PM"),
         sHour, m_sMinute, m_sSecond);
      } else {
         if (sHour == 0)
            sHour = 12;

         wsprintf(
            pszText, 
            _T("%02d:%02d:%02d AM"),
            sHour, m_sMinute, m_sSecond);      
      }   
      
   DrawText(
      di.hdcDraw, 
      pszText, 
      -1, 
      &rc, 
      DT_CENTER | DT_VCENTER | DT_SINGLELINE);      
   }

   return S_OK;
}

Build the control once more, update the desktop version by copying over the .IDL, .RGS, .H, and .CPP files, rebuild the desktop version, and re-open the Visual Basic project containing our test client. Add a button labeled Synchronize to the form, and add code to the click handler for this button to call the SynchToSystemTime() method on the DigiClock1 object. Also, if you now select our control in the form in design mode, you should see the TimeDisplayFormat property listed amongst all the other standard properties. Setting this property causes the clock to display in either 12-hour or 24-hour format.

However, you'll notice that if you run the program, the clock always displays in 24-hour time format, regardless of what value you set for the TimeDisplayFormat property in design-mode. The reason for this is that the property is not persistent. That is, its value is not saved when the form itself is saved. This can be fixed by adding an entry to the property map for CDigiClock as follows:

BEGIN_PROP_MAP(CDigiClock)
   PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
   PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
   PROP_DATA_ENTRY("TimeDisplayFormat", m_TimeFormat, VT_UI4)
   // Example entries
   // PROP_ENTRY("Property Description", dispid, clsid)
   // PROP_PAGE(CLSID_StockColorPage)
END_PROP_MAP()

The property map is used by the ATL classes that implement persistence. Since our class already inherits from these classes, all that is required to make the property persistent is the entry in the property map. The IPersistStreamInit_Save() method of the CComControlBase class (from which our control inherits) goes through the property map and writes the values stored in the listed data members out to storage.

Property pages for ActiveX controls

Visual Basic programs can manipulate the properties that we expose through our control's default interface. For example, if a Visual Basic program wanted to set the format that our clock uses to display the time, it could use a line of code such as this:

DigiClock1.TimeDisplayFormat = TIMEFORMAT_24HOUR

Alternatively, a Visual Basic form designer could set the property through the Properties Window that Visual Basic very conveniently provides. A client program using our control might also want to present its users with a graphical interface with which to set the control's properties, like the Property Window in Visual Basic. The client could implement such a user interface, but a more convenient approach would be for the control itself to provide the interface. Property pages were designed for this purpose.

Property pages are simple dialogs that can be displayed within a frame. They contain controls such as buttons and list boxes that can be used to set the properties of the ActiveX control to which they belong. They are displayed in one of two ways; either the control can be asked to display a frame containing all its property pages, or the client can query the control for a list of its property pages, which can then be created and arranged in a frame by the client in its own way.

Just like ActiveX controls themselves, property pages are COM objects that implement a specific set of standard interfaces. Most notably, property page objects must implement the IPropertyPage interface. As you might expect, ATL and the Visual C++ Object Wizard make it easy to write property page objects by providing skeleton code and base classes that implement most of work that goes on behind the scenes. Typically, all one needs to do is design the layout of the property page using the Visual C++ dialog editor, write the code to read the internal state of the control and display it in the property page, and write the code to assign the values set on the property page to the properties of the control.

Adding a property page to the DigiClock control

To add a property page to our control, go to the Insert menu in Visual C++ and choose New ATL Object.... Select Controls in the Category pane, and Property Page in the Objects pane. In the Short Name entry box in the wizard screen that follows, enter "DigiClockPropPage" and let the wizard fill out the rest of the names. Go to the Strings tab, and enter "Digital Clock Properties" as a title. Click OK to finish the wizard.

This will open the dialog editor that we can use to create the visual layout of the property page. Our property page will be simple, offering just two radio buttons that can be used to switch from one display mode to the other. Use the control bar to add the two radio buttons and a group box around them. The dialog editor should end up looking like Figure 13. Specify IDC_RADIO_12HR as the ID for the first radio button and IDC_RADIO_24HR for the second.

Figure 13. The property page constructed in the dialog editor

Close the dialog editor, and notice that there is a new class listed under ClassView: CDigiClockPropPage. This class implements the COM object that represents our property page. It is derived from the ATL classes IPropertyPageImpl, which provides the implementation of the IPropertyPage interface, and CDialogImpl, which gives it much of its dialog behavior.

Because the property page is an independent object, not part of the ActiveX control object itself, the property page needs a way to access the properties of the control. To accomplish this, the client that creates the property page calls the IPropertyPage::SetObjects() method on the property page object, passing it an array of IUnknown pointers. These point to IUnknown interfaces on the controls to which the property page object refers. (There might be more than one instance of the control served by the same property page object, hence an array is passed, rather than a single pointer.)

Customizing the property page

Now that the dialog and the skeleton code have been created, we need to add code to customize the property page to our needs. First, we must implement the IPropertyPage::Show() method, which is called when the property page is to be displayed. This method needs to reflect the current internal state of the control on the property page. For our simple property page, this simply involves reading the current state of the control's TimeDisplayFormat property and making the appropriate radio button appear selected. Add the following inline code to the CDigiClockPropPage class declaration in DigiClockPropPage.h:

STDMETHOD(Show)(UINT nCmdShow)
{
   HRESULT hr;

       for (UINT i = 0; i < m_nObjects; i++) {
      IDigiClock *pDigiClock = NULL;

      hr = m_ppUnk[i]->QueryInterface(
         IID_IDigiClock, 
         (void **)&pDigiClock);

      if (FAILED(hr)) {
         return E_FAIL;
      }

      TimeFormat DisplayFormat = TIMEFORMAT_24HOUR;

      hr = pDigiClock->get_TimeDisplayFormat(
         &DisplayFormat);
         
pDigiClock->Release();

      if (FAILED(hr)) {         
         return E_FAIL;
      }   

      HWND    hWnd12HourRadioButton = 
            GetDlgItem(IDC_RADIO_12HR);
      HWND    hWnd24HourRadioButton = 
            GetDlgItem(IDC_RADIO_24HR);

      if (DisplayFormat == TIMEFORMAT_24HOUR) {
         ::SendMessage(
            hWnd12HourRadioButton, 
            BM_SETCHECK, 
            BST_UNCHECKED, 
            0);

         ::SendMessage(
            hWnd24HourRadioButton, 
            BM_SETCHECK, 
            BST_CHECKED, 
            0);            
      } else {
         ::SendMessage(
            hWnd12HourRadioButton, 
            BM_SETCHECK, 
            BST_CHECKED, 
            0);
         ::SendMessage(
            hWnd24HourRadioButton, 
            BM_SETCHECK, 
            BST_UNCHECKED, 
            0);              
      }
   }

   m_bDirty = FALSE;
   hr = IPropertyPageImpl<CDigiClockPropPage>::Show(nCmdShow);
   
return hr;
}

This code goes through the array of IUnknown pointers passed to the IPropertyPage::SetObjects() method by the client. The ATL implementation of IPropertyPage in IPropertyPageImpl stores these IUnknown pointers in an array called m_ppUnk, and stores the number of elements in the array in a member variable called m_nObjects. Recall that each of these is an IUnknown pointer to an instance of our ActiveX control. So we call QueryInterface() on each of these IUnknown pointers to get the IDigiClock interface on the control.

We then use the get_TimeDisplayFormat() method on this interface to get the current display format set on the control. CWindow::GetDlgItem() (recall that our class is indirectly derived from the ATL CWindow class) is then used to retrieve window handles to the two radio buttons and we set their checked state appropriately using the Win32 SendMessage() API function.

The IPropertyPage::Show() method takes care of reflecting the internal state of the control on the property page. The user can use the radio buttons on the property page to specify a desired value for the display format property. This needs to be set internally on the control when the Apply button is clicked. This is handled by the IPropertyPage::Apply() method. Change the body of the Apply() method in DigiClockPropPage.h as follows:

STDMETHOD(Apply)(void)
{
   ATLTRACE(_T("CDigiClockPropPage::Apply\n"));
   for (UINT i = 0; i < m_nObjects; i++)
   {
      HRESULT hr;
      IDigiClock *pDigiClock = NULL;
      TimeFormat  DisplayFormat;
         
      hr = m_ppUnk[i]->QueryInterface(
         IID_IDigiClock, 
         (void **)&pDigiClock);

      if (FAILED(hr)) {
         return E_FAIL;
      }

      HWND    hWnd24HourRadioButton = 
            GetDlgItem(IDC_RADIO_24HR);

      if (::SendMessage(
         hWnd24HourRadioButton, 
         BM_GETCHECK, 0, 0) == BST_CHECKED) {
            DisplayFormat = TIMEFORMAT_24HOUR;
         } else {
            DisplayFormat = TIMEFORMAT_12HOUR;
         }

         hr = pDigiClock->put_TimeDisplayFormat(
            DisplayFormat);

         pDigiClock->Release();

         if (FAILED(hr)) {
            return E_FAIL;
         }
      }
   }

   m_bDirty = FALSE;
   return S_OK;
}

Again, the array of IUnknown pointers is traversed, and for each, an IDigiClock interface is retrieved. After reading the state of the radio buttons to determine the desired value for the display format property (we only need to read one of the radio buttons, since they are mutually exclusive), we use the IDigiClock interface pointer to set the property value using the put_TimeDisplayFormat() method.

Connecting the control to the property page

We've now created a complete property page object, but you might have noticed that there is no connection from the control to this object. The control needs to be able to tell clients that this object is the one representing its property page, when asked. Typically, clients query the control for the ISpecifyPropertyPages interface (implemented by the ISpecifyPropertyPagesImpl ATL class), which contains a single method that returns a list of GUIDs that are the CLSIDs of the control's property pages. The ATL implementation gets this list of CLSIDs from the property map of the class, which we saw at the end of the previous section. So all that remains is to add an entry in the property map, listing the CLSID of the object we just created as the property page for our control. Remove the Prop_Data_Entry() macro that we placed in the property map in DigiClock.h and replace it with the following entry:

PROP_ENTRY("TimeDisplayFormat", 4, CLSID_DigiClockPropPage)

The number, 4, in this entry, is the dispatch identifier of the property described by the property page (this can be found in the "id()" specifier for the property in the IDL file).

To view the property page in action, we must first rebuild the desktop version of the control. However, instead of simply copying the files over this time, we need to make some modifications to the desktop project itself because we added a new object (the property page). To bring the desktop project into line with the Windows CE project, go to the instance of Visual C++ containing the desktop project, select New ATL Object... from the Insert menu, and then add a property page object.

Give the object the same short name as the one created in the Windows CE project ("DigiClockPropPage"), and finish the wizard. Then copy over all the .CPP, .H, .RGS, and .IDL files from the Windows CE version. In addition, copy all the .RC files from the Windows CE version. This is necessary because we added several new resources (the dialog and its controls) in the Windows CE version that are not present in the desktop version. Now rebuild the desktop version, and open the test client again in Visual Basic.

You might get an error loading the form because of the changes we made to the property map. If this happens, open the form (in which the control will be absent because it failed to load) and reinsert the control. Select the control in the form and look at the properties pane. There should be a new entry at the top labeled "Custom." Clicking in this entry should bring up our property page. If you don't see the changes made in the property page reflected properly on the display, it is because the standard Visual Basic property window has an entry for the TimeDisplayFormat property also. The value in this entry and the one in the property page must be made to match.

Adding events

Containers can communicate with controls through the control's exposed properties and methods. It is also useful for the control to be able to communicate back to the container. For example, the container might want to know when the control's state changes in some way. We've already seen an example of this in our control when we call the FireViewChange() function. Calling this function actually sends an event to the container, informing it that the control needs to redraw itself, which results in the container making the control execute its drawing code.

As with all things in COM, communication between controls and containers happens through interfaces. If we want to define custom events, we need to define an interface that has methods corresponding to those events. In the IDL file, we need to mark our control's default interface as capable of generating the events on this event interface, so that the IDL compiler can place this information in the control's type library.

The container has to read the control's type library to determine the IID of the control's event interface, implement this interface, and pass the control a pointer to this interface. The control then calls the methods on this interface pointer to signal events. Since the container implements the event interface, not the control (though its type information is defined by the control), the container can make the methods take whatever action is necessary to respond to the event.

We can add an event to our clock control to notify the container when the hour changes. Recall that, when we first created the control in the Object Wizard, we selected an option called Support Connection Points. This caused the wizard to do several things for us: It created a default event interface for our control (notice that this appears in ClassView as _IdigiClockEvents; it marked our CDigiClockClass as a source for events on this interface (in the IDL file, the coclass definition contains a line that starts with [default, source]); and it added a connection point map to the class.

We need to add a method to the event interface that we can call when the hour changes. Right-click the _IDigiClockEvents interface in ClassView, and choose Add Method..., as before. Specify void as the method return type, HourChanged as the method name, and "[in] short sNewHour" in the Parameters field. This will add a method to the event interface, taking one parameter (the new hour). When the hour changes, our control will invoke this method on an interface pointer passed to us by the client.

Now that the event interface is described in the IDL file, when the type library is built, the client can read it to determine the IID of the event interface for our control. It then queries our control for the standard IConnectionPointContainer interface (implemented for us by the ATL base class IConnectionPointContainerImpl). After successfully getting this interface, it calls the FindConnectionPoint() method that (thanks to the ATL implementation) will return a pointer to an IConnectionPoint interface on our control.

The container reads the description of the _IDigiClockEvents from the type library and implements it. It passes an IUnknown pointer to the object in which it has implemented _IDigiClockEvents to the Advise() method on the IConnectionPoint interface it got from our control. When the time comes to generate an event, our control must invoke a method on the event interface. One complicating factor is that, because the event interface is declared as a dispinterface in the IDL file, its methods cannot be accessed through a vtable as with the other interfaces we have encountered. Instead, they must be invoked using IDispatch::Invoke(), a technique that can become fairly involved. Fortunately, the Class Wizard saves us from having to manually invoke methods through IDispatch::Invoke(), by generating a proxy class that wraps IDispatch invocation in simple methods that we can call directly.

Before using the wizard to generate the proxy class, rebuild the project. This is required because we need to have the changes we made to the _IDigiClockEvents interface compiled into the type library so that the wizard can access this information.

Right-click on the CDigiClock class in ClassView and choose Implement Connection Point... from the context menu. The dialog that appears presents a list of connection point interfaces from which to choose. In our case there will be only one: _IDigiClockEvents. Select this interface, and click OK.

This will add a new class, CProxy_IdigiClockEvents, to the ClassView display and will add this class to the inheritance list of CDigiClock. Looking at the implementation of this class will show that it contains a single method named Fire_HourChanged(). We can call this method within the control when we want to generate the Hour Changed event, and it will take care of invoking the method on the container's interface using IDispatch::Invoke(). Finally to fire the event, make the changes to OnTimer() as shown in bold below:

LRESULT OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, 
BOOL& bHandled)
{
   short sOldHour = m_sHour;

   m_sSecond++;

   if (m_sSecond > 59) {
      m_sSecond = 0;
      m_sMinute++;
   }
   
   if (m_sMinute > 59) {
      m_sMinute = 0;
      m_sHour++;
   }

   if (m_sHour > 23)
      m_sHour = 0;

   if (sOldHour != m_sHour)
      Fire_HourChanged(m_sHour);

   //
   // Set the timer to fire again.
   //
   SetTimer(1, 1000);

   //
   // Update the display.
   //
   FireViewChange();

   bHandled = TRUE;
   return 0;
}

Due to a bug in the Class Wizard, the event interface identifier is listed in the CDigiClock connection point map as IID__IDigiClockEvents, but because it's a dispinterface, the IDL compiler defines it as DIID__IDigiClockEvents. This will generate a compile error, so change the connection point entry so that it contains "DIID__IDigiClockEvents."

Copy all the files over to the desktop version of the control and rebuild it to pick up our latest changes. Once that is done, we can add code to our Visual Basic client to handle the Hour Changed event. If we open up the code window for the Visual Basic client and select the DigiClock1 object in the left drop-down box at the top of the window, we will see an event called HourChanged in the right drop-down box. Selecting this allows you to write code to handle the event. I will leave it as an exercise for you to find something interesting to do with this event!

Suggestions for Further Reading

These are some books that I found useful, both as guides while learning about COM, ATL, and ActiveX controls, and as references later on. Many of these books were not written specifically for Windows CE, but the information contained in them applies as much to Windows CE as it does to other versions of Windows. Full citations appear in the References section at the end of this article.

  • An excellent introduction to COM programming: Inside COM by Dale Rogerson.
  • A good introduction to ATL: Beginning ATL COM Programming by Richard Grimes, Alex Stockton, George Reilly, and Julian Templeman.
  • More advanced ATL guide: ATL COM Programmer's Reference by Richard Grimes.
  • General Visual C++ programming, with chapters on COM, ATL, and ActiveX controls: Programming Microsoft Visual C++ by David J. Kruglinski, George Shepherd, and Scot Wingo.

Conclusion

It's hard to deny that the market for Windows CE applications will only grow in the coming years. Programmers targeting Windows CE will almost certainly turn to Visual Basic as an ideal tool for quickly implementing small to medium-sized applications. Since the standard controls available in Visual Basic for Windows CE are fairly limited in number and capability, these programmers will require powerful ActiveX controls to give their applications the features they need.

ATL and the Microsoft Visual Studio development tools take a lot of the boring work out of developing ActiveX controls. In this article we've seen how to implement many of the features of full-fledged ActiveX controls, including properties, methods, property pages, and events. Thanks to the code generated by the wizards and the implementation classes supplied by ATL, these features have taken very little time to accomplish.

Obviously, ActiveX controls can do a lot more than the simple ActiveX control we implemented here. After learning the material presented in this article, you should be well-placed to learn about the more advanced topics from the many books and papers written on the subject.

References

  • Boling, Douglas. Programming Microsoft Windows CE. Redmond, WA: Microsoft Press, 1998.
  • Grimes, Richard, Stockton, Alex, Reilly, George, and Templeman, Julian. Beginning ATL COM Programming. Wrox Press Ltd., 1998.
  • Kruglinski, David, Wingo, Scot, and Shepherd, George. Programming Microsoft Visual C++, 5th ed. Redmond, WA: Microsoft Press, 1998.
  • Rogerson, Dale. Inside COM. Redmond, WA: Microsoft Press, 1997.
  • Roof, Larry. Visual Basic Windows CE Programming. Wrox Press Ltd., 1998.
  • Stroustrup, Bjarne. The C++ Programming Language. New York, NY: Addison-Wesley, 1997.
Show: