Creating Shaped UI Objects

Shape up! Don't be a square

Steve Sipe

Steve is a developer with GE Fanuc Automation in Charlottesville, Virgina. He can be reached at steve.sipe@cho.ge.com.


As the user interface of Windows applications constantly evolves, many new applications are presenting users with "shaped" objects with which they can interact. For example, a desktop navigation program may present a desk that contains phones, file folders, pencils, and the like. Users can activate each of the associated items by clicking on its representation.

Windows developers facing the challenge of creating and manipulating shaped objects must strike a balance between creating pure objects that don't care about their shape, and objects that must be aware of their precise shape every time they are painted or when a mouse button is clicked.

While Windows 3.x APIs only offered support for rectangular windows, Windows 95 and NT 3.51 implement an API function that makes creating and managing shaped windows straightforward. In this article, I present CShape, a class that encapsulates this function. I also include two shaped objects (built with Visual C++ 4.0)a "sticky note" with a turned up corner (see Figure 1) that demonstrates how to create a polygon shaped CWnd, and a "stellar calculator" (see Figure 2) that demonstrates how to create shaped custom controls and shaped dialog boxes. The source code and related files for both the stellar calculator and sticky-note window are available electronically.

The Old Way

Windows 3.x gave you a way to create nonrectangular windows, but there were several "tricks" needed to make this approach work. The approach consisted of creating a transparent window with the extended window style of WS_EX_TRANSPARENT. A transparent window is painted after any sibling windows underneath it are painted, allowing them to show through.

This approach sounds simple and easy to implement, but there is a problem. Assume that your window displays a circle depicting a clock with hands. You define your painting code to draw the circle using the Ellipse() API call, create the window with the WS_EX_TRANSPARENT extended style, add a title bar, and then build and execute the program.

So far so good. The circle and windows underneath appear. It gives every indication of doing exactly what you want. Now, grab the title bar and move the window. This is where the problem surfaces. Transparent windows keep the contents of the windows beneath them and only update those contents when the windows underneath repaint. The result is a clock window with garbage surrounding the face of the clock. There is a way to solve this but it involves hit-testing the points outside the circle and sending paint messages to each of the sibling windows underneath. This is an awkward approach and makes the resizing and repainting code painfully aware of the shape of the window.

Another problem with this approach involves processing mouse clicks. A true shaped window should allow the windows underneath to receive mouse clicks if the mouse button is not clicked somewhere in the shape of the object. A clock, for example, might process a left-button click to let users set an alarm. If users click outside of the clock face, you need to pass the left-button click to the window underneath. Again, you must hit-test for the window underneath and forward the mouse message to it.

The New Way

Windows 95 and Windows NT 3.51 provide an easier way to create shaped windows. They provide a new API function SetWindowRgn() that allows the creation of windows with visible regions. A visible region defines the presentation area of the window and also determines which windows receive mouse clicks. Regions free the developer from the responsibility of forcing the repainting of sibling windows or routing mouse clicks to them. The operating system automatically handles these responsibilities based on the visible region's shape.

SetWindowRgn() takes two arguments: A window handle that sets the visible region, and the handle of a GDI region object that describes the visible region. Example 1 demonstrates how to use SetWindowRgn() to create a dialog box with rounded corners.

The m_rgnWnd data member is an instance of a CRgn MFC class. CRgn encapsulates a GDI region object. (A region is similar to a RECT structure, but has more shapes than just rectangular.) The CRgn class exposes this capability by providing methods for creating rounded rectangles, polygons, ellipses, and so on. Region objects are an integral part of creating shaped windows.

A New Class

After experimenting with setting the shapes of various windows, I built a set of common "shape" methods to encapsulate setting visible regions for various CWnd-derived windows. Unfortunately, classes like CDialog and CView, though derived from CWnd, require unique classes derived from CDialog and CView, respectively, because they require different methods and message maps. This list of unique CShape classes could also expand indefinitely as new, unique CWnd-derived objects appear that would be good candidates for shaping. Of course, a drawback to implementing these various classes is that the same basic shape-setting code is contained in each new class.

I solved this problem by using multiple inheritance, which allows a class to be derived from two base classes. Some feel that multiple inheritance violates true object-oriented programming because a class might contain the definition of two or more objects. In my implementation, I view the CWnd as the object and its shape as nothing more than an attribute. There is no confusion about what the object really is. Example 2 (extracted from "Stellar Calculator") is the class definition for a shaped dialog box.

Notice that CCalculator is derived from both CDialog and CShape. The CShape class is a base C++ classit is not derived from anything (including CObject, more on this later). It contains several methods that simplify setting the shape of the associated window. Combining CShape with a CWnd-derived class is completely transparent except for one requirement. The CShape class defines a pure virtual function, GetHWND(), that returns a window handle. "Pure virtual" means that the method is not implemented in the CShape class, but must be implemented by any class derived from CShape. The CCalculator class definition shows how to implement GetHWND().

Visual C++ fully supports multiple inheritance. Likewise, MFC supports it, but with two restrictions. A class cannot contain two CWnd objects. For instance, a class derived from both CFrameWnd and CEdit is not legal because each of these classes contains a CWnd. Also, if both classes are derived from CObject, then the derived class must define how to handle the operators new and delete and the Dump() method. This is because both CObject classes contain these methods, and the compiler can't determine which CObject methods to use. I chose to avoid this confusion by not deriving my CShape class from CObject. This eliminates the need to redefine new, delete, and Dump() and takes nothing away from the functionality of the CShape class. In my implementation, a window's shape is an attribute just like its size or location. The CShape class implements methods to modify this shape attribute and requires an associated CWnd object to make it functional.

The CShape class that I provide encapsulates the various region methods into a simple-to-use class that allows you to create various familiar shapes such as stars, ellipses, and the like. This class can easily be extended to provide additional shapes. I have provided a few as examples of how region objects and visible regions interact.

The Shape of Things that Can't

Combining the CShape class with top-level windows such as CDialog and CMainFrame worked well, but I also wanted to create shaped button controls in dialog boxes. Because buttons and text controls are just child windows, I assumed that it would be easy to set their shape. I was wrong. It seems that standard controls such as buttons allow you to set their shape, then they politely set it back somewhere inside their window procedure. I decided to try a different approach altogethercreating simple custom controls that I could set the shape of and treat just like standard controls.

Creating custom controls with the SDK begins by defining a window procedure and calling RegisterClass() with a WNDCLASS definition that, among other things, contains a pointer to the control's window procedure. This window procedure is a standard exported C routine that is responsible for handling the various Windows messages for painting, mouse input, or whatever. I wanted to implement the same type of functionality in my shaped buttons, but I wanted to implement it with a CWnd-derived class instead of a C window procedure, so that I could use MFC methods. I began to examine how to create custom controls within the framework of MFC.

The Shape of Things that Can

Creating custom controls with MFC works much the same as with the SDK, but with a twistwhen does RegisterClass() get called for the control and how does it get attached to an MFC class? The trick to registering the class is to use a static member in each class that automatically calls a static Register() method. Making Register() a static method assures that it is called regardless of whether the class is instantiated.

Having taken care of how to register the control's class, I had one last detail to attend toattaching an MFC class to a newly created control. I added a switch statement case for the WM_NCCREATE message to the custom control's window procedure. WM_NCCREATE is sent by Windows just before a window is created. At this point, I instantiated the control's associated MFC class by doing a new, then I attached the class to the control by calling the MFC SubclassWindow() method. Subclassing causes any subsequent Windows messages to be sent to my MFC class where they can be handled.

The stellar calculator's CCalcKey class demonstrates how to create a simple custom control. CCalcKey uses two macros defined in shape.h to simplify the process of registering control classes and attaching MFC classes:

DECLARE_SHAPECLASS() creates a few basic definitions that are used to register and subclass the new control class. IMPLEMENT_SHAPECLASS() does most of the real work. It defines the WNDCLASS structure that is required to register the new class. It also defines a static window procedure pointed to by the WNDCLASS structure that contains the code to create and attach the custom control to its MFC class. The file shape.h contains the definitions of these macros, and the files calckey.cpp and calckey.h demonstrate how to use them to create custom controls.

Now that you can register a class, you might ask how the new custom control gets created. This is the simplest part of all. Simply add a user-defined control to the dialog using the dialog editor. The user-defined control's class name must match the name of the class from DECLARE_SHAPECLASS(). In the CCalcKey example, the control's class name would be CCalcKey. The dialog IDD_CALCULATOR shows an example of how CCalcKey custom controls are defined. Note that the custom control only appears as a blue rectangle at design time, but at run time it takes the shape and color that you set in your code.

Figure 2 shows the stellar calculator in action. Every object on the calculatornumber buttons, total display, operator buttons, and even the close buttonare CCalcKey custom controls. Run the calculator and click on a number button. Notice that the button's color changes to red when it is clicked. Click in the indented space between the points of the star. This time the color remains yellow because the mouse message is outside the button's visible region and is therefore automatically routed to the window underneath.

I also provide sample code to create a shaped sticky note window that contains a turned-up corner.

The Shape of Things to Come

To make my shape implementation complete, I had one last requirement. I wanted to be able to set the shape of OCX controls. Because OCX controls are based on CWnd objects, I assumed that I should be able to combine my CShape class with them just like any other type of CWnd. I derived the OCX from both COleControl and CShape and added OnCreate to handle the WM_CREATE message. The OnCreate() method is a good place to set the shape of the OCX because the CWnd is created at this point, and this method is called only once for each new control. Example 4 shows how to set the shape of the OCX to a star.

This is all that is necessary to set the shape of the OCX to something other than a boring rectangle. Keep in mind that, just like custom controls, the shape of the control is still rectangular at design time, but transforms into the shape your code specifies at run time.

Figure 1: The sticky-note window UI.

Figure 2: The stellar calculator UI.

Example 1: (a) In the header file, define a data member for the region class; (b) using it in the CPP implementation file, add calls in OnInitDialog to set the dialog's shape.

(a)

// (NOTE: m_rgnWnd must be a data member, not on the stack because Windows
// continues to use the region handle as long as the window object
// associated with it exists)
CRgn m_rgnWnd;

(b)

BOOL CMyDialog::OnInitDialog()
{
    // Call the base class
    CDialog::OnInitDialog();
    // Create a rectangular region (100x100) with rounded (30x30) corners
    m_rgnWnd.CreateRoundRectRgn(0,0,100,100,30,30);
    // Set the window's visible region to the specified shape
    SetWindowRgn(m_hWnd,m_rgnWnd);
    return TRUE;
}

Example 2: Class definition for a shaped dialog box.

class CCalculator : public CDialog, public CShape
{
private:
    // Any class derived from CShape must implement this method
    HWND GetHWND() { return m_hWnd; }
    ...
};

Example 3: (a) The DECLARE_SHAPECLASS() macro; (b) the IMPLEMENT_SHAPECLASS() macro.

(a)     class CCalcKey : public CWnd, public CShape
 {
     // Declare the custom control class
     // NOTE: argument MUST match class name
     DECLARE_SHAPECLASS(CCalcKey);
     ...
 };
 

 (b)     (various include files)
     ...
 // Implement the custom control class
 // NOTE: argument MUST match class name
 IMPLEMENT_SHAPECLASS(CCalcKey);
 
 (Rest of CCalcKey methods below) 
    ...

Example 4: Setting shape of an OCX to a star.

int CMyCtrl::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
    // Create the OLE control (and CWnd object)
    if (COleControl::OnCreate(lpCreateStruct) == -1)
        return -1;
    // Use a CShape method to make it look like a star
    SetStarShape();
    return 0;
}