MFC/COM Objects 2: Using Interfaces

Nigel Thompson
Microsoft Developer Network Technology Group

March 20, 1995

Click to open or copy the files in the HOUSE2 sample application for this technical article.

Click to open or copy the files in the Animate library.

Abstract

This technical article is the second in a series that describes creating and using 32-bit Component Object Model (COM) objects with Visual C++™ and the Microsoft® Foundation Class Library (MFC). This article describes how COM object interfaces are used to control the objects themselves.

Introduction

In "MFC/COM Objects 1: Creating a Simple Object," we looked at creating a simple COM object and using that object from inside an application. The object we created was a simple light bulb, which supported an interface that allowed the application using the object to tell the object to draw itself. I called that interface IDrawing. The application didn't need to know anything about the light bulb other than the fact that it supported the IDrawing interface. Because the application (the house) knew how to use the IDrawing interface, it was able to show the light bulb by calling appropriate functions in the light bulb's IDrawing interface.

What I'm going to do next is introduce some other objects: a standard lamp, a TV, and a radio. These new objects will all support the IDrawing interface, so the house will be able to show them in the rooms they occupy. We're also going to define some new interfaces and show how the house can use these new interfaces to control one of the objects (the lamp, TV, or radio) without knowing exactly what kind of object it's actually dealing with.

What's the point of this? The idea is that at some point in the future, new objects can be added to the house, and the house will be able to control those new objects without any changes in the house code, providing the new objects support the interfaces the house understands. So the house code becomes somewhat future-proof as new appliances are invented.

An Interface Hierarchy

All appliances are not created equal. Some appliances are very simple and can only be turned on and off. Lights can be on or off, and some lights can be set at intermediate levels of brightness. And some appliances, like TVs or radios, can be off or on, but also can be on a particular station, and so on.

It's obviously possible to design an interface that deals with off and on. It's reasonably easy to see how to design an interface that also deals with brightness, but it's not easy to see how to design the totally universal remote control that would be needed to control all the TVs, radios, and stereos in the world. It's not even very practical to try to design a future-proof TV or radio controller, so how are we going to produce a generic interface that the house can use for TVs and radios? We're not. We're going to let the appliance itself do that, and the house will simply ask the appliance if it has some fancy interface of its own that it can show to the user.

So when the house wants to control an appliance, it first asks the appliance if it has its own user interface, and if so, it tells the appliance to show the control panel to the user. If the appliance isn't that smart, the house tries to see if the appliance has a brightness-control interface. If it does, the house can show a simple dimmer-type control, and use the brightness interface in the appliance to set the light level in response to what the user does with the dimmer. And as a last resort, the house can see if the appliance supports the on-off interface, and if so, the house can show the user a simple switch and use the appliance's on-off interface to control it.

The interfaces that the house understands are as follows:

Interface Functions
IOutlet On, Off, and GetState
ILight SetBrightness and GetBrightness
IApplianceUI ShowControl

All appliances support one or more of these interfaces. Let's see how the house uses these when the user double-clicks an object:

void CMainFrame::OnLButtonDblClk(UINT nFlags, CPoint point) 
{
    if(m_pSelectRect == NULL) return; // No selection

    // Get the object's IUnknown interface pointer.
    IUnknown* pIUnknown = m_pAppliance[m_iSelect];
    ASSERT(pIUnknown);

    // See if it supports the IApplianceUI interface.
    IApplianceUI* pIApplianceUI = NULL;
    if (pIUnknown->QueryInterface(IID_IApplianceUI,
                                  (LPVOID*)&pIApplianceUI) == S_OK) {

        // Put up the interface.
        pIApplianceUI->ShowControl(this);
        pIApplianceUI->Release();
        return;
    }

    // See if it supports the ILight interface.
    ILight* pILight = NULL;
    if (pIUnknown->QueryInterface(IID_ILight,
                                  (LPVOID*)&pILight) == S_OK) {

        // Put up the interface.
        CLightDlg* pDlg = new CLightDlg;
        pDlg->m_pILight = pILight;
        pDlg->m_pParent = this;
        pDlg->Create();
        pILight->Release();
        return;
    }

    // See if it supports the IOutlet interface.
    IOutlet* pIOutlet = NULL;
    if (pIUnknown->QueryInterface(IID_IOutlet,
                                  (LPVOID*)&pIOutlet) == S_OK) {

        // Put up the interface.
        COutletDlg* pDlg = new COutletDlg;
        pDlg->m_pIOutlet = pIOutlet;
        pDlg->m_pParent = this;
        pDlg->Create();
        pIOutlet->Release();
        return;
    }


    // Pretty much a dead loss, this one.
    AfxMessageBox("This appliance is not controllable");
}

First of all, the house gets a pointer to the object's IUnknown interface. This pointer was saved when the object was first created and inserted in the house. Then the object's IUnknown::QueryInterface function is called to see if the object supports the IApplianceUI interface. If it does, the object is asked to show its own control. Figure 1 shows the radio's controller.

Figure 1. The radio's controller

If IApplianceUI is not supported, the house tries for the ILight interface. If this is supported by the object, the house puts up a slider control like the one shown in Figure 2.

Figure 2. The house's light controller

When the user moves the slider, the house uses the object's ILight::SetBrightness function to set the new light level.

If the object doesn't support ILight, the house finally tries for the IOutlet interface, and if it finds the interface, it uses a control like the one shown in Figure 3 to allow the user to switch the appliance on or off.

Figure 3. The house's on-off controller

What Happens When Objects Change State

If you've played with the HOUSE2 sample, you'll have discovered that the lights do turn on and off, and the radio plays a tune. That's what we wanted to happen, of course, but there's a part of the story that I've skipped. Let's say you double-clicked on a light bulb and got a controller like the one shown in Figure 3. You click the On button, and the house responds by calling the object's IOutlet::On function. The object then sets its own state to be ON. Fine and dandy, but how does the image of the light in the house change to show the new state? The object can't just redraw itself because it can't know how the house is rendering its own image. For example, the house may be using an off-screen bitmap to compose changes in its own image. In order to show the new state of the light bulb, the house must ask the light bulb to draw itself to the off-screen buffer (or whatever) and then make these changes show on the screen.

In the sample shown here, I cheated to make the light bulb's visible state change. When you click the buttons in the Outlet dialog box, the dialog code sends a message to the house's main window, asking it to repaint itself. As a result of this message, the house will ask the light bulb to draw itself, and so you'll see the change of state.

When you play with the radio's buttons, the radio itself starts the tune playing. There is no visible change of state in the house—perhaps you hadn't noticed that!

What we have so far is a push-only system. The application using the COM objects is telling them what to do, but isn't providing a path for information from the objects to the application. So currently there is no way for an appliance to tell the house that its state has changed and the house should redraw it.

Another side effect of this push-only model is that if you double-click an object twice, so as to bring up two instances of its controller, you'll find that the controllers don't know about each other. So (for example) if you have two sliders controlling the same light, moving one slider doesn't move the other one—it just changes the state of the light.

What we really need is a way for users of an object to be notified of changes of state in the object. When an object changes state, its users may indicate that change by either redrawing the object or showing a control in a new position. We'll be looking at how to implement this in the next article in the series, "MFC/COM Objects 3: Objects That Talk Back."

Objects with Multiple Interfaces

The COM objects we created in the previous article, "MFC/COM Objects 1: Creating a Simple Object," had only a single interface: IDrawing. Let's look at what's required to support several interfaces in a single object. We'll look at how the standard lamp that implements IDrawing, IOutlet, and ILight was done. Let's begin by looking at what got added to the header file to define the new interfaces. Here's part of the STANDARD.H file showing all the interface definitions:

class CStandardLamp : public CCmdTarget
{
   [...]

    // Declare the interface map for this object.
    DECLARE_INTERFACE_MAP()

    // IDrawing interface
    BEGIN_INTERFACE_PART(Drawing, IDrawing)
        STDMETHOD(Draw)(CDC* pDC,int x, int y);
        STDMETHOD(SetPalette)(CPalette* pPal);
        STDMETHOD(GetRect)(CRect* pRect);
    END_INTERFACE_PART(Drawing)

    // IOutlet interface
    BEGIN_INTERFACE_PART(Outlet, IOutlet)
        STDMETHOD(On)();
        STDMETHOD(Off)();
        STDMETHOD(GetState)(BOOL* pState);
    END_INTERFACE_PART(Outlet)

    // ILight interface
    BEGIN_INTERFACE_PART(Light, ILight)
        STDMETHOD(SetBrightness)(BYTE bLevel);
        STDMETHOD(GetBrightness)(BYTE* pLevel);
    END_INTERFACE_PART(Light)

   [...]
};

Notice that all that's needed is a declaration of each interface supported. The BEGIN_INTERFACE_PART and END_INTERFACE_PART macros are supplied by the Microsoft® Foundation Class Library (MFC). The STDMETHOD macro is supplied by the OLE libraries (which define COM objects).

The implementation is a little bit more involved because every interface must support the IUnknown interface functions. MFC provides almost everything needed to support IUnknown in your own interfaces, but you still need to write a small amount of code to implement AddRef, Release, and QueryInterface. If you refer to the first article in this series ("MFC/COM Objects 1: Creating a Simple Object"), you'll see that I included code for these functions in the IDrawing interface. We need to include almost exactly the same code in the IOutlet and ILight interfaces. In fact, it's so similar that I gave in and used a macro to avoid repeatedly typing the same code. The macro is called IMPLEMENT_IUNKNOWN and can be found in the IMPIUNK.H file. Please note that although this sounds a lot like an MFC macro name, it is not an MFC macro. Here it is:

#ifndef IMPLEMENT_IUNKNOWN

#define IMPLEMENT_IUNKNOWN_ADDREF(ObjectClass, InterfaceClass) \
    STDMETHODIMP_(ULONG) ObjectClass::X##InterfaceClass::AddRef(void) \
    { \
        METHOD_PROLOGUE(ObjectClass, InterfaceClass); \
        return pThis->ExternalAddRef(); \
    }

#define IMPLEMENT_IUNKNOWN_RELEASE(ObjectClass, InterfaceClass) \
    STDMETHODIMP_(ULONG) ObjectClass::X##InterfaceClass::Release(void) \
    { \
        METHOD_PROLOGUE(ObjectClass, InterfaceClass); \
        return pThis->ExternalRelease(); \
    }

#define IMPLEMENT_IUNKNOWN_QUERYINTERFACE(ObjectClass, InterfaceClass) \
    STDMETHODIMP ObjectClass::X##InterfaceClass::QueryInterface(REFIID riid, LPVOID* ppVoid) \
    { \
        METHOD_PROLOGUE(ObjectClass, InterfaceClass); \
        return (HRESULT)pThis->ExternalQueryInterface(&riid, ppVoid); \
    }

#define IMPLEMENT_IUNKNOWN(ObjectClass, InterfaceClass) \
    IMPLEMENT_IUNKNOWN_ADDREF(ObjectClass, InterfaceClass) \
    IMPLEMENT_IUNKNOWN_RELEASE(ObjectClass, InterfaceClass) \
    IMPLEMENT_IUNKNOWN_QUERYINTERFACE(ObjectClass, InterfaceClass)

#endif // IMPLEMENT_IUNKNOWN

Now that we have the macro, we can look at how the standard lamp's interfaces are implemented with a bit less clutter. The first addition is to the interface map:

BEGIN_INTERFACE_MAP(CStandardLamp, CCmdTarget)
    INTERFACE_PART(CStandardLamp, IID_IDrawing, Drawing)
    INTERFACE_PART(CStandardLamp, IID_IOutlet, Outlet)
    INTERFACE_PART(CStandardLamp, IID_ILight, Light)
END_INTERFACE_MAP()

We have simply added entries for the IOutlet and ILight interfaces.

I'm not going to show you the implementation of IDrawing because that's unchanged. Let's look at how IOutlet is implemented:

/////////////////////////////////////////////////////////
// IOutlet interface

// IUnknown for IOutlet
    IMPLEMENT_IUNKNOWN(CStandardLamp, Outlet)

// IOutlet methods
STDMETHODIMP CStandardLamp::XOutlet::On()
{
    METHOD_PROLOGUE(CStandardLamp, Outlet);
    pThis->m_bLevel = 255;
    return NOERROR;
}

STDMETHODIMP CStandardLamp::XOutlet::Off()
{
    METHOD_PROLOGUE(CStandardLamp, Outlet);
    pThis->m_bLevel = 0;
    return NOERROR;
}

STDMETHODIMP CStandardLamp::XOutlet::GetState(BOOL* pState)
{
    METHOD_PROLOGUE(CStandardLamp, Outlet);
    if (!pState) return E_INVALIDARG;
    *pState = (pThis->m_bLevel > 0) ? TRUE : FALSE;
    return NOERROR;
}

Yes, that's the entire thing. The IMPLEMENT_IUNKNOWN macro saves a lot of clutter. Note that each of the methods must include the METHOD_PROLOGUE macro, which provides access to the member of the containing class (CStandardLamp) and its pThis member. The actual implementation of the functionality of the On, Off, and GetState functions is trivial.

Implementation of the ILight interface is even simpler:

// IUnknown for ILight
    IMPLEMENT_IUNKNOWN(CStandardLamp, Light)

// ILight methods
STDMETHODIMP CStandardLamp::XLight::SetBrightness(BYTE bLevel)
{
    METHOD_PROLOGUE(CStandardLamp, Light);
    pThis->m_bLevel = bLevel;
    return NOERROR;
}

STDMETHODIMP CStandardLamp::XLight::GetBrightness(BYTE* pLevel)
{
    METHOD_PROLOGUE(CStandardLamp, Light);
    if (!pLevel) return E_INVALIDARG;
    *pLevel = pThis->m_bLevel;
    return NOERROR;
}

Summary

Adding interfaces to an existing COM object is quite simple. For an application to use a new interface, it is required only to understand the interface; it need have no knowledge of the nature of the actual object that supports it.

You might try adding a new appliance to the sample given here and see if the house can control it correctly. If your new appliance is to have its own user interface, take a look at how the radio was implemented.