IDispatch & Automation for COM Components

Using features of the IDispatch, Arnaud creates a component that sets pixel colors in a bitmap, writes the bitmap into a stream, and stores the picture in a BMP file.


January 01, 2002
URL:http://drdobbs.com/idispatch-automation-for-com-components/184416392

January 2002/IDispatch & Automation for COM Components

Using Visual C to create COM components for VB (Visual Basic) or VBScript development is a common task. Still, many C developers don’t understand how VB programmers code efficiently when using standard Microsoft components. For example, VB developers often use For Each instructions or optional parameter syntaxes that some C developers don’t provide with their components. In this article, I’ll describe some common techniques that make it easier to use components for non-C developers.

For the purposes of example, I’ll create a component that enables 16-color bitmap management by setting pixel colors in the bitmap, writing the bitmap into a stream (for example, as in an ASP Response object), storing the picture in a BMP file, and getting a collection of all the pixels in the file. The complete source code that implements the sample classes discussed in this article is available online.

Enum Types

The basic object I’ll use is the CPixel class (see Listing 1), which defines a pixel of some color in a bitmap at some coordinates using three properties: Row, Column, and Color. The Color property for the pixel could be one value between 0 and 15 for each of the 16 colors (the standard 4-bit Windows palette). Because it is difficult to remember the meaning of each color index, I define some constants for each color. The best way to do this is to create an enum type in your IDL file instead of creating it in your header file. Doing this makes the constants to be stored in the component’s type library visible in the VB Object Browser and IDE. The enumerator is also available under C in the header file generated by MIDL. Furthermore, if you define the Color property as returning a value declared with this enum type, the VB IDE automatically displays the list of correct values for the property (see Figure 1).

Enums are not compatible with scripting environments such as VBScript or Jscript — only environments that can read type libraries (VBA and VB, for example). You can, however, use enum typed properties in scripts by setting the corresponding integer values to the constant name.

Default Properties

Color is the most important property of the Pixel class. It therefore may be useful to declare it as a default property, letting VB developers omit typing the property name. You only have to change the DISPID of the property in your IDL file to DISPID_VALUE in order to make it become a default property.

[propput, id(DISPID_VALUE), helpstring("Pixel color")]
HRESULT Color([in] baseColors newVal);

In fact, DISPID values are usually used by scripting engines to invoke methods on objects based on the result of search-by-method names. They usually get them using the IDispatch interface, which is implemented by the ATL’s IDispatchImpl template.

Now Color is the Pixel class’s default property, myPixel.Color = Red and myPixel = Red are both valid and do the same thing.

Error Management

Once the Pixel object supports the Color property “in a VB way,” you should ensure that the new color passed in parameter is valid (that is, part of the 16 base colors).

Reporting errors in C is usually made using exceptions or special return values. COM uses HRESULT as a return value allowing component developers to return some status information on the completion of an invoked method. However, returning rich error information in a 32-bit value is difficult. This is why COM can associate an Error object supporting the IErrorInfo interface to each method call. This interface fully supports marshalling, and an error generated on a remote server can therefore be returned on the client. IErrorInfo presents the following five methods:

ATL provides a straightforward way to create objects that implement IErrorInfo using the strongly overloaded AtlReportError function. You can then associate a string to your error and optionally specify a help file context.

return AtlReportError(CLSID_Pixel, IDS_BAD_COLOR,
    IID_IPixel, 0, _Module.GetModuleInstance());

Once an ErrorInfo object is associated with the context, you then only need to return a DISP_E_EXCEPTION (returned by default by AtlReportError) to generate an exception in client scripting code. The client can then catch the exception and get information on the error by invoking the Err global object (see Example 1).

You can either set the error description string using a string pointer or a string table resource identifier. However, resource identifiers should lie between 0x200 and 0xFFFF, inclusively.

Once the pixel object is fully coded, you create a new class named Bitmap that manages the picture.

Boolean Type

The windows.h header file defines a type named BOOL that is in fact an integer value usually set to TRUE (1) or FALSE (0). VB or VBScript can also manage Booleans, but they do not allow them to be set to any value other than True or False. In VB, True equals -1 (not 1, like BOOL).

The VB Boolean type is encoded in C as a VARIANT_BOOL, and it can take the VARIANT_TRUE (-1) or VARIANT_FALSE (0) value. Defining some properties or parameters in an IDL file as VARIANT_BOOL instead of a simple BOOL results in a change on the VB/VBA IDE while coding. It displays the same list box as it does for the enum types and therefore helps prevent you from using values other than -1 and 0.

The CBitmap class has a property of this type named AutoSize set to True by default and specifying that if a pixel is set outside of the bitmap, the picture should be automatically resized.

VARIANT Arrays

The VARIANT automation type can encapsulate almost any kind of data including arrays. Variant arrays are just like C arrays that could be set to a new dimension (redim). The vt member of such VARIANTs is equal to the type of the value in the array added to VT_ARRAY. Be advised that variant arrays are always passed by reference in parameters and that you should use arrays of VT_VARIANT (for instance, vt = VT_VARIANT | VT_ARRAY) type to work with script environments.

The bitmap class in Example 2 has a method named SetPoints that lets you define an array of pixel objects in the bitmap. I simply get a pointer to the SAFEARRAY from the referenced VARIANT and use the SafeArrayGetLBound and SafeArrayGetUBound functions to get the lower and upper bound of the array. A lower-bound value is required because of the fact that VB allows arrays not to start at index 0.

Once the array size is known, you need to call SafeArrayGetElement to copy the VARIANT structure of each item of the array and analyze it.

Collections

Collections are container objects that can enumerate child objects via the IEnumVARIANT interface. Just like for default properties, the way to present a collection is to use a special DISPID named DISPID_NEWENUM for a get property named _NewEnum. This property returns an IEnumVARIANT interface that enumerates the child items.

The bitmap object has a Pixels property returning an IPixels interface implementing this _NewEnum property. _NewEnum sends back to the client the pixels collection for each pixel of the current bitmap.

Just like for IErrorInfo, ATL provides a feature that lets you avoid implementing the IEnumVARIANT interface (see Example 3). This can be accomplished by using the CComEnum class template.

Each time a collection is requested by calling get__NewEnum, the function creates a CComEnum templatized for IEnumVARIANT. It then initializes the collection by calling Init and specifying a pointer to the beginning and another one to the end of the VT_DISPATCH array pointing to each of my created pixels in the IDispatch interface. The AtlFlagCopy tells CComEnum to make its own local copy of the VARIANT. (You could also have specified AtlFlagTakeOwnership.)

Collection objects usually implement properties like Count to return the number of child items in the collection. ATL provides some other automatic mechanisms to code this property not described here.

Now that you have a pixels collection on the pixels property of the bitmap, VB developers can use the For Each syntax to query each pixel of the bitmap (see Example 4).

IStream Support

Supporting Stream parameters can be useful for components used in ASP where you should write to the Response object (note that only the IIS 5.0 Response object supports IStream). A stream is a way to read and/or write data at some place whether it goes on a hard disk, a network connection, or anything else.

The IIS Response object is a stream object that you can write text or binary data to. Therefore, you can implement a function that lets you write a bitmap file into the ASP Response object to create dynamic bitmaps (for example, to create real-time analysis graphics).

Example 5 shows calling Write on an IStream interface to generate a dynamic page using ASP. The form in test.asp posts a list of pixels to colorize to GenBmp.asp that uses the Bitmap component and its Save method to generate the picture (see Figure 2).

Optional Parameters

MIDL supports a special attribute named [optional] that only can be applied on VARIANT-typed parameters. Setting this attribute results in a parameter VB developers can omit. For example, the SaveToFile method of CBitmap takes two optional parameters: the first one setting the filename where to write the bitmap and the second indicating a parent window handle if no filename is specified showing the Save As dialog box.

[id(6), helpstring(
 "Save bitmap to a file on a partition")]
HRESULT SaveToFile([in, optional] 
                      VARIANT vbstrFileName,
                   [in, optional] 
                      VARIANT 
                      vhWndParentWindow);

If no parameter is specified under VB, the VARIANT can then take two different vt values:

Optional parameters define default behaviors for omitted parameters and often simplify VB code readability.

if (vbstrFileName.vt == VT_EMPTY ||
   (vbstrFileName.vt == VT_ERROR && 
                 vbstrFileName.scode ==
                 DISP_E_PARAMNOTFOUND) )

Another simpler but more restricted method can be used using the defaultvalue attribute in your IDL file. The value specified in the parameter to this attribute represents the value that will be passed to the function if the parameter has been bypassed. This may be inconvenient at times, but one advantage of this technique is that the defaultvalue will be displayed by the IDE of the component consumer. For example, to declare a method that adds one and two by default, the IDL should specify the following:

[id(7), helpstring(
    "Adds two numbers (1+2) by default")]
HRESULT SaveToFile(
 [in, defaultvalue(1)] lone lFirstVal,
 [in, defaultvalue(2)] long lSecondVal,
 [out, retval] long* pRetVal);

Conclusion

The C++-based Bitmap component that creates the picture uses some IDispatch features to make development for VB or ASP developers a lot easier. You can view a running demonstration of the GenBmp.asp image generator online at www.arnaudaubert.com, under the Articles section.


Arnaud Aubert is the R&D manager of IS Decisions. He can be reached at [email protected] or http://www.arnaudaubert.com.

January 2002/IDispatch & Automation for COM Components

Figure 1: Automatically displaying the list of correct values

January 2002/IDispatch & Automation for COM Components

Figure 2: Image generated by GenBmp.asp

January 2002/IDispatch & Automation for COM Components

Listing 1: Pixel.cpp
Implementation of CPixel

// Pixel.cpp : Implementation of CPixel
#include "stdafx.h"
#include "Picture.h"
#include "Pixel.h"

/////////////////////////////////////////////////////////////////////////////
// CPixel


STDMETHODIMP CPixel::get_Color(baseColors *pVal)
{
    *pVal = m_color;
    return S_OK;
}

STDMETHODIMP CPixel::put_Color(baseColors newVal)
{
    HRESULT hr = S_OK;

    // Test if the new color is valid or not
    if (newVal < 0 || newVal > 15)
        return AtlReportError(CLSID_Pixel, IDS_BAD_COLOR, 
                    IID_IPixel, 0, _Module.GetModuleInstance());

    if (SUCCEEDED(hr = IsReadOnly()))
        m_color = newVal;
    return hr;
}

STDMETHODIMP CPixel::get_Row(long *pVal)
{
    *pVal = m_row;
    return S_OK;
}

STDMETHODIMP CPixel::put_Row(long newVal)
{
    HRESULT hr = S_OK;
    if (SUCCEEDED(hr = IsReadOnly()))
        m_row = newVal;
    return hr;
}

STDMETHODIMP CPixel::get_Column(long *pVal)
{
    *pVal = m_col;
    return S_OK;
}

STDMETHODIMP CPixel::put_Column(long newVal)
{
    HRESULT hr = S_OK;
    if (SUCCEEDED(hr = IsReadOnly()))
        m_col = newVal;
    return hr;
}

HRESULT CPixel::IsReadOnly()
{
    // Test if this pixel is in readonly mode
    if (m_bReadOnly)
        return AtlReportError(CLSID_Pixel, IDS_READONLY_PIXEL,
            IID_IPixel, 0, _Module.GetModuleInstance());
    else 
        return S_OK;
}
//End of File

January 2002/IDispatch & Automation for COM Components

Example 1:
Getting information on an error

Private Sub TestIErrorInfo()
' The following line asks VB to catch the error by going

' to the CatchError label
On Error GoTo CatchError

Dim mypix As New pixel
mypix.Color = 17
Exit Sub

CatchError:
' Now an exception occured, we can get information 
' about it using the Err object
MsgBox "The following error occured:" & Err.Description
Exit Sub
End Sub

January 2002/IDispatch & Automation for COM Components

Example 2:
Defining an array of pixel objects in the bitmap

Dim pPixels(100)
Set pPicture = CreateObject("Picture.Bitmap")
pPicture.Width = 100
pPicture.Height = 100
For i = 0 to 99
    Set pPixels(i) = CreateObject("Picture.Pixel")
    pPixels(i).Row = i
    pPixels(i).Column = i
    pPixels(i).Color = 2
Next
pPicture.SetPoints pPixels

STDMETHODIMP CBitmap::SetPoints(VARIANT vPoints)
{
    // Read a set of points defined in vPoints to apply them to 
    // the current picture
    if (!(vPoints.vt & VT_VARIANT) || 
        !(vPoints.vt & VT_BYREF)) return E_INVALIDARG;
    // Get the low and upper bound of the array
    HRESULT hr = S_OK;
    SAFEARRAY* pArray = *vPoints.pvarVal->pparray;
    LONG lBound, uBound;
    if (FAILED(SafeArrayGetLBound(pArray, 1, &lBound)) ||
        FAILED(SafeArrayGetUBound(pArray, 1, &uBound)))
            return E_INVALIDARG;
    // And set the point for each pixel of the array
    for (long iPixel = lBound; iPixel < uBound; iPixel++)
    {
        // Get the Pixel object
        VARIANT vPixel;
        SafeArrayGetElement(pArray, &iPixel, &vPixel);
        // Set the point by forwarding the object to SetPoint
        if (vPixel.vt == VT_DISPATCH || vPixel.vt == VT_UNKNOWN)
        {
            CComQIPtr<IPixel> pPixel = vPixel.pdispVal;
            if (pPixel.p && FAILED(hr = SetPoint(pPixel))) break;
        }
    }
    return hr;

}

January 2002/IDispatch & Automation for COM Components

Example 3:
Avoiding the IEnumVARIANT interface

typedef CComEnum<IEnumVARIANT, &IID_IEnumVARIANT, VARIANT,
                        _Copy<VARIANT> > VarArrEnum;
STDMETHODIMP CPixels::get__NewEnum(IUnknown **pVal)
{
    // Create an array of VT_DISPATCH for each pixel
    HRESULT hr;
    long height = m_pBitmap->m_height;
    long width = m_pBitmap->m_width;
    long iVariant = 0;
    
    VARIANT* pVars = new VARIANT[width*height];
    if (pVars == NULL) return E_OUTOFMEMORY;

    // Create a pixel in a VT_DISPATCH for each pixel of the array
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            // Create this pixel object
            VariantInit(&pVars[iVariant]);
            pVars[iVariant].vt = VT_DISPATCH;
            hr = get_Pixel(x, y, 
                (IPixel**) &pVars[iVariant++].pdispVal);
            // Abort creation if anything failed
            if (FAILED(hr))
            {
                delete pVars;
                return hr;
            }
        }
    }
    // Create a pixel for each bitmap and add it to collection
    CComObject<VarArrEnum>* pPixelsColl;
    hr = CComObject<VarArrEnum>::CreateInstance(&pPixelsColl);
    if (SUCCEEDED(hr))
    {
        if (SUCCEEDED(hr = pPixelsColl->Init(
            pVars, pVars + (width*height), NULL, AtlFlagCopy)))
        {
            hr = pPixelsColl->QueryInterface(pVal);
        }
    }
    // Free our local copy of the VARIANT array
    delete pVars;
    return hr;
}

January 2002/IDispatch & Automation for COM Components

Example 4:
Using the For Each syntax to query each pixel of the bitmap

' Display each bitmap color
For Each pix In mybmp.Pixels
    MsgBox "(" & pix.Column & "," & pix.Row & ")=" & pix
Next

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.