ActiveX Control Fundamentals

. Since their introduction, ActiveX controls have earned a reputation as being among the most complex and least understandable COM objects one can build. Even seasoned COM developers' heads start spinning as they try to figure out which of the 18 control interfaces they should place their call to CreateWindow() in, or why their implementation of SetExtent() works fine when the control is embedded in HTML, but when brought into Visual Basic, quickly swamps its stack.

This has always struck me as unfortunate because there is nothing inherently complex about the functionality of a control. First of all, controls are always built as single-threaded in-process COM servers, the simplest of all COM servers. Second, they are simply COM wrappers around the creation and management of a window, a relatively simple process. Unfortunately, the control interfaces don't make it easy to identify where this simple functionality should be provided. My goal in this column will be to approach ActiveX controls from the ground up, looking at only the interfaces and methods necessary to create and manage a window from a container.

Readers of past versions of this column will notice that the name has changed to include ATL as well as MFC, reflecting the shift in focus of the column to COM programming. Having said that, however, I will discuss neither ATL nor MFC this month, but instead will build an ActiveX control in straight C++. This is not to eschew the use of libraries, but rather to provide a core level of understanding of the architecture that is encapsulated by the C++ libraries. In future columns I will look in detail at some of the ActiveX control features provided in both ATL and MFC.

THE CASE FOR COMPLEXITY The first question to be answered is, why then, are the control interfaces so complex? This can be answered in two parts. First, controls inherit much of their design from the larger and more complex ancestors, OLE documents (and their more recent incarnation, Active documents). OLE documents allow applications to embed their data transparently in other applications. This implies several complex interactions, including interprocess communication, rendering into metafiles, supporting a sophisticated persistence scheme that allows heterogeneous objects to share the same file stream, and the list goes on. Controls were introduced as an in-process special case version of OLE documents, which made them immediately usable by containers that knew how to embed OLE documents. Unfortunately, this also meant that controls had to support a large number of interfaces, a significant percentage of which were not even used by the control or its container.

The second source of complexity in control interfaces arises from the fact that the inherited OLE document interfaces did not provide all of the necessary control-like behavior that developers were used to seeing (like mnemonic key propagation, event notification, and property pages, to name a few). Also, as developers started using controls more and more, they ran across limitations in the control interfaces, so additional interfaces were added to compensate for these problems (including features like windowless controls, flicker-free activation, and a control licensing scheme).

The end result is that a control is defined by a large number of interfaces, including both inherited and new functionality, not all of which should be used by every implementation. To confuse matters further, some of the inherited interfaces contain methods that a control will almost never implement, but the interface itself must still be supported.

WHAT IS CONTROL? Before we attempt to understand the control interfaces, we had better understand exactly what it is we are building. ActiveX controls are designed to behave much like native Windows controls, providing developers a way of distributing custom visual components that integrate easily into development environments like Visual Basic, Visual C++, or even HTML. To better understand what functionality we are seeking to provide, let's first look at how a native Windows control (say a button) could be used:
HWND hwndButton =
CreateWindow("BUTTON",   // Window class
             "Foo",      // Window title
             WS_CHILD |  // Window styles
    WS_VISIBLE |
    BS_PUSHBUTTON,
    10, 10, 100, 100,    // Initial rectangle
    hwndParent, 0,                   // Parent window
    ghInstance,
    IDC_FOOBUTTON);                  // Identifier
ShowWindow(hwndButton, SW_HIDE);
ShowWindow(hwndButton, SW_SHOW);
SetWindowPos(hwndButton, NULL, 0, 0, 50, 50, SWP_NOMOVE);
RECT r;
GetClientRect(&r);
DestroyWindow(hwndButton);
The above code created the button control, hid it, redisplayed it, changed its size, queried for its rectangle, and finally destroyed it. In order to keep things simple, our focus in building an ActiveX control will be to provide this functionality, and no more. Thus, a container should be able to ask us to:
  • create our window;
  • toggle our window's display;
  • change our window's size;
  • ask for our window's size;
  • destroy our window.
figure 1
Figure 1. The LED control.

ActiveX controls can also provide methods and notifications that containers can call and respond to, but our focus will only be on creating and manipulating the window. The following code shows how a container might perform the same sequence of tasks on an ActiveX control (identified by CLSID_Foo) as we performed above on the button control:
IOleObject* pControl;
CoCreateInstance(CLSID_Foo, 0, CLSCTX_ALL,
                   IID_IOleObject, (void**)&pControl);
RECT r;
SetRect(&r, 10, 10, 100, 100);
pControl->DoVerb(OLEIVERB_INPLACEACTIVATE, NULL,
             pClientSite, 0, hwndParent, &r);
pControl->DoVerb(OLEIVERB_HIDE, NULL,
             pClientSite, 0, hwndParent, &r);
pControl->DoVerb(OLEIVERB_SHOW, NULL,
             pClientSite, 0, hwndParent, &r);
SIZEL sz;
sz.cx = sz.cy = 1764;           // ~50 pixels in HIMETRIC units
pControl->SetExtent(DVASPECT_CONTENT, &sz);
pControl->GetExtent(DVASPECT_CONTENT, &sz);
pControl->Close(OLECLOSE_NOSAVE);
There are a few things to note about the differences in the two pieces of code. First, the ActiveX control must be instantiated through standard COM creation before it can be called upon (CoCreateInstance). Second, there is no direct interaction with the window handle of the control—instead, all interaction is performed through the IOleObject interface supported by the control. Third, you should note that ActiveX controls have some odd ways of doing things. For example, the DoVerb method is overloaded to perform window creation and visibility toggling. Also, the request for changing the size of a control must be done in HIMETRIC units instead of pixels. Keep these oddities in mind as we begin building our control.

CONTROL CLASS Our goal is now to create an ActiveX control that can be inserted into all well-known control containers, but which provides only simple window management, thus reducing the number of methods we will really need to implement. This means that our control will not have any properties or methods, so to make our control a bit more interesting, we will have it display the current memory usage of our computer through a slick LED-like display as shown in Figure 1. The complete implementation of this control as described in this column can be downloaded at www.develop.com/hp/onion/cpprep/RawCtrl.zip.

There are several uninteresting things to note about our control class. It will be housed in an in-process COM server (dll). It will have a standard implementation of the IUnknown methods, and will be a heap-based object whose lifetime is controlled by its reference count. It will have a standard class object that implements IClassFactory, allowing clients to instantiate it. The dll will export the standard functions required by an in-process COM server. It will have a type library, which it will register as part of the registration process. It will have standard registry entries for an in-process COM object.

Now, on to the interesting things about our control class. First of all, we need to decide which of the 18 or so ActiveX control interfaces we absolutely have to support so that arbitrary containers will be able to insert our control into their forms and windows. Surprisingly, we can basically choose only three interfaces to support, and of the 33 methods that comprise those three interfaces, we can get away with implementing only eight. In the full implementation of the control in the sample code, you will notice several more interfaces are added to our control class' base-class list. The methods on these additional interfaces are not implemented, or they simply return a trivial return code, but were necessary for some containers to be able to query successfully for.

As you probably guessed from the sample container code above, one of the interfaces we will have to support is IOleObject. The other two are IOleInPlaceObject and IViewObject. Here then, is what our control class will look like (only the methods we are going to implement have been listed):
class RawCtrl : public IOleObject,
                public IOleInPlaceObject,
                public IViewObject
{
private:
 LONG               m_cRef;             // Reference count
 IOleClientSite*    m_pOleClientSite;   // Container ptr
 SIZE               m_hmSize;           // Size in HIMETRIC
 HWND               m_hWnd;             // Our window handle

public:
// IUnknown methods not shown
// Relevant IOleObject methods
    STDMETHODIMP
       SetClientSite(IOleClientSite *pClientSite);
    STDMETHODIMP
       GetClientSite(IOleClientSite **ppClientSite);
    STDMETHODIMP Close(DWORD dwSaveOption);
    STDMETHODIMP DoVerb(LONG iVerb, ...,
       HWND hwndParent, LPCRECT lprcPosRect);
    STDMETHODIMP GetUserClassID(CLSID *pClsid);
    STDMETHODIMP SetExtent(DWORD dwDrawAspect,
                                            SIZEL *psizel);
    STDMETHODIMP GetExtent(DWORD dwDrawAspect,
                                            SIZEL *psizel);
// Relevant IOleInPlaceObject methods
    STDMETHODIMP GetWindow(HWND *phwnd);
// Relevant IViewObject methods
    STDMETHODIMP Draw(..., HDC hdcDraw,
                 LPCRECTL lprcBounds,...);
};
Notice that in addition to the methods of the three interfaces we are implementing, we have added four data members as well. The reference count is a standard COM reference count. The IOleClientSite pointer will be a pointer into our current container, which we will discuss shortly. The size is necessary because containers will communicate size requirements to us in HIMETRIC units, and finally the HWND will simply store the window handle of our control.

CONTAINER COMMUNICATION Our control will need to keep a reference to its container, primarily through its IOleClientSite interface. This will allow us to ask the container questions and send it notifications at any time. Although our simple control will not take advantage of this container back pointer, it is still necessary to implement the set and get functions for it, as we may be asked by others for our client site pointer.
STDMETHODIMP
RawCtrl::SetClientSite(IOleClientSite *pClientSite)
{
 if (m_pOleClientSite)
 {
    m_pOleClientSite->Release();
    m_pOleClientSite = NULL;
 }
 if (pClientSite)
 {
    m_pOleClientSite = pClientSite;
    m_pOleClientSite->AddRef();
 }
 return S_OK;
}

STDMETHODIMP
RawCtrl::GetClientSite(IOleClientSite **ppClientSite)
{
  *ppClientSite = m_pOleClientSite;
  (*ppClientSite)->AddRef();
  return S_OK;
}
VERBIAGE One of the most confusing aspects of a control's interfaces is the DoVerb method of IOleObject. This method was inherited from OLE documents, where it serves the purpose of transitioning servers among various states of being passive, loaded, running and visible, or running and not visible. It also provides a way of issuing generic server-defined commands to a server.

Because controls are in-process, they don't typically have any of the various states that OLE document servers do, so the DoVerb function for a control basically controls its visibility. That is, controls could simply have a single method, ShowWindow(bool bShow), to replace the entire verb concept. Unfortunately, we are stuck with verbs, so let's bite the bullet and implement our DoVerb method.

The first time a container calls our DoVerb method requesting our control to display or activate, we should create our control's window and make it visible. On subsequent calls requesting display, we simply show and set the focus to our window. The one other possibility is the container asks us to hide ourselves, in which case we hide our window. In our implementation, we are assuming that there is a registered window class called "LedClass" that provides our slick LED-like display, and that we have cached our instance handle in a global variable called gHInstance. Our complete implementation will, thus, look something like:
STDMETHODIMP RawCtrl::DoVerb(LONG iVerb, LPMSG lpmsg,
        IOleClientSite *pActiveSite, LONG lindex,
        HWND hwndParent, LPCRECT lprcPosRect)
{
   switch (iVerb)
   {
   case OLEIVERB_PRIMARY:
   case OLEIVERB_SHOW:
   case OLEIVERB_INPLACEACTIVATE:
   case OLEIVERB_UIACTIVATE:
      if (m_hWnd != NULL)
      {
        ShowWindow(m_hWnd, SW_SHOW);
        if (!IsChild(m_hWnd, GetFocus()))
             SetFocus(m_hWnd);
      }
      else
      {
         m_hWnd = ::CreateWindow("LedClass", "",
           WS_CHILD | WS_VISIBLE,
           lprcPosRect->left, lprcPosRect->top,
           lprcPosRect->right-lprcPosRect->left,
           lprcPosRect->bottom-lprcPosRect->top,
           hwndParent, 0, gHInstance, 0);
      }
      break;

 case OLEIVERB_HIDE:
    if (m_hWnd != NULL)
         ShowWindow(m_hWnd, SW_HIDE);
    break;

  default:
     return E_NOTIMPL;
  }

  return S_OK;
}
It should be noted that many controls respond to the OLEIVERB_PRIMARY verb based on whether they are in design mode or not (which you can find out by querying your container). When in design mode, it is often appropriate to bring up a property page allowing the user to edit your control's properties. Because our control doesn't have properties, we simply treat it as a request to display ourselves.

The other method that affects our control's window is the Close method of IOleObject. This one's easy—if we have a valid window handle, close it, and release any resources.
STDMETHODIMP RawCtrl::Close(DWORD dwSaveOption)
{
 if (m_hWnd)
      DestroyWindow(m_hWnd);
 if (m_pOleClientSite)
      m_pOleClientSite->Release();
 return S_OK;
}
SIZING A WINDOW Sizing of the control is usually dictated by the container, and that size is handed down in the SetExtent() call. Because of its shared history with OLE document servers, ActiveX controls are forced to interpret sizing in HIMETRIC units, so we will have to perform a conversion from our native pixel coordinate sizes to HIMETRIC. It is also important to be aware that the typical container will perform initial sizing as an iterative process. This process begins with the container asking the control to set itself to a specific size. The container will then query the control to find out what size it took on, and if it is not acceptable, the container might again request the control to resize itself. Our control will always just conform to the container's wishes, so we simply respond to SetExtent by changing our size to the requested size. ConvertUnits is a function to convert from pixels to HIMETRIC units and back and is included in the sample code.
STDMETHODIMP RawCtrl::SetExtent(DWORD dwDrawAspect, 
                                            SIZEL *psizel)
{
     if (dwDrawAspect & DVASPECT_CONTENT)
   { // Convert psizel to pixels
    SIZE sl = ConvertUnits(*psizel, false);

       // Save new size to our control's rect
    m_rcPos.right = m_rcPos.left + sl.cx;
    m_rcPos.bottom = m_rcPos.left + sl.cy;
      // Save HIMETRIC units in our m_hmSize
    m_hmSize.cx = psizel->cx;
    m_hmSize.cy = psizel->cy;

      // Resize our window if we have one
    if(m_hWnd)
     SetWindowPos(m_hWnd, NULL, 0, 0, sl.cx, sl.cy,
       SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);

       return S_OK;
    }
 else // we don't support any other aspects.
         return DV_E_DVASPECT;
}

STDMETHODIMP RawCtrl::GetExtent(DWORD dwDrawAspect,
                                             SIZEL *psizel)
{
     if (dwDrawAspect & DVASPECT_CONTENT)
 {
    psizel->cx = m_hmSize.cx;
    psizel->cy = m_hmSize.cy;

    return S_OK;
     }
 else
       return DV_E_DVASPECT;

 return S_OK;
}
DESIGN-TIME SUPPORT There are two other functions that really must be implemented for most containers to work correctly, and they both have to do with providing design-time support for your control. Some containers treat a control differently when it is used at design-time versus when it is used at runtime (a control placed on a form with a resource editor would be considered in design time, but when that form was brought up in a running program, it would be considered runtime).

The first function is IOleObject::GetUserClassID, which simply returns the CLSID of our control:
STDMETHODIMP RawCtrl::GetUserClassID(CLSID *pClsid)
{
  *pClsid = CLSID_RawCtrl;
  return S_OK;
}
Containers will call this function to query the CLSID to store it in their resource stream so that the control can be recreated at runtime. For example, in Visual C++ when you add an ActiveX control to a dialog template, the CLSID is stored as part of the dialog template resource script.

The other function we should probably provide is IViewObject::Draw(). This function is used by OLE document servers to provide in-process renderings of embedded objects (typically handled by an in-process handler). Because ActiveX controls are already in-process servers, this function doesn't seem necessary because we already create our own window that receives its own paint messages. However, some containers may request it anyway. For example, a container may decide to cache metafile representations of all of its controls so it can bring up design-time forms without instantiating every control in the form. The way a container would do this is by creating a metafile, attaching a device context to it, and passing that device context into the IViewObject::Draw function of each control on the form. Fortunately, our draw function will look exactly the same as our paint handler, as it will in most controls. Only if your window display performs some tasks that may not translate well into metafiles would you implement your draw function differently than your paint handler. It should also be noted that the draw function is used by windowless controls, where it is used at runtime as well.

CONCLUSION Here we are, eight functions later, and we have an ActiveX control that can be embedded in any control container. This doesn't mean that you shouldn't reach for your nearest C++ library the next time you need to implement a control, but hopefully you have a better perspective on what that library is doing for you, and where to look when things don't behave as you expect. In future columns, I will look at additional features of controls, and how they are implemented in both ATL and MFC.


Fritz Onion is a trainer with DevelopMentor, and a software-development consultant based in southern Maine.
He can be contacted at fonion@ix.netcom.com.

Featured

Most   Popular
Upcoming Events

AppTrends

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.