Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

ActiveX Controls for Embedded Visual Basic


Jul01: ActiveX Controls for Embedded Visual Basic

Steve is a senior software engineer at BSQUARE Corp. He can be reached at stephenl@ bsquare.com.


Many Windows CE developers find Microsoft's Embedded Visual Basic 3.0 (eVB) a tool with limitations, especially when compared to Visual Basic 6.0. Consequently, to enhance the speed and power of eVB applications, they turn to ActiveX controls written in C++. However, writing an ActiveX control that targets eVB as its host application presents some challenges:

  • Almost all books and articles on writing ActiveX controls assume that you are targeting a desktop Windows platform, not Windows CE.
  • You must build your run-time control with Embedded Visual C++, but you must build your design-time control with Visual Studio. Adapting your control so it builds in both environments is a manual procedure with much opportunity for error and frustration.

  • Your run-time control must use Unicode to run under Windows CE, but your design-time control must use ANSI to run under Windows 98.

  • Embedded Visual Basic requires that controls support certain optional ActiveX features, such as property-bag persistence and the free-threading model. This information is not well documented.

  • Embedded Visual Basic uses special data types not normally found in C++. This affects how strings, arrays, and Boolean data are exchanged between controls and eVB applications.

In this article, I'll present an ActiveX control called "Sample" that addresses these issues. While I have targeted the control for the Pocket PC Emulator, everything covered here applies to actual Pocket PC devices, and with few exceptions, to other eVB-capable Windows CE devices such as the Handheld PC Pro. The complete source code for Sample is available electronically; see "Resource Center," page 5.

Creating a New Control

The first task I tackled was creating a single project directory to build both the design-time and run-time versions of the Sample control.

  1. In Embedded Visual C++, I created a new WCE ATL COM AppWizard project named "Sample ActiveX control" with CPU support for ARM, MIPS, SH3, and x86em (which is different from x86). I did not select the MFC checkbox.

  2. In Visual Studio, I created a new ATL COM AppWizard project named "Sample ActiveX control_desktop." The name is identical to my eVC project but with "_desktop" appended; this is important.

  3. I chose the DLL server type, but did not select proxy merging, MFC, or MTS. When the project was created successfully, I shut down Visual Studio.

  4. In eVC, I opened "Sample ActiveX control_desktop.dsp" for editing as a text file. Note that in the Open dialog box, I had to choose "Text" explicitly, versus "Auto."

  5. The top of this file contains a warning that says, "DO NOT EDIT." I must confess to some civil disobedience: I deleted all occurrences of the string "_desktop," renamed the edited file to "Sample ActiveX control.dsp," and saved it in the "Sample ActiveX control" directory from Step 1.

  6. Finally, I closed the file and deleted the entire "Sample ActiveX control_desktop" directory.

Next, I created an ATL class for the control.

  1. Using eVC's ATL Object Wizard, I added a "Full Control" to the project.

  2. On the Names tab, I named the control "Sample."

  3. On the Attributes tab, I choose free threading and selected both "Support Connection Points" and "Support ISupportErrorInfo." This lets the control fire events and raise custom eVB errors.

  4. On the Miscellaneous tab, I selected "Windowed Only," because Embedded Visual Basic does not support windowless controls.

  5. Clicking OK added the control's header file, implementation file, ATL registry script, and toolbox icon to the eVC project.

  6. In Visual Studio, I opened "Sample ActiveX control.dsp" as a workspace. Then I added Sample.h, Sample.cpp, Sample.rgs, and Sample.bmp to the project; see Figure 1.

Threading Issues

You may wonder why I chose free threading in Step 3. Windows CE requires that ActiveX controls be free threaded, not apartment threaded. This is a significant difference from desktop Windows, which encourages the apartment model for controls! In fact, the desktop version of ATL can't even support free-threaded controls properly, so you must make the design-time version of your control be apartment threaded while still making the run-time version be free threaded.

To pull off this trick, I edited two of the project files as follows:

  1. In Stdafx.h, I rewrote the section of the file near the statement #if defined(_WIN32_WCE) so that Windows CE builds are free threaded, desktop builds are apartment threaded, and _WIN32_WINNT is only defined for desktop builds; see Example 1.

  2. In Sample.rgs, I changed the threading model from "Free" to "Both." Since the IDE prefers apartment-threaded controls, that is how the control runs at design time; see Example 2.

Building the Control

After saving and closing all the files I had edited, it was then possible to build the control in both environments.

In Visual Studio, I set the active configuration to "Win32 Debug" and selected the Rebuild All command. Although the control built fine, for some reason Visual Studio failed to register it automatically. So I used the Register Control command on the Tools menu instead.

In Embedded Visual C++, I set the active platform to "Pocket PC," the active configuration to "Win32 (WCE x86em) Debug," and selected the Rebuild All command. When the control finished building, eVC started the Pocket PC Emulator, downloaded the control to the emulator's file system, and registered the control within the emulation environment.

Trying the Control

At this point, I was ready to try the control in an eVB application.

  1. Starting Embedded Visual Basic, I created a new Pocket PC project.

  2. Under the Project menu, I selected Components.

  3. Since this control had never been used before, I clicked the Browse button, changed the file type from the obsolete ".ocx" to "All files," and located the desktop DLL in the project's Debug subdirectory.

  4. eVB prompted me to add the control to the current platform; I clicked Yes.

The Sample control's icon appeared in the eVB toolbox, and I added it to the form just like any other control; see Figure 2.

To run the application, I made sure "Pocket PC Emulation" was selected on the eVB toolbar, then chose Execute on the Run menu. The application appeared in the Emulator, looking like Figure 3.

Afterwards, I clicked the OK button to close the application. Then I shut down eVB. This freed up both copies of the DLL so I could rebuild them.

Adding Properties

Switching back to eVC, I added some properties to the Sample control. When defining properties and methods, it is important to remember that Embedded Visual Basic's support for data types is somewhat limited.

  • eVB does not support user-defined types (structs).
  • eVB does not support any unsigned integers.

  • eVB does not support the C++ bool or BOOL types. Controls must declare all Boolean properties and method parameters as VARIANT_BOOL. Also, eVB does not use 1 and 0 to represent True and False; it uses the VARIANT_TRUE and VARIANT_FALSE constants instead.

  • eVB supports enumerations, as long as the enum is defined in the control's type library (IDL).

  • eVB requires that ActiveX controls use a special kind of string called a "BSTR."

Many developers do not handle BSTRs correctly, which leads to memory leaks, data corruption, and access violations. To a C++ program, a BSTR looks like a normal NULL-terminated Unicode string. However, there are two key differences:

  • BSTRs must be allocated using SysAllocString (to copy an existing string) or SysAllocStringLen (to build a new string). You can't allocate a BSTR on the stack using the new operator.
  • Once a BSTR is allocated, you can't change its length, not even to shorten it. If you need to alter the length of a BSTR, you must create a new BSTR and free the old one using SysFreeString.

Also, a Unicode string is not necessarily a proper BSTR. If all you have is an LPWSTR or LPOLESTR pointer, do not assign it directly to a BSTR; call SysAllocString instead.

To demonstrate proper use of BSTRs, I added a property, SampleString, to the Sample control by right-clicking on the ISample interface and selecting Add Property; see Figure 4. After eVC set up the skeleton of the property for me, I implemented it in three steps:

  1. In Sample.h, I added a private member variable, bstrSampleString, to hold the string itself. I declared bstrSampleString as a CComBSTR, which is an ATL class that handles most of the tricky details of managing BSTRs for me.

  2. In get_SampleString, I used the CComBSTR::Copy method to copy my string for my caller. One of COM's rules is that callers own any pointers that get passed to or returned from methods; therefore, my caller has the right, in fact the obligation, to free the BSTR that I return. I don't want eVB to free my own copy of the string.

  3. In put_SampleString, I used CComBSTR::operator = to free my old string (if any) and copy my new string. Again, I make my own copy because my caller owns the one it passes to me. See Sample.cpp, and also look up the definition of CComBSTR in the AtlBase.h header file.

It is not necessary to use CComBSTR. Listings One and Two (Sample.h and Sample.cpp, respectively) have code surrounded by #if USE_PLAIN_BSTR, showing what I would have to do if I declared bstrSampleString to be a simple BSTR pointer variable. I would have to initialize bstrSampleString in the constructor and call SysFreeString in the destructor. Also, in the get/set methods, I would have to free old strings and copy new strings myself.

Obviously, using CComBSTR to take care of these details is convenient. However, you can't use CComBSTR blindly. For instance, the version of put_SampleString compiled with USE_PLAIN_BSTR leaks memory if bstrSampleString is actually declared as a CComBSTR, because the overridden assignment operator makes a second call to SysAllocString. This produces an extra copy of the string, which immediately becomes orphaned.

The Windows 98 Issue

The fact that BSTRs are always Unicode presents another challenge. Previous versions of Embedded Visual Tools (marketed as the Windows CE Toolkits for Visual C++ and Visual Basic) only worked on Windows NT. This made life easier for ActiveX control developers, because Windows NT supports Unicode in addition to ANSI, and Windows CE, of course, is Unicode only. Thus, ActiveX controls could be written to use Unicode strings for all occasions.

However, the recent "retro" trend apparently affected Microsoft as well. Starting with Version 3.0, the Embedded Visual Tools run under Windows 98/ME, but this requires your design-time control to pass strings to Windows APIs. Since eVB still uses Unicode BSTRs, you need to call a conversion function whenever you have a BSTR that you want to pass to an API.

For instance, take a look at the OnDraw method in Sample.cpp. Notice how I use ATL's OLE2T macro to convert bstrDisplayName into a Win32 LPTSTR before passing it to the DrawText API. At design time, this macro creates a temporary string from the original Unicode so it can be passed to Windows 98's DrawText. But at run time, this macro is a no-op, because Windows CE's DrawText accepts Unicode.

Be aware that OLE2T places the temporary string on your function's stack, so you should never store a pointer to that string in a global, static, or class member variable, nor return it to your caller.

Passing Arrays To/From Controls

One of the least-understood aspects of writing ActiveX controls for Embedded Visual Basic is passing arrays back and forth between the control and the eVB application.

In C++, an array must be homogenous; that is, every element must have the same type. In eVB, however, every element of every array is a Variant — a special structure that stores a value along with a code indicating the type of that value. This means that each element of an eVB array could have a different type. Example 3, for instance, is perfectly legal eVB.

The Sample control has an array-valued property, SampleArray, that is supposed to be an array of longs. Figure 5 shows how I added this property to the control. Notice that the property type is VARIANT. This makes sense when you examine the put_SampleArray and get_SampleArray methods in Sample.cpp.

In put_SampleArray, I have to convert an eVB array to a C++ array of longs. First, I measure the size of the eVB array and allocate a C++ array of the same size. Then, I use SafeArrayGetElement to read the Variants in the eVB array one-by-one. Finally, I try to convert each individual element to a long, which is Variant type code VT_I4 (4-byte signed integer). Of course, not all Variants may be converted to longs successfully. For instance, a Variant containing the string "123" is parsed and converted to the value 123, but a Variant containing the string "Hello" raises an error.

In get_SampleArray, when converting the C++ array back to an eVB array, I have to create a SafeArray of Variants with the appropriate size. Then I fill in each element by storing the long value in a temporary Variant and calling SafeArrayPutElement to copy it into the SafeArray. I like to use ATL's CComVariant for my temporary Variants, because it takes care of cleanup that may need to occur when storing arrays of more complicated objects. Finally, I set eVB's Variant to point to my SafeArray before returning.

In your controls, you may want to let users set array-valued properties at design time. Unfortunately, the eVB IDE's property browser does not support arrays. For the sake of space, I have chosen to fill SampleArray with some dummy data in the constructor. In a real-world control, you would add a custom property page that allowed you to read and set the array from the IDE. Take a look at eVB's built-in ListBox and ComboBox controls to see how custom property pages are used to initialize the List and ItemData array properties at design time.

Property Persistence

Controls must have a way to save and load their properties. In ActiveX jargon, this is called "persistence," and there are two types: streams (which are binary) and property bags (which are human-readable text).

Embedded Visual Basic requires that controls implement both types. It uses property bags when it saves projects to disk and compiles applications into .vb files, and it uses streams when copying and pasting controls in the IDE. By default, ATL controls only implement stream persistence. I had to add property-bag persistence to the Sample control manually.

First, I added ATL's IPersistPropertyBagImpl<> template to the list of base classes for CSample. Then, I modified the interface map in Sample.h to add a COM_INTERFACE_ENTRY for IPersistPropertyBag, and changed the entry for IPersist to use it instead of IPersistStream. Finally, I added the SampleString property to the control's ATL property map so that IPersistPropertyBagImpl<> would know to persist it; see the PROP_ENTRY line in Sample.h.

Adding support for property bags came with an extra challenge. The property bags in eVB do not support eVB's own array format. Therefore, I couldn't use IPersistPropertyBagImpl<> to persist the SampleArray property. I had to override IPersistPropertyBag::Save and IPersistPropertyBag::Load to handle the array. Here is what I learned:

  • You can't tell why Save/Load are being called. Users may be saving the project to disk, opening the project in the IDE, compiling the project into a .vb file, or loading the .vb file on a device.
  • The property bag that eVB uses to compile and read .vb files cannot handle SafeArrays at all. At run time, it returns a NULL pointer if you attempt to read a SafeArray. Even worse, at compile time, it does not raise an error if you attempt to write a SafeArray, so your control has no way of knowing that this operation is not supported. However, this property bag does support a special Variant type called a "blob," which is an array of bytes preceded by a 32-bit integer indicating the number of bytes in the array.

  • The property bag that eVB uses to save and load .ebf files won't accept blobs. At least this property bag does return an error if you try to write a blob. It can, however, handle homogenous SafeArrays, but not SafeArrays of Variants. This means that I couldn't just recycle get_SampleArray and put_SampleArray; I had to duplicate the logic of those methods, but using SafeArrays of longs rather than SafeArrays of Variants.

Sample.cpp includes my complete Save/Load methods. When eVB asks the Sample control to save its properties, I construct a homogenous SafeArray of longs that has an extra element at the beginning to store a byte count. This makes the raw data in the SafeArray into a blob, which I attempt to write first. The write succeeds if eVB is compiling an application, but fails if it is saving a form to disk. In that case, I write the SafeArray itself. I can't attempt the writes in the opposite order, because eVB won't tell me that writing the SafeArray failed if the application is being compiled.

Loading is considerably easier, because the Variant has already been created. I just have to read its type field and unpack it appropriately.

Raising Errors

Errors that occur in your ActiveX control must be sent back to eVB using HRESULTs. Unfortunately, HRESULTs are not the same thing as eVB error numbers. HRESULTs are based on standard Win32 error codes, but eVB error numbers are based on previous versions of Microsoft Basic. (Many of them are the same as the error numbers on my first TRS-80, which used Microsoft Level II Basic in 1981!)

eVB recognizes some standard HRESULTs. Table 1 shows the most common ones. If you return one of these HRESULTs, eVB raises its own error appropriately.

If you need to raise an eVB error that is not in Table 1, you can construct a special HRESULT that only eVB understands using the MAKE_HRESULT macro. This macro takes three parameters: The first indicates that the HRESULT is an error; the second indicates that the error is eVB specific; and the third is the eVB error number itself. For example,

return MAKE_HRESULT(SEVERITY_ERROR, 3625, 11);

makes eVB raise error number 11, Division by Zero. Microsoft Knowledge Base article Q180751 has a complete list of eVB error numbers that can be raised this way. Knowledge Base article Q189134 describes in more detail why this technique works, but note that eVB, unlike Visual Basic, requires the use of facility code 3625, not FACILITY_ITF.

It is also possible to raise a custom error that is unique to your ActiveX control. To avoid conflict with existing eVB errors, you should choose a custom error number between 512 and 65,535. Because this is not a standard eVB error number, you must build the HRESULT with facility code FACILITY_CONTROL, not facility code 3625, and you must cache a description string for your error. Your control must also implement ISupportErrorInfo.

The RaiseError method in Sample.cpp raises a custom eVB error. It takes two parameters, an error number, and a description string. First, I create a custom HRESULT for the error number using the aforementioned technique. Then, I invoke ATL's CComCoClass::Error method, which caches the description string so eVB can retrieve it later using the GetErrorInfo API. Finally, I return the result to eVB. The eVB application can capture the error by placing the On Error Resume Next statement before the method call and examining Err.Number and Err.Description afterwards.

Ambient Properties

An "ambient property" is a property that eVB exposes to your control. This is the reverse of regular properties, which your control exposes to eVB. Table 2 shows some of the ambient properties exposed by eVB.

The Sample control uses three ambient properties. UserMode specifies whether the control is running at design time or run time. DisplayName is the control's name, for instance, "Sample1." SIPBehavior involves the Pocket PC user-interface guidelines. When SIPBehavior is set to vbSIPAutomatic, controls that accept keyboard input are expected to display the Soft Input Panel (the virtual keyboard) whenever they gain focus, and hide the SIP whenever they lose focus. The TextBox, the ComboBox, and the TreeView are examples of such controls.

To access these properties, I override two virtual methods. When the control is first created, eVB calls IOleObject::SetClientSite to give the control the COM interface pointer for accessing ambient properties. ATL stores this pointer for us, so I call ATL's SetClientSite implementation first. Then I request the ambient properties.

Also, any time the value of an ambient property changes, eVB calls IOleControl::OnAmbientPropertyChanged. I intercept this call to redraw the control whenever its name changes.

Not all ambient properties are available all the time. DisplayName is only available at design time. And SIPBehavior is only available on a Pocket PC. It is very important to examine HRESULTs returned by these functions to know whether you have successfully read the property you are trying to access.

Controls that initialize themselves using ambient properties face an issue when they get reloaded. Which do they see first: eVB's ambient properties or their own persisted properties? In theory, there is supposed to be an OLE Miscellaneous Status bit, OLEMISC_SETCLIENTSITEFIRST, which allows a control to tell its clients which order it prefers.

Unfortunately, eVB does not respect the OLEMISC_SETCLIENTSITEFIRST bit. On the Pocket PC, eVB always provides a control with ambient properties before asking it to load any properties from the property bag. Older versions of eVB (such as the Handheld PC Pro) will not expose their ambient properties until after a control has loaded its properties from the property bag. Code defensively.

Debugging

Debugging the design-time version of an ActiveX control is no different from debugging any other DLL using Visual Studio. Use evb3.exe as the executable for the debug session. In the Additional DLLs category, fill in the path to the desktop DLL.

Debugging the run-time version of an ActiveX control requires that you build and download your eVB application to the Emulator in advance. The eVB IDE does this when you use the Execute command on the Run menu. You can also build a .vb file and copy it using Windows Explorer. Then switch to eVC and follow these steps:

  1. For the local executable, browse the Runtimes directory of the Pocket PC SDK and find PVBLoad.exe in the x86em subdirectory.

  2. For the remote executable, type "\windows\pvbload.exe."

  3. For the program arguments, specify the Windows CE path of the eVB application. This is set in the eVB Project Properties dialog, and by default, it is "\windows\start menu\project1 .vb." Figure 6 shows the correct entries assuming that you have not changed the default.

  4. In the Additional DLLs category, the local name is the full path to the DLL in the x86emdbg subdirectory of the eVC project. The remote name is just "Sample ActiveX control.dll" without any path information; see Figure 7.

When you start the debugger, eVC launches the eVB application in the Emulator. Notice that you do not start the application using the Embedded Visual Basic IDE; in fact, eVB does not even have to be running while you debug your DLL in eVC.

Conclusion

If you have followed the development of the Sample project, you should have a fully operational ActiveX control that can pass string and array data back and forth to eVB, access ambient properties, persist its own properties correctly, and raise arbitrary errors when it gets upset with the state of the union. You should be able to compile the control and debug it under both Windows NT and Windows CE. This control can easily serve as a template for your future Windows CE ActiveX control projects.

DDJ

Listing One

// Sample.h : Definition of CSample
#include "resource.h"       // main symbols
#include <atlctl.h>

class ATL_NO_VTABLE CSample :
    public CComObjectRootEx<CComMultiThreadModel>,
    public IDispatchImpl<ISample, &IID_ISample, 
                                  &LIBID_SAMPLEACTIVEXCONTROLLib>,
    public CComControl<CSample>,
    public IPersistStreamInitImpl<CSample>,
    public IOleControlImpl<CSample>,
    public IOleObjectImpl<CSample>,
    public IOleInPlaceActiveObjectImpl<CSample>,
    public IViewObjectExImpl<CSample>,
    public IOleInPlaceObjectWindowlessImpl<CSample>,
    public ISupportErrorInfo,
    public IConnectionPointContainerImpl<CSample>,
    public IPersistStorageImpl<CSample>,
    public ISpecifyPropertyPagesImpl<CSample>,
    public IQuickActivateImpl<CSample>,
    public IDataObjectImpl<CSample>,
    public IProvideClassInfo2Impl<&CLSID_Sample, &DIID__ISampleEvents,
                &LIBID_SAMPLEACTIVEXCONTROLLib>,
    public IPropertyNotifySinkCP<CSample>,
    public CComCoClass<CSample, &CLSID_Sample>,

    // Added to support property bag persistence:
    public IPersistPropertyBagImpl<CSample>
{
public:
    CSample()
    {
        m_bWindowOnly = TRUE;
        // Added to demonstrate using plain BSTR (as opposed to CComBSTR)
        // for string-valued property:
        #if USE_PLAIN_BSTR
            bstrSampleString = NULL;
        #endif
        // Added to demonstrate custom property persistence:
        plSampleArray = new long[1];
        plSampleArray[0] = 12345;
        countSampleArray = 1;
        // Added to demonstrate ambient properties:
        bAutomaticSIP = FALSE;
    }
    ~CSample()
    {
        // If we use CComBSTR, the destructor does this automatically...
        #if USE_PLAIN_BSTR
            if (bstrSampleString)
                 SysFreeString(bstrSampleString);
        #endif
        // Free up our array-valued property.
        if (plSampleArray)
            delete [] plSampleArray;
    }
DECLARE_REGISTRY_RESOURCEID(IDR_SAMPLE)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CSample)
    COM_INTERFACE_ENTRY(ISample)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IViewObjectEx)
    COM_INTERFACE_ENTRY(IViewObject2)
    COM_INTERFACE_ENTRY(IViewObject)
    COM_INTERFACE_ENTRY(IOleInPlaceObjectWindowless)
    COM_INTERFACE_ENTRY(IOleInPlaceObject)
    COM_INTERFACE_ENTRY2(IOleWindow, IOleInPlaceObjectWindowless)
    COM_INTERFACE_ENTRY(IOleInPlaceActiveObject)
    COM_INTERFACE_ENTRY(IOleControl)
    COM_INTERFACE_ENTRY(IOleObject)
    COM_INTERFACE_ENTRY(IPersistStreamInit)
    // added to support property bag persistence:
    COM_INTERFACE_ENTRY(IPersistPropertyBag)
    // changed to support property bag persistence:
    COM_INTERFACE_ENTRY2(IPersist, IPersistPropertyBag)
    COM_INTERFACE_ENTRY(ISupportErrorInfo)
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
    COM_INTERFACE_ENTRY(ISpecifyPropertyPages)
    COM_INTERFACE_ENTRY(IQuickActivate)
    COM_INTERFACE_ENTRY(IPersistStorage)
    COM_INTERFACE_ENTRY(IDataObject)
    COM_INTERFACE_ENTRY(IProvideClassInfo)
    COM_INTERFACE_ENTRY(IProvideClassInfo2)
END_COM_MAP()

BEGIN_PROP_MAP(CSample)
    PROP_DATA_ENTRY("_cx", m_sizeExtent.cx, VT_UI4)
    PROP_DATA_ENTRY("_cy", m_sizeExtent.cy, VT_UI4)
    // added to make IPersistPropertyBagImpl<> persist this property:
    PROP_ENTRY("SampleString", // property name
                1, // dispid
                CLSID_NULL) // no custom property page
END_PROP_MAP()

BEGIN_CONNECTION_POINT_MAP(CSample)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
END_CONNECTION_POINT_MAP()

BEGIN_MSG_MAP(CSample)
    CHAIN_MSG_MAP(CComControl<CSample>)
    DEFAULT_REFLECTION_HANDLER()
END_MSG_MAP()
// ISupportsErrorInfo
    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid)
    {
        if (InlineIsEqualGUID(IID_ISample, riid))
            return S_OK;
        else
            return S_FALSE;
    }
// IViewObjectEx
    DECLARE_VIEW_STATUS(VIEWSTATUS_SOLIDBKGND | VIEWSTATUS_OPAQUE)
// Drawing:
public:
    HRESULT OnDraw(ATL_DRAWINFO& di);
// Added to demonstrate string-valued properties:
public:
    STDMETHOD(get_SampleString)(BSTR *pVal);
    STDMETHOD(put_SampleString)(BSTR newVal);
private:
    #if USE_PLAIN_BSTR
        BSTR bstrSampleString;
    #else
        CComBSTR bstrSampleString;
    #endif
// Added to demonstrate array-valued properties:
public:
    STDMETHOD(get_SampleArray)(VARIANT *pVal);
    STDMETHOD(put_SampleArray)(VARIANT newVal);
private:
    long * plSampleArray;
    long countSampleArray;
// Added to demonstrate custom property bag persistence:
public:
    STDMETHOD(Save)(LPPROPERTYBAG pPropBag, BOOL fClearDirty,
                    BOOL fSaveAllProperties);
    STDMETHOD(Load)(LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog);
// Added to demonstrate raising custom errors:
public:
    STDMETHOD(RaiseError)(long number, BSTR message);
// Added to demonstrate using ambient properties:
public:
    STDMETHOD(OnAmbientPropertyChange)(DISPID dispid);
    STDMETHOD(SetClientSite)(IOleClientSite *pClientSite);
private:
    BOOL bAutomaticSIP;
    CComBSTR bstrDisplayName;
};

Back to Article

Listing Two

// Sample.cpp : Implementation of CSample
#include "stdafx.h"
#include "Sample ActiveX Control.h"
#include "Sample.h"

// Implementation of string-valued property, SampleString:
#if USE_PLAIN_BSTR // bstrSampleString is a plain BSTR, not a CComBSTR.
STDMETHODIMP CSample::get_SampleString(BSTR *pVal)
{

    if (IsBadWritePtr(pVal, sizeof(*pVal)))
        return E_POINTER;
    *pVal = ::SysAllocString(bstrSampleString);
    return S_OK;
}
STDMETHODIMP CSample::put_SampleString(BSTR newVal)
{
    if (bstrSampleString)
        ::SysFreeString(bstrSampleString);
    bstrSampleString = ::SysAllocString(newVal);
    return S_OK;
}
#else // bstrSampleString is a CComBSTR smart string.
STDMETHODIMP CSample::get_SampleString(BSTR *pVal)
{
    if (IsBadWritePtr(pVal, sizeof(*pVal)))
        return E_POINTER;
    *pVal = bstrSampleString.Copy();
    return S_OK;
}
STDMETHODIMP CSample::put_SampleString(BSTR newVal)
{
    bstrSampleString = newVal; // See CComBSTR::operator=
    return S_OK;
}
#endif

// Drawing.  Notice conversion of bstrDisplayName for Windows 98.
HRESULT CSample::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);
    USES_CONVERSION;
    LPCTSTR pszText = OLE2T(bstrDisplayName);
    DrawText(di.hdcDraw, pszText, -1, &rc, 
                  DT_CENTER | DT_VCENTER | DT_SINGLELINE);
    return S_OK;
}
// Implementation of array-valued property, SampleArray:
STDMETHODIMP CSample::get_SampleArray(VARIANT *pVal)
{
    // Make sure pVal points to a valid variant.
    if (IsBadWritePtr(pVal, sizeof(*pVal)))
        return E_POINTER;
    // Make sure we actually have an array to return.
    if (plSampleArray == NULL)
        return E_FAIL;
    // Allocate a SafeArray of the same size as our C++ array.
    SAFEARRAY * safeArray;
    SAFEARRAYBOUND arrayBounds;

    arrayBounds.lLbound = 0; // lower bound
    arrayBounds.cElements = countSampleArray; // element count

    safeArray = SafeArrayCreate(VT_VARIANT, 1, &arrayBounds);
    if (safeArray == NULL)
        return E_OUTOFMEMORY;
    // Copy elements from C++ array to SafeArray.
    for (long i = 0; i < countSampleArray; i++)
    {
        CComVariant var(plSampleArray[i]);
        SafeArrayPutElement(safeArray, &i, &var);
    }
    // Package the SafeArray into the variant.
    pVal->vt = VT_VARIANT | VT_ARRAY;
    pVal->parray = safeArray;
    return S_OK;
}
STDMETHODIMP CSample::put_SampleArray(VARIANT newVal)
{
    SAFEARRAY * safeArray;
    // Our single variant parameter points to a SafeArray of variants.
    if (newVal.vt == (VT_ARRAY | VT_VARIANT | VT_BYREF))
        safeArray = *(newVal.pparray);
    else
        return E_UNEXPECTED;
    // Find out how many elements the SafeArray contains.
    long lowerBound, upperBound;
    SafeArrayGetLBound(safeArray, 1, &lowerBound);
    SafeArrayGetUBound(safeArray, 1, &upperBound);
    countSampleArray = upperBound - lowerBound + 1;
    // Allocate a C++ array of the same size as the SafeArray.
    if (plSampleArray)
        delete [] plSampleArray;
    plSampleArray = new long[countSampleArray];
    if (plSampleArray == NULL)
        return E_OUTOFMEMORY;
    // Copy elements from the SafeArray to the C++ array.
    for (long i = lowerBound; i <= upperBound ; i++)
    {
        CComVariant var; // smart wrapper class for VARIANT
        SafeArrayGetElement(safeArray, &i, &var);
        if (FAILED(var.ChangeType(VT_I4))) // VT_I4 is "long"
            return DISP_E_TYPEMISMATCH;
        plSampleArray[i - lowerBound] = var.lVal;
    }
    return S_OK;
}
// This will persist our array-valued property to a property bag.
STDMETHODIMP CSample::Save(LPPROPERTYBAG pPropBag, BOOL fClearDirty,
                           BOOL fSaveAllProperties)
{
    if (countSampleArray > 0)
    {
        // Allocate a SafeArray that is one larger than our C++ array.
        SAFEARRAY * safeArray;
        SAFEARRAYBOUND arrayBounds;

        arrayBounds.lLbound = 0; // lower bound
        arrayBounds.cElements = countSampleArray + 1; // element count

        safeArray = SafeArrayCreate(VT_I4, 1, &arrayBounds);
        if (safeArray == NULL)
            return E_OUTOFMEMORY;

        // Store the size of our C++ array, in bytes, in the first element
        // of the SafeArray.
        long i = 0;
        long bytes = countSampleArray * sizeof(long);
        SafeArrayPutElement(safeArray, &i, &bytes);
        // Copy the array elements.  Notice plSampleArray[0] goes into
        // SafeArray element 1, and so on.
        for (i = 1; i <= countSampleArray; i++)
            SafeArrayPutElement(safeArray, &i, plSampleArray + i - 1);
        // Package the array into the variant.
        VARIANT var;
        var.vt = VT_BLOB;
        // Get a pointer to the data inside the SafeArray.
        // This data is in the format of a "blob."
        SafeArrayAccessData(safeArray, &var.byref);
        // Try writing the blob to the property bag.
        HRESULT hr = pPropBag->Write(OLESTR("SampleArray"), &var);
        // Release the pointer.
        SafeArrayUnaccessData(safeArray);
        // If the blob didn't work, try writing the SafeArray instead.
        if (E_INVALIDARG == hr)
        {
            var.vt = VT_ARRAY | VT_I4;
            var.parray = safeArray;

            pPropBag->Write(OLESTR("SampleArray"), &var);
        }
        // Release the SafeArray.
        SafeArrayDestroy(safeArray);
    }
    // Let ATL handle the other properties.
    return IPersistPropertyBagImpl<CSample>::
                Save(pPropBag, fClearDirty, fSaveAllProperties);
}
// This will retrieve our array-valued property from a property bag.
STDMETHODIMP CSample::Load(LPPROPERTYBAG pPropBag, LPERRORLOG pErrorLog)
{
    CComVariant var;
    long i;
    SAFEARRAY * safeArray;
    HRESULT hr = pPropBag->Read(OLESTR("SampleArray"), &var, pErrorLog);
    if (SUCCEEDED(hr))
    {
        // The variant will either be a SafeArray of longs or a blob.
        switch (var.vt)
        {
        case (VT_ARRAY | VT_I4):
            safeArray = var.parray;
            SafeArrayGetUBound(safeArray, 1, &countSampleArray);
            // Allocate our own array of longs.
            if (plSampleArray)
                delete [] plSampleArray;
            plSampleArray = new long[countSampleArray];
            if (plSampleArray == NULL)
                return E_OUTOFMEMORY;
            // Copy the array.
            for (i = 1; i <= countSampleArray; i++)
                SafeArrayGetElement(safeArray, &i, plSampleArray + i - 1);
            break;
        case VT_BLOB:
            countSampleArray = var.plVal[0] / sizeof(long);
            // Allocate our own array of longs.
            if (plSampleArray)
                delete [] plSampleArray;
            plSampleArray = new long[countSampleArray];
            if (plSampleArray == NULL)
                return E_OUTOFMEMORY;
            // Copy the array.
            for (i = 0; i < countSampleArray; i++)
                plSampleArray[i] = var.plVal[i + 1];
            break;
        default:
            return DISP_E_TYPEMISMATCH;
        }
    }
    // Let ATL handle the other properties.
    return IPersistPropertyBagImpl<CSample>::Load(pPropBag, pErrorLog);
}
// This will raise an arbitrary eVB error.
// Err.Number will be set to number and Err.Description will be set to message.
STDMETHODIMP CSample::RaiseError(long number, BSTR message)
{
    HRESULT hr = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_CONTROL, number);
    return Error(message, IID_ISample, hr);
}
// This lets us know when we can start requesting ambient properties.
STDMETHODIMP CSample::SetClientSite(IOleClientSite *pClientSite)
{
    // Call base class method that we are overriding.
    // This sets up m_spAmbientDispatch.
    HRESULT hr = IOleObjectImpl<CSample>::SetClientSite(pClientSite);
    if (SUCCEEDED(hr)
        && pClientSite) // pClientSite may be NULL if we are shutting down.
    {
        // If we are running in design mode, get our name
        BOOL bRuntime = TRUE;
        GetAmbientUserMode(bRuntime);
        if (!bRuntime)
        {
            GetAmbientDisplayName(bstrDisplayName.m_str);
                }
        // If we are running on a Pocket PC, test for automatic SIP behavior.
        CComVariant var;
        if (SUCCEEDED( // will only succeed on a Pocket PC
                m_spAmbientDispatch.GetPropertyByName(L"SIPBehavior", & var)))
        {
            var.ChangeType(VT_I4);
            if (1 /* vbSIPAutomatic */ == var.lVal)
                bAutomaticSIP = TRUE;
        }
    }
    return hr;
}
// This lets us know when ambient properties have changed.
STDMETHODIMP CSample::OnAmbientPropertyChange(DISPID dispid)
{
    // Call base class method that we are overriding.
    HRESULT hr = IOleControlImpl<CSample>::OnAmbientPropertyChange(dispid);
    // If we are changing our name, then request a redraw.
    if (DISPID_AMBIENT_DISPLAYNAME == dispid || DISPID_UNKNOWN == dispid)
    {
        GetAmbientDisplayName(bstrDisplayName.m_str);
        FireViewChange();
    }
    return hr;
}







Back to Article


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.