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

.NET

Transparent ATL Controls


Dr. Dobb's Journal March 1998: Transparent ATL Controls

Add excitement to your web site with this ActiveX control

Tom is the author of The Active Template Library: A Developer's Guide (M&T Books, 1997). He can be contacted at [email protected]. Mark is the author of The C++ Programmer's Guide to the Standard Template Library (IDG Books, 1995). He can be contacted at [email protected].


Displaying transparent GIF files on web sites using standard browsers has become a popular activity. Unfortunately, Windows 95/NT doesn't provide much native support for the display of images with transparent components. In this article, we'll use Microsoft's Active Template Library (ATL) to present an ActiveX control that displays a bitmap with a single transparent color. The control can be used with Internet Explorer, Netscape Navigator (with the ScriptActive plug-in), Visual Basic, and most other ActiveX containers.

When referring to bitmapped images, "transparency" refers to an image that has an arbitrary number of transparent areas. When the image is drawn, the transparent areas don't obscure the area behind the image in the z-order. In the simplest case, you've seen this used on the Web, producing images of complex objects (like Figure 1) that appear to be floating on top of a background.

Sites like Figure 1 typically have a standard background pattern that shows up on many or all of their pages. The GIF on the left side has navigation bars that are used in an image map. A GIF file with the site logo is on the right. Both images feature transparent areas that let them blend in with the background.

How do They do That?

After seeing this effect on the Web, we set to the task of duplicating it in an ActiveX control. We discovered a couple of different ways to implement transparency in a control; the most straightforward requires you to set up a windows HRGN object, used to define a nonrectangular drawing area. Figure 2 is an example of a drawing region. As long as you can define your region as a path connecting a series of points (or elliptical regions), you can create a drawing region that Windows understands.

Once you have a drawing region defined, transparency is easy. In the simplest case, you take advantage of the Windows 95 ability to define a nonrectangular window. That done, you draw the window as you normally would. The areas outside the region are drawn by whatever resides behind the window.

Defining a drawing region works well with containers that implement Microsoft's OCX 96 specification (which ships with the ActiveX SDK). In the best case, you can implement two-pass drawing, which lets you draw the foreground of your control first, then lets the container draw the background. This provides fast, flicker-free drawing of controls.

Ointment Ready, Enter Fly

Defining drawing regions is great for some applications, but might not be a general-purpose technique. We wanted to be able to draw any type of image with randomly configured transparent areas. This means having transparency controlled on a pixel-by-pixel basis. Attempting to define a region for any arbitrary bitmap is simply asking for a headache.

For our control, we used a masking technique described by Ron Gery in an MSDN article ("TRANSBLT Demonstrates Bitmaps with Transparency," 2/15/96 ID: Q97365) that uses a simple masking technique, making it trivial to use arbitrarily complex transparent regions. The Gery algorithm assumes that you have a bitmap with a single color that defines the transparent area. Given that, the drawing portion of your code needs to execute the following steps:

  1. Create a monochrome bitmap of the same size as the bitmap you are going to draw.
  2. Set all the transparent pixels in the monochrome bitmap to 1, and all the opaque pixels to 0.
  3. XOR the screen region with the bits in your image bitmap.
  4. AND the screen with your monochrome bitmap. This leaves all the transparent areas unchanged, and sets the opaque areas to black.
  5. XOR the screen region with the image bits again. This sets all the transparent areas back to their original color, since the two XOR operations cancel one another. The opaque areas now contain the desired image bits, since XORing with solid black is the same as simply setting the bits.

This drawing algorithm is simple to implement using standard Windows raster operations.

Background Checks

While this drawing scheme works with containers that comply to OCX 96 recommendations, it does make an important assumption: The XOR/Mask sequence preserves the background behind transparent areas, but only if the background has already been drawn.

To programs like Internet Explorer 3.0 (IE3), the ActiveX control is a child window responsible for drawing its entire rectangular area. When IE3 is drawing the background for a web page, it excludes the areas occupied by child windows, allowing them to draw their own background. If you've set up a background GIF, IE3 won't bother to draw it in any areas covered by your control, making any attempt at transparent drawing doomed from the start.

Microsoft documented a way around this problem in Knowledgebase article Q165073, which provides a code snippet you can drop into a control's WM_ERASEBKGND handler. Listing One is MFC code similar to that presented in the Knowledgebase article. This code sends a WM_ERASEBKGND message to Internet Explorer, along with the device context for the ActiveX control. This convinces Internet Explorer to draw the background behind the control as if it didn't belong to a child window. This code does not cause any trouble for more sophisticated containers (such as IE4) because the WM_ERASEBKGND never gets sent to controls. Newer containers that implement the OCX 96 specification don't create child windows for each embedded control. Instead, each control renders itself directly on the container's device context.

Putting it Together with ATL

We created an ActiveX control called TransCtl that displays an eight-bit BMP file, and treats all solid white areas (RGB(255,255,255)) as transparent. To keep things simple, the control doesn't do any palette management, so if you use it on 256-color displays, your bitmap should only use the 20 system colors.

We created this control using Visual C++ 5.0 and the ATL. Programming with ATL is somewhat more difficult than programming with MFC, but the resulting ActiveX controls are usually faster, smaller, and can eliminate dependencies on corpulent DLLs.

With command-line compilers, we could have just included a single C file that held all the source code for this project. Things aren't so easy with IDEs that feature Wizard-based development. The instructions for building this project with Visual C++ look more like a recipe in a cookbook, with an interesting mixture of dialog boxes, button presses, menu options, and code snippets.

To build an ATL project using Visual C++, select File|New|Project|ATL COM AppWizard. The dialog box asks you for a Project Name, which we set to TransparentControl; and Location, which we filled in with C:\. We selected the default values on the next dialog, creating a DLL without MFC support, and no merging of proxy/stub code. See Figure 3.

At this point, we need to create the actual ActiveX control that will display our BMP files. To do this, we used Insert|New ATL Object, which brought up the ATL Object Wizard. We selected Controls|Full Control and clicked the Next button, which brought up a tabbed properties dialog. In the edit box that asks for a short name, we entered TransCtl. In the Miscellaneous sheet of the properties dialog, we turned off the Opaque option.

The Stock Properties tab lets you specify which stock properties your control will support. Our control needs the ReadyState stock property, which is supported by ATL, but is absent from the Stock Properties tab. To include it in our control's implementation, you have to add the code yourself.

Clicking the OK button at this point generates all the files necessary to create the basic ActiveX control. You can build the control and insert it into Microsoft's ActiveX Control Test Container (accessible from the Tools menu item). But before it can do anything interesting, you need to add the code that assigns an image file to the control, loads the image file into memory, and draws it on the screen.

Dealing with the Image File

Since TransCtl is to display an image file, we need a property that gives the name of the file. Adding properties to an ATL project is another Wizard-driven process. In the ClassView tab of the Workspace window (Figure 4), you should have two classes defined: CTransCtl, and ITransCtl. Right-click on the ITransCtl item in the tree, and select Add Property. Choose a property type of BSTR, which is the canonical string type used in ActiveX controls. Choose a property name of ImageFile, and use the default get and put functions the Wizard suggests.

While the VC++ Wizards help you by adding the properties to the control, they don't actually supply the internal implementation of the property. This is done manually. First, you add a new member variable called m_bstrImageFile, of type CComBSTR to class CTransCtl in file TransCtl.h. (available electronically; see "Resource Center," page 3). Next, you initialize it with an empty string in the CTransCtl constructor. Finally, you implement the get_ImageFile() and put_ImageFile() functions in TransCtl.cpp (also available electronically).

The put_ImageFile() function in TransCtl.cpp has to do more than just copy a string into your member variable. It clears our internal bitmap that actually holds the image, starts the download process, and informs users that the control is in an incomplete state.

Asynchronous Properties

Until recently, a control's property values (such as the bits of an image) were stored within a file managed by its container. As the control is instantiated within the container, its property values are provided by the container via COM-based interfaces. This process of binding a control's property values is a synchronous operation. The container opens its storage file, locates the control's persistent data, queries for the control's persistence interface (usually IPersistStreamInit or IPersistPropertyBag), then calls IPersist*::Load, whereby the control sets its property values.

This works fine for most control properties (such as background color and fonts) because the data is small. However, large property values (like the bits of an image) in low-bandwidth environments are a problem.

In early 1996, Microsoft developed a COM- based technology called "Asynchronous Moniker" to solve the synchronous-binding problem. Microsoft also provided an implementation of the specification called an "URL Moniker."

Monikers are used to name specific instances of COM classes. A moniker is a COM object that implements the IMoniker interface and encapsulates the details of instantiating a particular class instance. The moniker obscures from the client the process of locating, instantiating, and initializing a specific COM class. In other words, clients work through the standard IMoniker interface or the MkParseDisplayNameEx API and can ignore class-specific details.

Monikers are typically used to bind to a specific instance of a COM object. They can also be used to bind to a remote storage or stream. Microsoft's URL moniker implementation lets a client application asynchronously bind to a web resource specified by a URL. The client application, by implementing the IBindStatusCallback interface, treats the resource as a stream of bytes (via an IStream pointer). This means a client can download a remote file asynchronously by specifying only its URL, which is exactly what we need for our ImageFile property implementation.

The ImageFile property then becomes a string that holds just the URL for our bitmap file. The persistent data for our ImageFile property is no longer the bits of the image, but an embedded reference to them. To see how this looks in a web page, examine the ImageFile property in Listing Two.

Loading the image proceeds like this: As the control is instantiated by the container, it passes the image's URL as part of the synchronous property binding process. Once we have the URL, we initiate the asynchronous download process. The download occurs in a background thread, and when it is complete, we render the transparent control using our image data.

If you look closely at TransCtl.cpp, you'll notice that instead of using ATL's CBindStatusCallback class, we developed and used a derived class named COurBindStatusCallback. The CBindStatusCallback implementation provided with ATL 2.1 isn't quite ready for prime time. We tweaked a few of its methods to get everything working properly. You can examine the minor changes by downloading the code. The download process begins with a call to COurBindStatusCallback::Download. Listing Three presents the Download() method.

COurBindStatusCallback is itself an actual COM object. Whenever we need to download a URL-based file, we just call the static Download() method. As you can see from Listing Three, Download() creates an instance of itself and then starts the download process. The key point is that we pass in our control's pointer and the address of a callback method. As the download proceeds, we will be notified through our OnData() callback method.

The implementation of our callback method is shown in TransCtl.h. As data arrives, the OnData() method is called with a buffer containing the remote data and a flag indicating if this is the first, intermediate, or last data notification. We use the flag value to manage our download buffer. When the download is finished, we set the control's ReadyState property to complete. The rest of the code is general buffer management.

Are You Ready?

Before the addition of asynchronous properties, a control was ready as soon as it was instantiated and initialized by its container. Now, for those controls with asynchronous properties, their internal state may not allow immediate use after loading. Microsoft has added a standard control property called ReadyState. Controls that implement asynchronous properties use ReadyState to communicate readiness to their users. Listing Four shows the defined values for the ready state property.

Most of the states are self explanatory. The difference between the INTERACTIVE and COMPLETE states is up to the control implementor. Those controls that provided interaction with users (like mouse clicks) should move to the interactive state as soon as possible, even if asynchronous downloading is still in progress.

For example, you might develop a button control that displays a bitmap. The control should move to the interactive state and let its click event fire even as the bitmap is downloading. Once the download is finished and the bitmap is rendered, the control moves into the complete state.

Since we don't really provide any interactive behavior for our transparent control, we stay in the loaded state until our bitmap has finished downloading. By examining the OnDraw code in TransCtl.cpp and Listing Five, you can see that we defer rendering of the control until the bitmap download is complete.

There is also a standard-control event called ReadyStateChange that you can use to directly notify users that the ReadyState property has changed. For simplicity, we've left out this detail. Adding support for the ReadyState property to our control is easy. Add a data member of type long with the name of m_nReadyState, and update the control's IDL file with the get/put methods; see TransparentControl.idl (available electronically). Your control must also derive from CStockPropImpl instead of IDispatchImpl, and you need to add the property to your control's property map.

We've got DIBs

URL Monikers treat URL resources as a stream of bytes. It is up to the client application to add meaning to the returned stream. In our example, the stream contains a Windows DIB or bitmap. A DIB object is different than a GIF file in that it doesn't support progressive rendering, so our implementation requires the complete bitmap before we can draw the transparent image.

In most cases, working with DIBs involves reading the bitmap structure in from a local file and working with a handle to the DIB. There's even an API call (LoadImage) that makes this easy. However, in our case, we have to manage a DIB structure in memory that is assembled asynchronously. Once the structure contains all of the necessary information, we need to realize the DIB within a Windows device context. To encapsulate this behavior, we developed a DIB management class called CDib, which is loosely based on the WebImage example in the Win32 SDK. (The header file dib.h is available electronically.)

As data is downloaded, it is stored in a buffer in our control's implementation class. Once the bitmap data is downloaded, the control's ReadyState transitions to complete. This lets code from OnDraw() (Listing Six) execute. The buffer information is passed to the DIB class, a device context is created, a DIB section is created, and the actual bits of the image are passed to the DIB section.

Displaying the CDib Object

Once the image has been loaded into memory, all we have to worry about is displaying it. Compatibility with Internet Explorer 3.0 and older containers means we need to add a handler for WM_ERASEBKGND, and use it to spoof the container into drawing the background for our control. TransCtl.h presents the code added to the CTransCtl definition to process this message. First, we add an entry to the message map for the control, then we add the message handler itself. Essentially, this handler passes the WM_ ERASEBKGND message up to the parent window, along with a copy of the control's device context. (The ATL Object Wizard doesn't help you add message handlers; this is a manual process.)

The actual drawing of the image is done in the OnDraw() member function of the CTransCtl class. OnDraw() is called by the ATL framework, passing in a structure that contains various bits of information needed to paint information on the screen. Our implementation isn't much more complicated than it would be if we were simply displaying the BMP file. We have to add the code to create the monochrome bit mask, then perform the XOR/AND/XOR drawing methods previously discussed.

Listing Seven creates a monochrome bit mask in a compatible device context, relying on a Windows-specific characteristic of monochrome bitmaps. When copying from a color bitmap to a monochrome bitmap, any color pixel that's identical to the color bitmap's background color will get set to 1 in the monochrome bitmap; all other bits will get set to 0. With this knowledge in hand, you can see how the code will create the bitmap needed by the masking algorithm.

Once the mask has been created, we can do the actual drawing using three device contexts:

  • di.hdcDraw contains the screen device context.
  • HDC( m_dib) is the device context containing the color bitmap for our image file.
  • hdcMask is the device context containing the monochrome bitmap.

The three consecutive BitBlt() calls in Listing Eight are all that is required to perform the transparent draw.

Conclusion

To illustrate the techniques presented here, we've provided a demo that displays transparent BMP files at http:// www.widgetware.com/. Since this is an ActiveX control, you can use it in both desktop and Internet applications -- as long as your framework supports control containment. Fortunately, this includes the current releases of nearly all Windows development tools. But if you want to avoid the rigors of being an ActiveX user, you should still be able to implement the old-fashioned approach to reuse by cutting and pasting our drawing code directly into your application. Sometimes the old ways are still the best ways!

DDJ

Listing One

BOOL CTransCtrl::OnEraseBkgnd(CDC* pDC){
    CWnd*  pWndParent = GetParent();
    POINT  pt;
    pt.x = pt.y = 0;
    MapWindowPoints(pWndParent, &pt, 1);
    OffsetWindowOrgEx(pDC->m_hDC, pt.x, pt.y, &pt);
    ::SendMessage( pWndParent->m_hWnd, WM_ERASEBKGND, (WPARAM)pDC->m_hDC, 0 );
    SetWindowOrgEx( pDC->m_hDC, pt.x, pt.y, NULL );
    return 1;
}

Back to Article

Listing Two

<HTML><HEAD>
<TITLE>Test page for TransCtl</TITLE>
</HEAD>
<BODY BACKGROUND="background.gif">
<OBJECT ID="TransCtl" WIDTH=128 HEIGHT=128
     CLASSID="CLSID:B25D9AF5-E760-11D0-A052-00A0247B7657"
     CODEBASE="TransparentControl.dll">
     <PARAM NAME="ImageFile" VALUE="http://www.widgetware.com/image01.bmp">
</OBJECT>

Back to Article

Listing Three

static HRESULT Download( T* pT, ATL_PDATAAVAILABLE pFunc, BSTR bstrURL,                     IUnknown* pUnkContainer = NULL, BOOL bRelative = FALSE)
{
 CComObject<COurBindStatusCallback<T> > *pbsc;
 HRESULT hRes = CComObject<COurBindStatusCallback<T> >::CreateInstance(&pbsc);
 if (FAILED(hRes))
      return hRes;
 return pbsc->StartAsyncDownload(pT,pFunc,bstrURL,pUnkContainer,bRelative);
}

Back to Article

Listing Four

enum tagREADYSTATE{
   READYSTATE_UNINITIALIZED = 0,
   READYSTATE_LOADING       = 1,
   READYSTATE_LOADED        = 2,
   READYSTATE_INTERACTIVE   = 3,
   READYSTATE_COMPLETE      = 4
}  READYSTATE;

Back to Article

Listing Five

// If we're still downloading the image, draw some text and returnif ( m_nReadyState != READYSTATE_COMPLETE )
{
   DrawString( di.hdcDraw, 
               "Downloading bitmap...",
               &rc );
   return S_OK;
}

Back to Article

Listing Six

// If this is the first time through OnDraw, we need to setup the DIBif (! m_dib.IsInitialized() )
{
   m_dib.SetBitmapInfoHeader( m_pbBuffer );
   m_dib.Create( di.hdcDraw );
   m_dib.SetBits( m_pbBuffer );
}

Back to Article

Listing Seven

HDC hdcMask = ::CreateCompatibleDC( di.hdcDraw );HBITMAP bmMask = ::CreateBitmap( columns, rows, 1, 1, NULL );
HBITMAP hOldMaskBitmap = (HBITMAP) SelectObject( hdcMask, bmMask );
SetBkColor( HDC( m_dib ), RGB( 255, 255, 255 ) );
SetTextColor( HDC( m_dib ), RGB( 0, 0, 0 ) );
BitBlt( hdcMask, 0, 0, columns, rows, HDC( m_dib ), 0, 0, SRCCOPY );


</p>

Back to Article

Listing Eight

BitBlt(di.hdcDraw, rc.left, rc.top, columns, rows, HDC(m_dib),0,0,SRCINVERT);BitBlt(di.hdcDraw, rc.left, rc.top, columns, rows, hdcMask, 0, 0, SRCAND );
BitBlt(di.hdcDraw, rc.left, rc.top, columns, rows, HDC(m_dib),0,0,SRCINVERT);

Back to Article


Copyright © 1998, Dr. Dobb's Journal


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.