OLE Controls: Top Tips

Dale Rogerson
Microsoft Developer Network Technology Group

Created: August 9, 1994

Abstract

This article provides tips for building OLE Controls. Before reading this article, you may find it helpful to review the Scribble tutorial on building OLE servers in the Microsoft® Visual C++™ version 1.5 documentation (in Product Documentation, Languages, Visual C++ 1.5 [16-bit], OLE 2 Classes, Tutorial, Chapter 5). The Circle Sample Tutorial in the Control Development Kit (CDK) is also a good source of basic information on building controls.

Introduction

My initial foray into the development of OLE Controls showed me that OLE Controls are amazingly easy to write, compared to VBXes and other custom controls. However, this new area of custom control development includes pitfalls that I quickly learned to avoid. This article explains the pitfalls and provides tips for building OLE Controls, based on my experience.

Before we proceed, one note of caution: At the time this article was written, container applications (such as Microsoft® Visual Basic® version 4.0) had not been released. For this reason, I was unable to write a sample application to accompany this article. When container applications become available, I will provide you with some radical custom controls that will illustrate the concepts in this article.

Here's a quick checklist of tips. I will discuss these in detail in the following sections.

  1. Use TRACE statements at the beginning of functions.

  2. Don't assume anything.

  3. Don't assume that rcBounds uses the coordinates (0, 0) for the upper-left corner.

  4. Use an off-screen buffer

  5. Implement OnDrawMetafile.

  6. Optimize your drawing code.

  7. Use InvalidateControl before drawing on a container.

  8. Test with Visual Basic 4.0.

  9. Test with lots of other containers.

  10. Simplify the control.

  11. Put multiple controls together.

  12. Make your controls three-dimensional.

  13. Make your controls look real.

  14. Remember your users.

  15. Read the source code to understand and solve problems.

  16. Use ambient properties to integrate your control within an application.

Tip 1. Use TRACE Statements

OLE servers may appear to be a collection of independent functions that are called at random. However, if you place TRACE statements at the beginning of each function, you can see a pattern in this collection. The TRACE statement also helps you debug your OLE Control when you try your OLE control in a different container and it fails.

My first OLE Control was painting very slowly, so I put a TRACE statement in my override of the COleControl::OnDraw function. I quickly found out that COleControl::OnDraw was getting called twice. This problem turned out to be a quirk of the alpha container I was using.

If you're a Windows NT™ user, using the Tools menu to run the Test Container will not display your TRACE output—you must use a debugger instead. If you run the debugger, you will quickly find that you don't have an application to debug, only a dynamic-link library (DLL).

If you're a Visual C++™ 2.0 user, choose Settings from the Project menu, select the desired settings category on the left, click the Debug tab, and enter the full path to the Test Container application in the Executable for Debug Session box (in my installation, this path was C:\dev\msvc20\cdk32\bin\tstcon32.exe). After you make this change, whenever you click the debug button, Visual C++ will start Test Container. Since Test Container does not have any debug information, you will get a dialog box asking if you want to continue (you do). Note that Test Container will run whenever you choose Execute from the Project menu.

Tip 2. Don't Assume Anything

The Control Development Kit (CDK) Programmer's Guide contains the following note:

Note   When painting your control, you should not make assumptions about the state of the device context that is passed as the pdc parameter to your OnDraw function. Because the device context is occasionally supplied by the container application, it will not necessarily be initialized to the default state that a newly created device context would normally have. In particular, you should explicitly select whatever pens, brushes, colors, fonts, and other resources that your drawing code depends upon.

Don't assume that you have a black pen and a black brush by default—who knows what you might get? The only way to detect an assumption is to place the control in a container that invalidates that assumption. It's a rather tall order for your testing department to build containers that invalidate your assumptions. It's much easier to ensure that you haven't assumed anything. I recommend that you either use a function that explicitly sets up everything the way you need it, or examine each function in your drawing code and determine which device context (DC) settings can affect the output. Either way, it is possible to miss some of the assumptions you've made.

A good source for information on initial DC settings is Part 4, Chapter 11 of Programming Windows 3.1 by Charles Petzold (MSDN Library Archive, Books and Periodicals). The following list identifies some of the items that you should either set or not use at all:

And for Win32®:

Before you write a ResetDC function, note that Microsoft Windows® already has a function by that name. However, ResetDC doesn't set the DC back to its initial state; it is mainly for handling printer issues. So give your function a different name.

The first DC attribute in the list above is the mapping mode. The container application can pass the COleControl::OnDraw function a pointer to a DC that has changed the mapping mode. This is very important because the other parameters passed to COleControl::OnDraw (rcBounds and rcInvalid) are in logical units. I decided that the easiest way to deal with this was to convert these parameters to device units and always work from device units, as shown below. I use the suffix "DP" on variable names to remind me that the units are in device points.

void CMyCtrl::OnDraw( CDC* pdc, 
                      const CRect& rcBounds, 
                      const CRect& rcInvalid)
{
   CRect rcBoundsDP(rcBounds) ;
   CRect rcInvalidDP(rcInvalid) ;

   // Convert boundaries to device points.
   pdc->LPtoDP(&rcBoundsDP) ;
   pdc->LPtoDP(&rcInvalidDP) ;
   DrawMe(pdc, rcBoundsDP, rcInvalidDP) ;
}

void CMyCtrl::DrawMe( CDC* pdc,
                      const CRect& rcBoundsDP, 
                      const CRect& rcInvalidDP)
{
   SetMapMode(MM_ISOTROPIC) ;
   SetWindowOrg(0,0) ;
   SetWindowExt(1000,1000) ;
   SetViewportOrg(rcBoundsDP.left+rcBounds.Width()/2,
                  rcBoundsDP.top+rcBounds.Height()/2) ;
   SetViewportExt(rcBoundsDP.Width()/2,-rcBoundsDP.Height()/2) ;
   
   // Drawing code goes here.

}

Tip 3. Don't Assume an Upper-Left Corner of (0, 0) for rcBounds

Don't assume that the rcBounds parameter passed to COleControl::OnDraw uses the coordinates (0, 0) for the upper-left corner—it may not do so. After years of writing graphics device interface (GDI) code using MM_TEXT and assuming that the upper-left corner was (0, 0), this was a difficult change to make. It is very easy to write code that works great as long as the corner is (0, 0), but fails when rcBounds uses a non-zero.

In most cases, the program doesn't fail outright, but produces some strange effects. For example, in one case I had set up my mapping mode as follows, where rcBoundsDP is rcBounds converted to device points:

   SetMapMode(MM_ISOTROPIC) ;
   SetWindowOrg(0,0) ;
   SetWindowExt(1000,1000) ;
   SetViewportOrg(rcBoundsDP.Width()/2,
                  rcBoundsDP.Height()/2) ;
   SetViewportExt(rcBoundsDP.Width()/2,-rcBoundsDP.Height()/2) ;

I was using this mapping mode to create the following coordinate system:

Figure 1. Coordinate system with (0, 0) in the center and positive y axis extending up

However, sometimes the control painted in the upper-left corner of the container's client areas instead of painting inside the control's boundaries. The problem involves setting the viewport origin: When you set the viewport origin to (x, y) and the window origin to (0, 0), the logical point (0, 0) maps to the device point (x, y). Instead, we would like the logical point (0, 0) to map to the center of rcBounds.

Assume that rcBoundsDP is (0, 0, 50, 200). The center of this is (25, 100), so we would use:

SetViewportOrg(25,100) ;

Now, assume that rcBoundsDP is (100, 200, 150, 400). The size of this rectangle is still (25, 100), but the center is now (100+25, 200+100), or (125, 300), so I changed the upper-left boundary in the SetViewportOrg call as follows:

   SetMapMode(MM_ISTROPIC) ;
   SetWindowOrg(0,0) ;
   SetWindowExt(1000,1000) ;
   SetViewportOrg(rcBoundsDP.left+rcBounds.Width()/2,
                  rcBoundsDP.top+rcBounds.Height()/2) ;
   SetViewportExt(rcBoundsDP.Width()/2,-rcBoundsDP.Height()/2) ;

Tip 4. Use an Off-Screen Buffer

Nigel Thompson sits down the hall from me. One of the things that annoys him (there are billions) is flickering screen updating. As I sit in my office cranking out some code (or hacking away on a Doom monster with a chain saw), a loud string of British expletives will suddenly assault my ears—Nigel has just found another application developed by someone who doesn't know anything about using off-screen buffers.

It's really simple: If you clear a section of the screen, paint it, then draw on it repeatedly, it will flicker. The trick is to do all of your painting and drawing off-screen, in a memory bitmap, then blt the whole thing to the display at once. This simple fix improves the look and feel of an application dramatically.

For one of my OLE Controls, my drawing function looked like this:

void CMyCtrl::OnDraw( CDC* pdc, 
                      const CRect& rcBounds, 
                      const CRect& rcInvalid)
{
   DrawMe(pdc, rcBounds) ;
}

The CMyCtrl::DrawMe function draws directly on the screen, without using an off-screen buffer. Needless to say, it flickered a little. Instead of waiting for Nigel's wrath, I changed OnDraw as follows:

void CMyCtrl::OnDraw( CDC* pdc, 
                      const CRect& rcBounds, 
                      const CRect& rcInvalid)
{
   DrawMeOffScreen(pdc, rcBounds) ;
}

The new CPianoCtrl::DrawMeOffScreen function is a simple wrapper for DrawMe that uses an off-screen buffer. The DrawMeOffScreen code is listed below:

void CMyCtrl::DrawMeOffScreen(CDC* pdc, const CRect& rcBounds)
{

   CDC dcMem;
   CBitmap bitOff;
   CRect rcBoundsDP(rcBounds) ;

   // Convert bounds to device units. 
   pdc->LPtoDP(&rcBoundsDP) ;

   // The bitmap bounds have 0,0 in the upper-left corner.
   CRect rcBitmapBounds(0,0,rcBoundsDP.Width(),rcBoundsDP.Height()) ;

   // Create a DC that is compatible with the screen.
   dcMem.CreateCompatibleDC(pdc) ;

   // Create a really compatible bitmap.
   bitOff.CreateBitmap(&dcMem, 
                       rcBitmapBounds.Width(), 
                       rcBitmapBounds.Height(),
                       pdc->GetDeviceCaps(PLANES),
                       pdc->GetDeviceCaps(BITSPIXEL),
                       NULL) ;

   // Select the bitmap into the memory DC.
   CBitmap* pOldBitmap = dcMem.SelectObject(&bitOff) ;

   // Save the memory DC state, since DrawMe might change it.
   int iSavedDC = dcMem.SaveDC();

   // Draw our control on the memory DC.
   DrawMe(&dcMem, rcBitmapBounds) ;

   // Restore the DC, since DrawMe might have changed mapping modes.
   dcMem.RestoreDC(iSavedDC) ;

   // We don't know what mapping mode pdc is using.
   // BitBlt uses logical coordinates.
   // Easiest thing is to change to MM_TEXT.
   pdc->SetMapMode(MM_TEXT) ;
   pdc->SetWindowOrg(0,0) ;
   pdc->SetViewportOrg(0,0) ;

   // Blt the memory device context to the screen.
   pdc->BitBlt( rcBoundsDP.left,
                rcBoundsDP.top,
                rcBoundsDP.Width(),
                rcBoundsDP.Height(),
                &dcMem,
                0,
                0,
                SRCCOPY) ;

   // Clean up.
   dcMem.SelectObject(pOldBitmap) ;
}

When developing the drawing code, it is sometimes helpful to watch the code draw. I usually draw directly on the screen until I'm satisfied with the drawing code, then start using an off-screen buffer.

For more information on flicker-free screen updating, see the "Flicker-Free Displays Using an Off-Screen DC" article in the MSDN Library.

Tip 5. Implement OnDrawMetafile

In the default implementation of COleControl, OnDrawMetafile calls OnDraw. This usually works fine for simple controls. However, for controls that have complicated drawing routines (especially if they change mapping modes or use off-screen bitmaps), it is best to write a separate handler for OnDrawMetafile. To test the metafile with the Test Container, choose Draw Metafile from the Edit menu.

My first control used an off-screen buffer. It's usually not a good idea to put a bitmap in your metafile because the bitmap won't look very good when the metafile gets stretched. For this reason, I implemented an OnDrawMetafile handler. Instead of calling OnDraw and using an off-screen buffer, OnDrawMetafile simply draws the control directly in the metafile. This will also speed up drawing, although the user will have to watch the metafile build on the screen.

void CMyCtrl::OnDrawMetafile( CDC* pDC, 
                              const CRect& rcBounds)
{
   DrawMe(pDC, rcBounds) ;
}

In another control, I set the mapping mode, the viewport origin, and viewport extents in the OnDraw code—these are things that you don't want set in your metafile. I therefore had to implement a separate OnDrawMetafile so these calls would not be present.

The following are excerpts from my OnDraw code:

   // Adjust for different mapping modes.
   CRect rcBoundsDP(rcBounds) ;
   pdc->LPtoDP(rcBounds) ;

   //Set up the mapping modes.
   pdc->SetMapMode(MM_ISOTROPIC) ;
   pdc->SetWindowExt(RADIUS,RADIUS) ;
   pdc->SetViewportOrg( rcBoundsDP.left+rcBoundsDP.Width()/2,
                        rcBoundsDP.top+rcBoundsDP.Height()/2) ;
   pdc->SetViewportExt( rcBoundsDP.Width()/2,
                        -rcBoundsDP.Height()/2) ;

   //Draw.
   CRect rcSquare(RADIUS, RADIUS, -RADIUS, -RADIUS) ;
   CBrush brushFore(COLOR) ;
   CBrush* pOldBrush = pdc->SelectObject(&brushBack) ;
   CPen* pOldPen = pdc->SelectStockObject(BLACK_PEN) ;
   pdc->Ellipse(rcSquare);

   //etc.

OnDrawMetafile code:

   // Adjust for different mapping modes.
   CRect rcBoundsDP(rcBounds) ;
   pdc->LPtoDP(rcBounds) ;

   CSize size = rcBoundsDP.Size() ;
   int iShortSide = min(size.cx,size.cy) ;
   CPoint ptCenter(rcBoundsDP.left+size.cx/2,rcBoundsDP.top+size.cy/2) ;   

   // Get the short side and map 2*RADIUS to iShortSide;
   // this way my units should be ISOTROPIC.
   double dbConvert = 2*RADIUS/(double)iShortSide ;
   pdc->SetWindowOrg( -int(ptCenter.x*dbConvert),
                      int(ptCenter.y*dbConvert)) ;
   pdc->SetWindowExt( int(size.cx*dbConvert),
                      -int(size.cy*dbConvert)) ;

   // etc.

Tip 6. Optimize the Drawing Code

Optimizing your drawing code speeds the painting of dialogs and saves the user from watching the controls in a dialog paint themselves one by one. Here are two tricks for optimizing drawing operations for your OLE Control: using the rcInvalid parameter for COleControl::Ondraw, and using a bitmap buffer.

Using the rcInvalid Parameter

The rcInvalid parameter to the COleControl::OnDraw function identifies the area of the control that needs to be updated. If your drawing code is time-consuming, redrawing only the invalid area can provide a performance improvement. However, some OLE Control containers don't invalidate controls in the most optimized way. In many cases, the rectangle in rcInvalid may be bigger than it needs to be.

rcInvalid is very easy to use if you are using an off-screen bitmap buffer. Simply change the parameters to the BitBlt function:

void CMyCtrl::DrawMeOffScreen( CDC* pdc, 
                               const CRect& rcBounds
                               const Crect& rcInvalid)
{
   CDC dcMem;
   CBitmap bitOff;
   CRect rcBoundsDP(rcBounds) ;

   Crect rcInvalidDP(rcInvalid) ;


   // Convert bounds to pixels. 
   pdc->LPtoDP(&rcBoundsDP) ;

   pdc->LPtoDP(&rcInvalidDP) ;


   // Create a DC that is compatible with the screen.
   dcMem.CreateCompatibleDC(pdc) ;

   // The bitmap bounds have 0,0 in the upper-left corner.
   CRect rcBitmapBounds( 0,0,
                         rcBoundsDP.Width(),
                         rcBoundsDP.Height()) ;

   // Create a really compatible bitmap.
   bitOff.CreateBitmap(&dcMem, 
                       rcBitmapBounds.Width(), 
                       rcBitmapBounds.Height(),
                       pdc->GetDeviceCaps(PLANES),
                       pdc->GetDeviceCaps(BITSPIXEL),
                       NULL) ;

   // Select the bitmap into the memory DC.
   CBitmap* pOldBitmap = dcMem.SelectObject(&bitOff) ;

   // Save the memory DC state, since DrawMe might change it.
   int iSavedDC = dcMem.SaveDC();

   // Draw our control on the memory DC.
   DrawMe(&dcMem, rcBitmapBounds) ;

   // Restore the DC, since DrawMe might have changed mapping modes.
   dcMem.RestoreDC(iSavedDC) ;

   // We don't know what mapping mode pdc is using.
   // BitBlt uses logical coordinates.
   // Easiest thing is to change to MM_TEXT.
   pdc->SetMapMode(MM_TEXT) ;
   pdc->SetWindowOrg(0,0) ;
   pdc->SetViewportOrg(0,0) ;

   // Blt the memory DC to the screen.
   pdc->BitBlt( rcInvalidDP.left,

                rcInvalidDP.top,

                rcInvalidDP.Width(),

                rcInvalidDP.Height(),

                &dcMem,
                rcInvalidDP.left - rcBoundsDP.left,

                rcInvalidDP.top - rcBoundsDP.top,
                SRCCOPY) ;

   // Clean up.
   dcMem.SelectObject(pOldBitmap) ;
}

Using a Bitmap Buffer

If you draw the control into a bitmap, you simply blt the image onto the screen to satisfy paint requests. If you save this bitmap between OnDraw calls, you may be able to satisfy several paint requests without having to redraw the control with graphics device interface (GDI) calls. Most controls do not change their appearance unless the user clicks or otherwise manipulates them, so many of the OnDraw calls are requests from OnPaint for replacing parts of the screen covered by a window or dialog box. You should make sure that you update the bitmap image when the control's appearance changes.

void CMyCtrl::OnDraw( CDC* pdc, 
                      const CRect& rcBounds, 
                      const CRect& rcInvalid)
{
   CDC dcMem;
   CRect rcBoundsDP(rcBounds) ;
   CRect rcInvalidDP(rcInvalid) ;

   // Convert bounds to pixels. 
   pdc->LPtoDP(&rcBoundsDP) ;
   pdc->LPtoDP(&rcInvalidDP) ;

   // Create a DC that is compatible with the screen.
   dcMem.CreateCompatibleDC(pdc) ;

   // The bitmap bounds have 0,0 in the upper-left corner.
   CRect rcBitmapBounds( 0,0,
                         rcBoundsDP.Width(),
                         rcBoundsDP.Height()) ;

   // Check if size changed.
   BOOL bIsSizeDifferent = (m_rectLast != rcBitmapBounds) ;

   if ((m_pBitOff == NULL) || (bIsSizeDifferent))
   {
      // Delete bitmap if it already exists.
      if (m_pBitOff != NULL) delete m_pBitOff ;

      // Create a new bitmap.
      m_pBitOff = new CBitmap ;

      // Create a really compatible bitmap.
      m_pBitOff->CreateBitmap(&dcMem, 
                               rcBitmapBounds.Width(), 
                               rcBitmapBounds.Height(),
                               pdc->GetDeviceCaps(PLANES),
                               pdc->GetDeviceCaps(BITSPIXEL),
                               NULL) ;

      // Save the size of the bitmap.
      m_rectLast = rcBitmapBounds ;
   }

   // Select the bitmap into the memory DC.
   CBitmap* pOldBitmap = dcMem.SelectObject(m_pBitOff) ;

   if ( (IsModified()) || (bIsSizeDifferent) )
   {
      // Redraw the image if it has been modified or the size
      // has changed.

      // Save the memory DC state, since DrawMe might change it.
      int iSavedDC = dcMem.SaveDC();

      // Draw our control on the memory DC.
      DrawMe(&dcMem, rcBitmapBounds) ;

      // Restore the DC, since DrawMe might have changed mapping modes.
      dcMem.RestoreDC(iSavedDC) ;
   }

   // We don't know what mapping mode pdc is using.
   // BitBlt uses logical coordinates.
   // Easiest thing is to change to MM_TEXT.
   pdc->SetMapMode(MM_TEXT) ;
   pdc->SetWindowOrg(0,0) ;
   pdc->SetViewportOrg(0,0) ;

   // Blt the memory device context to the screen.
   pdc->BitBlt( rcInvalidDP.left,

                rcInvalidDP.top,

                rcInvalidDP.Width(),

                rcInvalidDP.Height(),

                &dcMem,
                rcInvalidDP.left - rcBoundsDP.left,

                rcInvalidDP.top - rcBoundsDP.top,
                SRCCOPY) ;

   // Clean up.
   dcMem.SelectObject(pOldBitmap) ;
}

Tip 7. Use InvalidateControl

Frequently, a control will need to update itself in response to messages and methods. Messages and methods don't come calling with a convenient DC to draw on. A control should not use:

CClientDC dc(this) ;
OnDraw(&dc, m_rectMyLastRect) ;

or similar methods (for example, GetDC).

The control's container owns the property the control is painting on. Therefore, the control should use COleControl::InvalidateControl to ask permission before drawing on the container.

When I first read the description of COleControl::InvalidateControl, it reminded me of CWnd::Invalidate and ::InvalidateRect. In a standard Windows-based application, these functions change only the invalidated rectangle—the screen is not updated until the next WM_PAINT message. Therefore, if you need to update the client area, you must call UpdateWindow to get the WM_PAINT message on demand.

However, COleControl::InvalidateControl does not work the same way as ::InvalidateRect or CWnd::Invalidate. The only function you need to call is InvalidateControl—not UpdateWindow. My timer handler looked like this:

m_iSecs++ ;
if (m_iSecs == 61)
{
   m_iSecs = 1 ;
   m_bColor = !m_bColor ;
}
InvalidateControl(NULL) ;

The source code to COleControl::InvalidateControl can be found in CTLCORE.CPP in the CDK source code directory. It is listed below (notice the SendAdvised calls):

void COleControl::InvalidateControl(LPCRECT lpRect)
{
        if (m_bInPlaceActive || m_bOpen)
                InvalidateRect(lpRect, TRUE);
        else
                SendAdvise(OBJECTCODE_VIEWCHANGED);

        SendAdvise(OBJECTCODE_DATACHANGED);
}

Tip 8. Test with Visual Basic 4.0

When Visual Basic 4.0 is released, it will become the most popular container for OLE Controls. Both Visual Basic developers and control developers will find OLE Controls very powerful, as easy to use as VBXes, and easier to develop than VBXes, especially with the help of ControlWizard and the Microsoft Foundation Class Library (MFC).

Because the majority of OLE Control users will be using Visual Basic, you should strive to make your controls work with Visual Basic. If they work with Visual Basic, it is more likely that they will work with MFC when it supports using OLE Controls.

Tip 9. Test with Lots of Containers

Ideally, if your OLE Control works with one container, it should work with others. In reality, however, this is not the case. Developers can create OLE containers and servers that do just about anything. As a result, one OLE container may be very different from another. For example, compare Microsoft Excel with the MFC sample application Contain. Both applications are OLE containers, but you can easily write a server that works with one but not the other.

The same is true for OLE Controls and their containers. For example, the first control I wrote worked with Test Container, but it did not work with Microsoft Access®. My control was depending on a WM_SIZE message that Microsoft Access never sent. Determining who's at fault—the Test Container, my control, or Microsoft Access—can be very difficult in these situations. However, the more containers you have on your side, the better off you will be.

Another good test is to insert your OLE Controls into existing OLE containers that do not directly support OLE Controls. Examples of these containers include Microsoft Excel and Microsoft Word. These programs are good OLE containers, but they aren't very good OLE Control containers. What's the difference? Well, the best explanation I've heard is this: To be an OLE Control container, the first thing you need to do is be a great OLE container. Then you need to be a great OLE container that supports inside-out activation and multiple active objects. Finally, you need to implement the event stuff.

In any event, if you test your control inside Microsoft Excel or Word, your drawing code will be more robust. You might also be able to use your control in these applications without implementing events.

Note   If you cannot insert your control into Microsoft Excel or Word, examine the implementation of COleObjectFactoryEx::UpdateRegistry. (For my piano control, ControlWizard implemented this function as CPianoCtrl::CPianoCtrlFactory::UpdateRegistry and placed it in the PIANOCTL.CPP file.) The default implementation calls AfxOleRegisterControlClass. If the sixth parameter of this function is TRUE, the control can be inserted.

Tip 10. Simplify the Control

Is your OLE Control simple to use but not versatile enough, or is it versatile but complicated to use? To phrase it another way: What does your application do, and what does your OLE Control do? This is the dilemma.

One thing that I really hate about the first spin control I developed was the amount of code I had to write to use it. I had to intercept the mouse events and send them to the proper control. The code wasn't difficult to write, but handling a large number of controls on a dialog box was very tedious. Using the spin controls took so much effort that in many instances I simply left them out. It would have been easier to build a single custom control from an edit control and a spin control, which is what I ended up doing. My combination control was much easier to use, but it was also less versatile—it would only spin numbers.

I also built a control with a sweeping second hand. As the second hand went around the control, it would fill in the circle. After 15 seconds, the control looked like this:

Figure 2. Control with sweeping second hand

When I thought about it, I realized that this control had uses beyond merely tracking time. It was really a pie control—it could be used to track progress during an installation, show the percentage of free space on a drive, or display any other percentage. I pulled out the timer code to make the control more versatile. With proper design, I could still use the control as a sweeping second hand. Visual Basic already has a timer control, which can be programmed to drive my control as the time changes. Since this is easy to do with Visual Basic, I left out the timer code from my control. It pays to test your controls with all containers.

Even glorified button controls require some thought. For example, I designed the piano control shown in Figure 3 to use in a chord calculator.

Figure 3. Piano control

In my original design, if you click the F piano key with the mouse, the key appears pressed (Figure 4).

Figure 4. F piano key depressed; left mouse button down

When you release the mouse button, the control sends a NoteOn("F") event and the key stays pressed (Figure 5).

Figure 5. F piano key depressed; left mouse button released

When you click the F key a second time with the mouse, the key is released on the mouse button up and sends a NoteOff("F") event. This interface works great for the chord calculator because you want the user to press several piano keys to build a chord.

However, most of the people who saw the control wanted it to work like a piano. They wanted the key to go down on a mouse button down and up on a mouse button up. For the control developer, this functionality is easier to write and more versatile because the previous chord method can be built from it. However, writing the code to support the chord method can get tedious for developers who use the piano control, so the best solution may be to support two modes for the control: single-key mode and chord-key mode. Having multiple modes is generally preferable to sending out a message for everything under the sun.

Tip 11. Put Multiple Controls Together

Unlike other custom controls, a single OLE Control dynamic-link library (DLL) can contain multiple controls. You can use this capability to your advantage in several ways. Let's say you have an application that teaches people chords. If you want to display chords on a piano keyboard and on a guitar, you can have a OLE Control DLL called CHORDS.OCX that contains a Piano control and a Guitar control.

You can take advantage of having multiple controls in one DLL in several ways. For example, CHORDS.OCX could contain the following:

Placing multiple controls in one DLL also reduces the overhead associated with having multiple DLLs supporting each control.

Tip 12. Make Your Controls 3-D

In the next version of the Windows operating system (called Windows 95), all controls appear three-dimensional. Currently, most applications use CTL3D or similar programs to give their controls a 3-D look. You will want to do the same for your OLE Controls so that they will be consistent with other controls when placed in a dialog box. If your control requires Windows 95, you can use the new DrawEdge function provided in Windows 95:

BOOL DrawEdge(
   HDC hdc,           // handle of device context
   LPRECT lprc,       // address of rectangle coordinates (logical)
   EDGE edgeType,     // type of inner and outer edge to draw
   UINT uFlags)       // type of border

The DrawEdge function draws a rectangle that appears raised or sunken.

Until Windows 95 is released, you will need to write your own functions to give your controls a 3-D appearance. Before you start coding, take a look at the User Interface Design Guide. In Chapter 13 ("Visual Design"), read the section on visual communication (dimensionality). (You may also find the "Visual Design of Controls" section useful.)

Now that you know what the control should look like, read the following articles by Kyle Marsh in the MSDN Library to find out how you can add the 3-D effect:

Giving arbitrary non-rectangular shapes a 3-D appearance is more difficult. Usually, you need an artist and a palette with lots of grays.

Tip 13. Make Your Controls Look Real

Making your controls look realistic is even better than making them three-dimensional. For example, implement buttons that look like toggle switches; create radio knobs instead of radio buttons. Interesting controls can turn a worthless program into an exciting application. (Well, maybe not.)

Properly designed custom controls can make an application easier to use. The key phrase is "properly designed." If your control looks or acts completely unlike a standard Windows control, users will get confused.

When designing realistic-looking controls, you may have to paint the control with scanned images instead of drawing the control with GDI calls. Most OLE Controls can be sized, so you might want to use a different bitmap for each control size. Keep in mind that stretched bitmaps usually look better than squashed bitmaps.

Your control will probably require 256 colors to look, well, realistic. To get the 256 colors you want, you must use a palette. Palettes are a nightmare to use unless you've read Nigel Thompson's book on animation, which is the best source of information on 256-color bitmaps. Animation is not only for games; you can use the techniques presented in Nigel's book to create animated controls that use several bitmaps to show the change from one state to another.

Tip 14. Remember Your Users

Focus on who your users are so you can anticipate their needs. Your users may include other developers, end-users, and yourself.

End-users of applications that use OLE Controls are, by definition, OLE Control users. You should take into account how these users will interact with your control. The more your control acts like other Windows controls or controls in the real world, the easier time the user will have.

Another user group consists of developers who use OLE Controls in their applications. The needs of these developers are different from those of end-users. Developers want an OLE Control that is easy to program and easy to customize for their applications. For example; property sheets that set OLE control features are usually seen only by the developer during design time, and not by the end-user.

Visual Basic developers, Microsoft Office developers, and MFC developers will eventually all use OLE Controls. Because the programming environments are different, a control that is easy for a C++ programmer to use could challenge a Visual Basic programmer. The reverse could also be true.

If you are designing OLE controls for your application only—that is, not for general distribution—you don't have to write property sheets or ensure that the OLE control works with MFC. However, if you are planning on selling OLE controls, this extra work will be required.

Tip 15. Use the Source, Luke

Many programmers claim that the best documentation is the source code. I don't know if this is true any longer, considering how complicated software is these days. In any event, Microsoft does ship the source code for MFC and for OLE Control extensions. Use this code to understand and solve problems. For example, I was trying to figure out why my OnDraw function was getting called so many times. By looking through the source code, I was able to determine that OnPaint, DrawContent, and OnDrawMetafile all call OnDraw.

Tip 16. Use Ambient Properties

Your cool button control (which makes funny noises when it's clicked) will look pretty ugly if it displays its caption in Arial in a dialog box full of Times New Roman captions.

Use ambient properties to integrate your control properly within an application. Ambient properties are properties that a container exposes to its controls. For more information on ambient properties, see the Control Development Kit (CDK) documentation.

Bibliography

Marsh, Kyle. "Adding 3-D Effects to Controls." September 1992 (revised August 1993). (MSDN Library Archive, Technical Articles)

Marsh, Kyle. "Creating a Toolbar." December 1992. (MSDN Library Archive, Technical Articles)

Petzold, Charles. Programming Windows 3.1. Redmond, WA: Microsoft Press, 1993. (MSDN Library Archive, Books and Periodicals).

Rodent, Herman. "Flicker-Free Displays Using an Off-Screen DC." April 1993.

Rogerson, Dale. "OLE Controls: State of the Union." October 1994.

Control Development Kit (CDK) documentation.