Chapter 26

Building an ActiveX Control


CONTENTS

ActiveX controls replace OLE controls, though it's a change more in name than in anything else. (Much of the Microsoft documentation still refers to OLE controls.) The exciting behavior of these controls is powered by ActiveX, formerly known as OLE. This chapter draws, in part, on the ActiveX work of the earlier chapters. An ActiveX control is similar to an ActiveX Automation server, but it also exposes events, and these enable the control to direct the behavior of the container.

ActiveX controls take the place that VBX controls held in 16-bit Windows programming, allowing programmers to extend the control set provided by the compiler. The original purpose of VBX controls was to allow programmers to provide unusual interface controls to their users. Controls that looked like gas gauges or volume knobs became easy to develop. But almost immediately, VBX programmers moved beyond simple controls to modules that involved significant amounts of calculation and processing. In the same way, many ActiveX controls are far more than just controls; they are components that can be used to build powerful applications quickly and easily.

A Rolling-Die Control

The sample application for this chapter is to be a die, one of a pair of dice. Imagine a picture of a cubic die with the familiar pattern of dots indicating the current value, between 1 and 6. When the user clicks the picture, a new number, chosen randomly, is shown. You might implement one or more dice into any game program.

Building the Control Shell

The process of building this die control starts, as always, with AppWizard. Start Developer Studio and then choose File, New. From the list that appears, choose Project Workspace and click OK, or double-click Project Workspace. Click OLE Control Wizard in the list at the left of the dialog box and fill in a project name at the top, choose an appropriate folder for the project files, and then click Create. Figure 26.1 shows the completed dialog box, with the project name Dieroll.

Figure 26.1 : AppWizard makes creating an ActiveX control simple.

NOTE
Even though the technology is now called ActiveX, the AppWizard dialogs still refer to OLE. Many of the class names that are used throughout this chapter have Ole in their names, and comments refer to OLE. While Microsoft has changed the name of the technology, it has not propagated that change throughout Visual C++ yet. You will have to live with these contradictions until the next release of Visual C++.

There are two steps in the ActiveX control wizard. Fill out the first dialog box as shown in Figure 26.2. You want one control, no runtime licensing, source-file comments, and no help files. After you have completed the dialog box, click Next.

Figure 26.2 : AppWizard's first step sets the basic parameters of your control.

Runtime Licensing

Many developers produce controls as a product that they sell. Other programmers buy the rights to use the control in their program. Imagine that a developer, Alice, produces a fantastic die control and sells it to Bob, who incorporates it into the best backgammon game ever. Carol buys the backgammon game and loves the die control, and she decides that it would be perfect for a children's board game she is planning. Since the file DIEROLL.OCX is in the backgammon package, there is nothing (other than ethics) to stop her from doing this.

Runtime licensing is simple: There is a second file, DIEROLL.LIC, that contains the licensing information. Without that file, a control cannot be embedded into a form or program, though a program into which the control is already embedded will work perfectly. Alice ships both DIEROLL.OCX and DIEROLL.LIC to Bob, but their licensing agreement states that only DIEROLL.OCX goes out with the backgammon game. Now Carol can admire DIEROLL.OCX, and it will work perfectly in the backgammon game, but if she wants to include it in the game she builds, she'll have to buy a license from Alice.

You arrange for runtime licensing with AppWizard when you first build the control. If you decide after the control is already built that you should have asked for runtime licensing after all, build a new control with licensing and copy your changes into that control.

The second and final AppWizard step allows you to set the features of the new control. Make sure that Activates When Visible, Available in "Insert Object" Dialog, and Has an "About Box" are selected, as shown in Figure 26.3, and then click Finish. AppWizard summarizes your settings in a final dialog box. Click OK, and AppWizard creates 18 files for you and adds them to a project to make them easy to work with. These files are ready to compile, but they don't do anything at the moment. You have an empty shell, and it is up to you to fill it.

Figure 26.3 : AppWizard's second step governs the appearance and behavior of your control.

AppWizard's Code

Eighteen files sounds like a lot, but it isn't. There are only three classes: CDierollApp, CDierollCtrl, and CDierollPropPage. They take up six files; the other 12 are the project file, make file, resource file, ClassWizard database, ODL file, and so on.

CDierollApp  CDierollApp is a very small class. It inherits from COleControlModule and provides overrides of InitInstance() and ExitInstance() that do nothing but call the base-class versions of these functions. This is where you find _tlid, the external globally unique ID for your control, and some version numbers that make delivering upgrades of your control simpler. The lines in Dieroll.cpp that set up these identifiers are the following:

const GUID CDECL BASED_CODE _tlid =

          { 0x46646b40, 0xea16, 0x11cf, { 0x87, 0xc, 0, 0x20, 0x18, 0x1, 0xdd,›;0xd6 } };

const WORD _wVerMajor = 1;

const WORD _wVerMinor = 0;

CDierollCtrl  The CDierollCtrl class inherits from COleControl, and it overrides the constructor and destructor, plus these four functions:

None of the code for these functions is particularly interesting. However, some of the maps that have been added to this class are interesting. There is an empty message map, ready to accept new entries, and an empty dispatch map, ready for the properties and methods that you choose to expose.

TIP
Message maps were explained in the "Message Maps" section of Chapter 5 "Messages and Commands." Dispatch maps are discussed in the "AppWizard's Automation Boilerplate" section in Chapter 25, "ActiveX Automation."

Below the empty message and dispatch maps comes a new map, the event map. The event map in the header file is shown in Listing 26.1 and the source file event map is shown in Listing 26.2.


Listing 26.1  Excerpt from DierollCtl.h-Event Map

// Event maps

     //{{AFX_EVENT(CDierollCtrl)

          // NOTE - ClassWizard will add and remove member functions here.

          //    DO NOT EDIT what you see in these blocks of generated code !

     //}}AFX_EVENT

     DECLARE_EVENT_MAP()



Listing 26.2  Excerpt from DierollCtl.cpp-Event Map

BEGIN_EVENT_MAP(CDierollCtrl, COleControl)

     //{{AFX_EVENT_MAP(CDierollCtrl)

     // NOTE - ClassWizard will add and remove event map entries

     //    DO NOT EDIT what you see in these blocks of generated code !

     //}}AFX_EVENT_MAP

END_EVENT_MAP()


Event maps, like message maps and dispatch maps, link real-world happenings to your code. Message maps catch things the user does, like choosing a menu item or clicking a button. They also catch messages sent from one part of an application to another. Dispatch maps direct requests to access properties or invoke methods of an OLE Automation server or ActiveX control. Event maps direct notifications from an ActiveX control to the application that contains them (and are discussed in more detail later in this chapter).

There's one more piece of code worth noting in DierollCtl.cpp, shown in Listing 26.3.


Listing 26.3  Excerpt from DierollCtl.cpp-Property Pages

/////////////////////////////////////////////////////////////////////////////

// Property pages



// TODO: Add more property pages as needed.  Remember to increase the count!

BEGIN_PROPPAGEIDS(CDierollCtrl, 1)

     PROPPAGEID(CDierollPropPage::guid)

END_PROPPAGEIDS(CDierollCtrl)


The code in Listing 26.3 is part of the mechanism that implements powerful and intuitive property pages in your controls. That mechanism is discussed later in this chapter.

CDierollPropPage  The entire CDierollPropPage class is the property of ClassWizard. Like any class with a dialog box in it, it has significant data-exchange components. The constructor will initialize the dialog-box fields using code added by ClassWizard, shown in Listing 26.4.

Listing 26.4  DierollPropPage.cpp-CDierollPropPage::CDierollPropPage()

CDierollPropPage::CDierollPropPage() :

     COlePropertyPage(IDD, IDS_DIEROLL_PPG_CAPTION)

{

     //{{AFX_DATA_INIT(CDierollPropPage)

     // NOTE: ClassWizard will add member initialization here

     //    DO NOT EDIT what you see in these blocks of generated code !

     //}}AFX_DATA_INIT

}


The DoDataExchange() function moderates the exchange of data between CDierollPropPage, which represents the dialog box that is the property page, and the actual boxes on the user's screen. It, too, is written by ClassWizard-see Listing 26.5.


Listing 26.5  DierollPropPage.cpp-CDierollPropPage::DoDataExchange()

void CDierollPropPage::DoDataExchange(CDataExchange* pDX)

{

     //{{AFX_DATA_MAP(CDierollPropPage)

     // NOTE: ClassWizard will add DDP, DDX, and DDV calls here

     //    DO NOT EDIT what you see in these blocks of generated code !

     //}}AFX_DATA_MAP

     DDP_PostProcessing(pDX);

}


There is, not surprisingly, a message map for CDierollPropPage, and some registration code, shown in Listing 26.6, that will enable the OLE framework to call this code when a user edits the properties of the control.


Listing 26.6  DierollPropPage.cpp-CDierollPropPage::DoDataExchange()

/////////////////////////////////////////////////////////////////////////////

// Initialize class factory and guid



IMPLEMENT_OLECREATE_EX(CDierollPropPage, "DIEROLL.DierollPropPage.1",

     0x46646b44, 0xea16, 0x11cf, 0x87, 0xc, 0, 0x20, 0x18, 0x1, 0xdd, 0xd6)





/////////////////////////////////////////////////////////////////////////////

// CDierollPropPage::CDierollPropPageFactory::UpdateRegistry -

// Adds or removes system registry entries for CDierollPropPage



BOOL CDierollPropPage::CDierollPropPageFactory::UpdateRegistry(BOOL bRegister)

{

     if (bRegister)

          return AfxOleRegisterPropertyPageClass(AfxGetInstanceHandle(),

               m_clsid, IDS_DIEROLL_PPG);

     else

          return AfxOleUnregisterClass(m_clsid, NULL);

}


Designing the Control

Typically, a control has internal data (properties) and shows them in some way to the user. The control takes input from the user that changes its internal data and perhaps the way the control looks. Some controls present data to the user from other sources, such as databases or remote files. The only internal data that makes sense for the die-roll control, other than some appearance settings that we'll cover later, is a single integer between 1 and 6 representing the current number showing in the die. Eventually, the control will show a dot pattern like a real-world die, but the first implementation of OnDraw() will simply display the digit. Another simplification is to hard-code the digit to a single value while coding the basic structure and then add the code to roll the die later, while dealing with input from the user.

Displaying the Current Value

Before you can display the value, the control must have a value to display. That involves adding a property to the control and then writing the drawing code.

Adding a Property

OCX controls have four types of properties:

To add the value to the die-roll control, use ClassWizard to add a custom property called Number. Follow these steps:

  1. Choose View, ClassWizard and then click the OLE Automation tab.
  2. Make sure that the drop-down list box at the upper-left of the dialog box is set to Dieroll (unless you chose a different name when building the control with AppWizard) and that the right-hand box has the class name CDieRollCtrl.
  3. Click the Add Property button and fill in the dialog box as shown in Figure 26.4.
    Figure 26.4 : ClassWizard simplifies the process of adding a custom property to your die-rolling control.

  4. Type Number into the External Name combo box and notice how ClassWizard fills in suggested values for the Variable name and Notification function boxes.
  5. Select short for the type.
  6. Click OK to close the Add Property dialog box and OK to close ClassWizard.

Before you can write code to display the value of the Number property, it needs to have a value. OCX properties are initialized in DoPropExchange(). This method actually implements persistence; that is, it allows the control to be saved as part of a document and read back in when the document is opened. Whenever a new control is created, the properties cannot be read from a file, so they are set to the default values provided in this method. Controls do not have a Serialize() method.

AppWizard generated a skeleton DoPropExchange() method whose code is in Listing 26.7.


Listing 26.7  DierollCtl.cpp-CDierollCtrl::DoPropExchange()

void CDierollCtrl::DoPropExchange(CPropExchange* pPX)

{

     ExchangeVersion(pPX, MAKELONG(_wVerMinor, _wVerMajor));

     COleControl::DoPropExchange(pPX);



     // TODO: Call PX_ functions for each persistent custom property.



}


Notice the use of the version numbers to be sure that a file holding the values was saved by the same version of the control. Take away the TODO comment that AppWizard left for you, and add this line:


PX_Short( pPX, "Number",  m_number, (short)3 );

PX_Short() is one of many property-exchange functions that you can call-one for each property type that is supported. The parameters you supply are as follows:

The following are the PX_ functions:

PX_Blob() (for binary large object [BLOB] types)
PX_Bool()
PX_Color()
PX_Currency()
PX_Double()
PX_Font()
PX_Float()
PX_IUnknown()
(for LPUNKNOWN types)
PX_Long()
PX_Picture()
PX_Short()
PX_String()
PX_ULong()
PX_UShort()

Filling in the property's default value is simple for some properties, but not so simple for others. For example, set colors with the RGB() macro, which takes values for red, green, and blue from 0 to 255 and returns a COLORREF. Say that you had a property with the external name EdgeColor and the internal name m_edgecolor, and you wanted the property to default to gray. You would code that like the following:


PX_Short( pPX, " EdgeColor ", m_edgecolor, RGB(128,128,128) );

Controls with font properties should, by default, set the font to whatever the container is using. To get this font, call the COLEControl method AmbientFont().

Writing the Drawing Code

The code to display the number belongs in the OnDraw() method of the control class, CDierollCtrl. (Controls do not have documents or views.) This function is called automatically whenever Windows needs to repaint the part of the screen that includes the control. AppWizard generated a skeleton of this method too, shown in Listing 26.8.


Listing 26.8  DierollCtl.cpp-CDierollCtrl::OnDraw()

void CDierollCtrl::OnDraw(CDC* pdc, const CRect& rcBounds, 

      const CRect& rcInvalid)

{

    // TODO: Replace the following code with your own drawing code.

    pdc->FillRect(rcBounds, 

    CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));

    pdc->Ellipse(rcBounds);

}


As discussed in the "Scrolling windows" section of Chapter 11, "Drawing on the Screen," the framework passes the function a device context to draw in, a CRect describing the space occupied by your control, and another CRect describing the space that has been invalidated. The code in Listing 26.8 draws a white rectangle throughout rcBounds and then draws an ellipse inside that rectangle, using the default foreground color. Although you can keep the white rectangle for now, rather than draw an ellipse on it, draw a character that corresponds to the value in Number. To do that, replace the last line in the skeletal OnDraw() with these lines:


CString val; //character representation of the short value

val.Format("%i",m_number);

pdc->ExtTextOut( 0, 0, ETO_OPAQUE, rcBounds, val, NULL );

These lines of code convert the short value in m_number (which you associated with the Number property on the Add Property dialog box) to a CString variable called val, using the new CString::Format() function (which eliminates one of the last uses of sprintf() in C++ programming). The function ExtTextOut() draws a piece of text-the character in val-within the rcBounds rectangle. As the die-roll control is written now, that number will always be 3.

You can build and test the control right now if you would like to see how little it takes to make a control that does something. Unlike the other OLE applications, a control is not run as a stand-alone application in order to register it. Build the project and fix any typing mistakes that you may have made. Choose Tools, OLE Control Test Container to bring up the control test container, shown in Figure 26.5.

Figure 26.5 : The OLE Control Test Container is the ideal place to test your control.

NOTE
If the Tools menu in Developer Studio does not include an OLE Control Test Container item, you can add it to the menu by following these steps:
  1. Choose Tools, Customize.
  2. Click the Tools tab.
  3. Look over the list of tools and make sure that OLE Control Test Container isn't there.
  4. Click Add.
  5. On the Add Tools dialog box that appears, click Browse.
  6. Browse to your CD, or to the hard drive on which you installed Visual C++, and to the \MSDEV\BIN folder. Highlight tstcon32.exe and click OK to finish browsing.
  7. Click OK on the Add Tools dialog box.
  8. There should be no need to adjust the fields on the Tools tab for the test container entry. Click Close.
After you have installed the test container once, you will not need to do so again. By bringing up the test container from within Developer Studio like this, you make it simpler to load your die-roll control into the test container.

Within the test container, choose Edit, Insert OLE Control and then choose Dieroll Control from the list that is displayed. As Figure 26.6 shows, the control appears as a white rectangle displaying a small number 3. You can move and resize this control within the container, but that little 3 stays doggedly in the upper-left corner. The next step is to make that number change when a user clicks the die.

Figure 26.6 : By adding one property and changing two functions, you have transformed the empty shell into a control that displays a 3.

Reacting to a Mouse Click and Rolling the Die

There are actually two things that you want your control to do when the user clicks the mouse on the control: to inform the container that the control has been clicked and to roll the die and display the new internal value.

Notifying the Container

Let's tackle using an event to notify a container first. Events are how controls notify the container of a user action. Just as there are stock properties, there are stock events. These events are already coded for you:

The best way to tell the container that the user has clicked over the control is to fire a Click stock event. The first thing to do is to add it to the control with ClassWizard. Follow these steps:

  1. Bring up ClassWizard by choosing View, ClassWizard and click the OLE Events tab.
  2. Click the Add Event button and fill in the Add Event dialog box, as shown in Figure 26.7.
    Figure 26.7 : ClassWizard helps you add events to your control.

  3. The external name is Click; choose it from the drop-down list box and notice how the internal name is filled in as FireClick.
  4. Click OK to add the event, and your work is done.

Now when the user clicks the control, the container class will be notified. So, if you are writing a backgammon game, for example, the container can respond to the click by using the new value on the die to evaluate possible moves or do some other backgammon-specific task.

The second part of reacting to clicks involves actually rolling the die and redisplaying it. Not surprisingly, ClassWizard helps implement this. When the user clicks over your control, you catch it with a message map entry, just as with an ordinary application. ClassWizard should still be up, but if not, bring it up, and follow these steps:

  1. Select the Message Maps tab this time and make sure that your control class, CDierollCtrl, is selected in the Class Name combo box.
  2. Scroll through the Messages list box until you find the WM_LBUTTONDOWN message, which is generated by Windows whenever the left mouse button is clicked over your control.
  3. Click Add Function to add a function that will be called automatically whenever this message is generated-in other words, whenever the user clicks your control. This function must be named OnLButtonDown(), so there is no dialog box asking you to confirm the name.
  4. ClassWizard has made a stub for you; click the Edit Code button to close ClassWizard and look at the new OnLButtonDown() code. Here's the stub:
    void CDierollCtrl::OnLButtonDown(UINT nFlags, CPoint point)
    {
    // TODO: Add your message handler code here and/or call default

    COleControl::OnLButtonDown(nFlags, point);
    }
  5. Replace the TODO comment with a call to a new function, Roll(), (this function will return a random number between 1 and 6) that you will write in the next section:
    m_number = Roll();
  6. To force a redraw, next add this line:
    InvalidateControl();
  7. Leave the call to COleControl::OnLButtonDown() at the end of the function; it takes care of the rest of the work of processing the mouse click.

Rolling the Die

To add Roll() to CDierollCtrl, right-click on CDierollCtrl in the ClassView pane and then choose Add Function from the shortcut menu that appears. As shown in Figure 26.8, Roll() should be a public function that takes no parameters and returns a short.

Figure 26.8 : Use the Add Member Function dialog box to speed routine tasks.

What should Roll() do? It should calculate a random value between 1 and 6. The C++ function that returns a random number is rand(), which returns an integer between 0 and RAND_MAX. Dividing by RAND_MAX + 1 gives a positive number that will always be less than 1, and multiplying by 6 gives a positive number that is less than 6. The integer part of the number will be between 0 and 5, in other words. Adding 1 produces the result that you want: a number between 1 and 6. The code is shown in Listing 26.9.


Listing 26.9  DierollCtl.cpp-CDierollCtrl::Roll()

short CDierollCtrl::Roll(void)

{

     double number = rand();

     number /= RAND_MAX + 1;

     number *= 6;

     return (short)number + 1;

}


NOTE
If RAND_MAX + 1 isn't a multiple of 6, this code will roll low numbers slightly more often than high ones. A typical value for RAND_MAX is 32,767, which means that 1 and 2 will on the average come up 5,462 times in 32,767 rolls, but 3 through 6 will on the average come up 5,461 times. You're neglecting this inaccuracy.
Some die-rolling programs use the modulo function instead of this approach, but it is far less accurate. The lowest digits in the random number are least likely to be accurate. The algorithm used here produces a much more random die roll.

The random number generator must be seeded before it is used, and it's traditional (and practical) to use the current time as a seed value. In DoPropExchange(), add the following line before the call to PX_Short():


srand( (unsigned)time( NULL ) );

Instead of hard-coding the start value to three, call Roll() to determine a random value. Change the call to PX_Short() so that it reads as follows:


PX_Short( pPX, "Number", m_number, Roll());

Build and test the control again in the test container. As you click on the control, the number that is displayed should change with each click. Play around with it a little: Do you ever see a number less than 1 or more than 6? Any surprises at all?

A Better User Interface

Now that the basic functionality of the die-roll control is in place, it's time to neaten it a little. It needs an icon, and it needs to display dots instead of a single digit.

A Bitmap Icon

Because some users of the die-roll control might want to add it to the Control Palette in Visual Basic, you should have an icon to represent it. Actually, AppWizard created one for you already, but it is just an MFC logo that doesn't represent your control in particular. You can create a better one with Developer Studio. Click the ResourceView tab of the Project Workspace window, click the + next to Bitmap, and double-click IDB_DIEROLL. You can now edit the bitmap one pixel at a time. Figure 26.9 shows an icon appropriate for a die. From now on, when you load the die-roll control into the test container, you will see your icon on the toolbar.

Figure 26.9 : The ResourceView of Visual C++ allows you to build your own icon to be added to the Control Palette in Visual Basic.

Displaying Dots

The next step in building this die-roll control is to make the control look like a die. A nice, three-dimensional effect with parts of some of the other sides showing is beyond the reach of an illustrative chapter like this one, but you can at least display a dot pattern.

The first step is to set up a switch statement in OnDraw(). Comment out the three drawing lines and then add the switch statement so that OnDraw() looks like Listing 26.10.


Listing 26.10  DierollCtl.cpp-CDierollCtrl::OnDraw()

void CDierollCtrl::OnDraw(

               CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)

{

     pdc->FillRect(rcBounds,

         CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));

//   CString val; //character representation of the short value

//   val.Format("%i",m_number);

//   pdc->ExtTextOut( 0, 0, ETO_OPAQUE, rcBounds, val, NULL );



     switch(m_number)

     {

     case 1:

          break;

     case 2:

          break;

     case 3:

          break;

     case 4:

          break;

     case 5:

          break;

     case 6:

          break;

     }

}


Now all that remains is to add code to the case 1: block that draws one dot, to the case 2: block that draws two dots, and so on. If you happen to have a real die available, take a close look at it. The width of each dot is about one quarter of the width of the whole die's face. Dots near the edge are about one-sixteenth of the die's width from the edge. All the other rolls except 6 are contained within the layout for 5, anyway; for example, the single dot for 1 is in the same place as the central dot for 5.

The second parameter of OnDraw(), rcBounds, is a CRect that describes the rectangle occupied by the control. It has member variables and functions that return the upper-left coordinates, width, and height of the control. The default code that AppWizard generated called CDC::Ellipse()to draw an ellipse within that rectangle. Your code will call Ellipse() too, passing a small rectangle within the larger rectangle of the control. Your code will be easier to read-and will execute slightly faster-if you work in units that are one-sixteenth of the total width or height. Each dot will be four units wide or high. Add the following code before the switch statement:


int Xunit = rcBounds.Width()/16;

int Yunit = rcBounds.Height()/16;



int Top = rcBounds.top;

int Left = rcBounds.left;

Before drawing a shape by calling Ellipse(), you need to select a tool to draw with. Because your circles should be filled in, they should be drawn with a brush. This code creates a brush, and tells the device context pdc to use it, while saving a pointer to the old brush so that it can be restored later:


CBrush Black;

Black.CreateSolidBrush(RGB(0x00,0x00,0x00)); //solid black brush

CBrush* savebrush = pdc->SelectObject(&Black);

After the switch statement, add this line to restore the old brush:


pdc->SelectObject(savebrush);

Now you're ready to add lines to those case blocks to draw some dots. For example, rolls of 2, 3, 4, 5, or 6 all need a dot in the upper-left corner. This dot will be in a rectangular box that starts one unit to the right and down from the upper-left corner and extends to five units right and down. The call to Ellipse looks like this:


pdc->Ellipse(Left+Xunit, Top+Yunit,

                  Left+5*Xunit, Top + 5*Yunit);

The coordinates for the other dots are determined similarly. The switch statement ends up as shown in Listing 26.11.


Listing 26.11  DierollCtl.cpp-CDierollCtrl::OnDraw()

switch(m_number)

     {

     case 1:

          pdc->Ellipse(Left+6*Xunit, Top+6*Yunit,

                        Left+10*Xunit, Top + 10*Yunit); //center

          break;

     case 2:

          pdc->Ellipse(Left+Xunit, Top+Yunit,

                        Left+5*Xunit, Top + 5*Yunit);   //upper left

          pdc->Ellipse(Left+11*Xunit, Top+11*Yunit,

                        Left+15*Xunit, Top + 15*Yunit); //lower right

          break;

     case 3:

          pdc->Ellipse(Left+Xunit, Top+Yunit,

                        Left+5*Xunit, Top + 5*Yunit);   //upper left

          pdc->Ellipse(Left+6*Xunit, Top+6*Yunit,

                        Left+10*Xunit, Top + 10*Yunit); //center

          pdc->Ellipse(Left+11*Xunit, Top+11*Yunit,

                        Left+15*Xunit, Top + 15*Yunit); //lower right

          break;

     case 4:

          pdc->Ellipse(Left+Xunit, Top+Yunit,

                        Left+5*Xunit, Top + 5*Yunit);   //upper left

          pdc->Ellipse(Left+11*Xunit, Top+Yunit,

                        Left+15*Xunit, Top + 5*Yunit);  //upper right

          pdc->Ellipse(Left+Xunit, Top+11*Yunit,

                        Left+5*Xunit, Top + 15*Yunit);  //lower left

          pdc->Ellipse(Left+11*Xunit, Top+11*Yunit,

                        Left+15*Xunit, Top + 15*Yunit); //lower right

          break;

     case 5:

          pdc->Ellipse(Left+Xunit, Top+Yunit,

                        Left+5*Xunit, Top + 5*Yunit);   //upper left

          pdc->Ellipse(Left+11*Xunit, Top+Yunit,

                        Left+15*Xunit, Top + 5*Yunit);  //upper right

          pdc->Ellipse(Left+6*Xunit, Top+6*Yunit,

                        Left+10*Xunit, Top + 10*Yunit); //center

          pdc->Ellipse(Left+Xunit, Top+11*Yunit,

                        Left+5*Xunit, Top + 15*Yunit);  //lower left

          pdc->Ellipse(Left+11*Xunit, Top+11*Yunit,

                        Left+15*Xunit, Top + 15*Yunit); //lower right

          break;

       case 6:

               pdc->Ellipse(Left+Xunit, Top+Yunit,

                Left+5*Xunit, Top + 5*Yunit);   //upper left

               pdc->Ellipse(Left+11*Xunit, Top+Yunit,

                Left+15*Xunit, Top + 5*Yunit);  //upper right

               pdc->Ellipse(Left+Xunit, Top+6*Yunit,

                Left+5*Xunit, Top + 10*Yunit);  //center left

               pdc->Ellipse(Left+11*Xunit, Top+6*Yunit,

                  Left+15*Xunit, Top + 10*Yunit); //center right

               pdc->Ellipse(Left+Xunit, Top+11*Yunit,

                Left+5*Xunit, Top + 15*Yunit);  //lower left

               pdc->Ellipse(Left+11*Xunit, Top+11*Yunit,

                Left+15*Xunit, Top + 15*Yunit); //lower right

               break;

     }


Build the OCX again and try it out in the test container. You should see something like Figure 26.10, which actually looks like a die!

Figure 26.10 : Your rolling-die control now looks like a die.

If you're sharp-eyed or if you stretch the die very large, you might notice that the pattern of dots is just slightly off center. That's because the height and width of the control are not always an exact multiple of 16. For example, if Width() returned 31, Xunit would be 1, and all the dots would be arranged between positions 0 and 16-leaving a wide blank band at the far right of the control. Luckily, the width is typically far more than 31 pixels, and so the asymmetry is less noticeable. If it bothers you, declare XUnit and YUnit as double variables rather than int and then cast to int only after multiplying (by 6, 11, or 15) within the switch. Your code will be a little less readable with 76 casts, but your die will look more balanced.

Property Sheets

ActiveX controls have property sheets that enable the user to set properties without any change to the container application. You set these up as dialog boxes, taking advantage of prewritten pages for font, color, and other common properties. For this control, the obvious properties to add are the following:

NOTE
It's easy to get confused about what, exactly, a property page is. Is each one of the tabs on a dialog box a separate page? Or is the whole collection of tabs a page? Each tab is called a page and the collection of tabs is called a sheet. You set up each page as a dialog box and use ClassWizard to connect the values on that dialog box to member variables.

Digits versus Dots

It's a simple enough matter to allow the user to choose whether to display the current value as a digit or a dot pattern. Simply add a property that indicates this preference and then use the property in OnDraw(). The user can set the property using the property page.

First, add the property using ClassWizard. Here's how: Bring up ClassWizard and then select the OLE Automation tab. Make sure that the CDierollCtl class is selected and then click Add Property. On the Add Property dialog box, provide the external name Dots and the internal name m_dots. The type should be BOOL, because Dots can be either TRUE or FALSE. Implement this new property as a member variable (direct-access) property. Click OK to complete the Add Property dialog box and click OK to close ClassWizard. The member variable is added to the class, the dispatch map is updated, and a stub is added for the notification function, OnDotsChanged().

To initialize Dots and arrange for it to be saved with a document, add the following line to DoPropExchange() after the call to PX_Short():


PX_Bool( pPX, "Dots", m_dots, TRUE);

Initializing the Dots property to TRUE ensures that the default behavior of the control is to display the dot pattern.

In OnDraw(), uncomment those lines that displayed the digit. Wrap an if around them so the digit is displayed if m_dots is FALSE, and dots are displayed if it is TRUE. The code looks like Listing 26.12.


Listing 26.12  DierollCtl.cpp-CDierollCtrl::OnDraw()

void CDierollCtrl::OnDraw(

               CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid)

{

     pdc->FillRect(rcBounds, 

         CBrush::FromHandle((HBRUSH)GetStockObject(WHITE_BRUSH)));



     if (!m_dots)

     {

        CString val; //character representation of the short value

        val.Format("%i",m_number);

        pdc->ExtTextOut( 0, 0, ETO_OPAQUE, rcBounds, val, NULL );     

     }

     else

     {

         //dots are 4 units wide and high, one unit from the edge

         int Xunit = rcBounds.Width()/16;

         int Yunit = rcBounds.Height()/16;



         int Top = rcBounds.top;

         int Left = rcBounds.left;



         CBrush Black;

         Black.CreateSolidBrush(RGB(0x00,0x00,0x00)); //solid black brush



         CBrush* savebrush = pdc->SelectObject(&Black);

               

         switch(m_number)

         {

         case 1:

                  ...

         }

         pdc->SelectObject(savebrush);

       }

}


To give the user a way to set Dots, you build a property page by following these steps:

  1. Click the ResourceView tab in the Project Workspace window and then click the + next to Dialog.
  2. The OCX has two dialog boxes: one for the About box and one for the property page. Double-click IDD_PROPPAGE_DIEROLL to open it. The boilerplate property page generated by AppWizard is shown in Figure 26.11.
    Figure 26.11 : AppWizard generates an empty property page.

  3. Remove the static control with the TODO reminder by highlighting it and pressing Delete.
  4. Drag a check box from the Control Palette onto the dialog box; choose Edit, Properties; and then pin the Property dialog box in place.
  5. Change the caption to Display Dot Pattern and change the resource ID to IDC_DOTS, as shown in Figure 26.12.
    Figure 26.12 : You build the property page for the die-roll control like any other dialog box.

When the user brings up the property page and clicks to set or unset the check box, that does not directly affect the value of m_dots or the Dots property. To connect the dialog box to member variables, use ClassWizard and follow these steps:

  1. Bring up Class Wizard while the dialog box is still open and on top and then select the Member Variables tab.
  2. Make sure that CDierollPropPage is the selected class and that the IDC_DOTS resource ID is highlighted, and then click the Add Variable button.
  3. Fill in m_dots as the name and BOOL as the type, and fill in the Optional OLE Property Name combo box with Dots, as shown in Figure 26.13.
    Figure 26.13 : You connect the property page to the properties of the control with ClassWizard.

  4. Click OK, and ClassWizard generates code to connect the property page with the member variables in CDierollPropPage::DoDataExchange().

The path that data follows can be a little twisty. When the user brings up the property sheet, the value of TRUE or FALSE is in a temporary variable. Clicking the check box toggles the value of that temporary variable. When the user clicks OK, that value goes into CDierollPropPage::m_dots and also to the OLE Automation property Dots. That property has already been connected to CDierollCtrl:: m_dots, so the dispatch map in CDierollCtrl will make sure that the other m_dots gets changed. Since the OnDraw() function uses CDierollCtrl:: m_dots, the appearance of the control changes in response to the change made by the user on the property page. Having the same name for the two member variables makes things more confusing to first-time control builders, but less confusing in the long run.

This works now. Build the control and insert it into the test container. To change the properties, choose Edit, Dieroll Control Object Properties; your own property page appears, as shown in Figure 26.14. Prove to yourself that the control displays dots or a digit, depending on the setting on this page, by changing the setting, clicking OK, and then watching the control redraw.
Figure 26.14 : Your own property page is displayed by the control test container.

When the control is displaying the value as a number, you might want to display that number in a font that's more in proportion with the current width and height of the control and centered within the control. That's a relatively simple modification to OnDraw(), which we leave for you to investigate.

User-Selected Colors

The die you've created up to this point will always have black dots on a white background, but giving the user control to change this is remarkably simple. You will need a property for the foreground color and another for the background color. These have already been implemented as stock properties: BackColor and ForeColor.

Stock Properties  Here is the complete list of stock properties available to a control that you write:
Ambient Properties  Controls can also access ambient properties, which are properties of the environment that surrounds the control-that is, properties of the container into which you place the control. You cannot change ambient properties, but the control can use them to adjust its own properties; for example, the control can set its background color to match that of the container.

The container provides all support for ambient properties. Any of your code that uses an ambient property should be prepared to use a default value if the container does not support that property. Here's how to use an ambient property called UserMode:


BOOL bUserMode;

    if( !GetAmbientProperty( DISPID_AMBIENT_USERMODE,

        VT_BOOL, &bUserMode ) )

    {

        bUserMode = TRUE;

    }

This code calls GetAmbientProperty() with the display ID (dispid) and variable type (vartype) required. It also provides a pointer to a variable into which to put the value. This variable's type must match the vartype. If GetAmbientProperty() returns FALSE, bUserMode is set to a default value.

olectl.h lists the following dispids:

DISPID_AMBIENT_BACKCOLOR
DISPID_AMBIENT_DISPLAYNAME
DISPID_AMBIENT_FONT
DISPID_AMBIENT_FORECOLOR
DISPID_AMBIENT_LOCALEID
DISPID_AMBIENT_MESSAGEREFLECT
DISPID_AMBIENT_SCALEUNITS
DISPID_AMBIENT_TEXTALIGN
DISPID_AMBIENT_USERMODE
DISPID_AMBIENT_UIDEAD
DISPID_AMBIENT_SHOWGRABHANDLES
DISPID_AMBIENT_SHOWHATCHING
DISPID_AMBIENT_DISPLAYASDEFAULT
DISPID_AMBIENT_SUPPORTSMNEMONICS
DISPID_AMBIENT_AUTOCLIP
DISPID_AMBIENT_APPEARANCE

Remember that not all containers support all these properties; some might not support any, and others might support properties not included in the preceding list.

The vartypes include those shown in Table 26.1.

Table 26.1  Variable Types for Ambient Properties

VartypeDescription
VT_BOOLBOOL
VT_BSTRCString
VT_I2short
VT_I4long
VT_R4float
VT_R8double
VT_CYCY
VT_COLOROLE_COLOR
VT_DISPATCHLPDISPATCH
VT_FONTLPFONTDISP

Remembering which vartype goes with which dispid and checking the return from GetAmbientProperty() is a bothersome process, so the framework provides member functions of COLEControl, to get the most popular ambient properties:

All these functions assign reasonable defaults if the container does not support the requested property.

Implementing BackColor and ForeColor  To add BackColor and ForeColor to the control, follow these steps:
  1. Bring up ClassWizard, and select the OLE Automation tab.
  2. Make sure that CDierollCtrl is the selected class, and click Add Property.
  3. Choose BackColor from the top combo box, and the rest of the dialog box is filled out for you, grayed to remind you that you cannot set any of these fields for a stock property. Figure 26.15 shows the values that are provided for you.
    Figure 26.15 : Stock properties are described for you by Class Wizard.

  4. Click OK and then add ForeColor in the same way. After you click OK, the OLE Automation tab of ClassWizard should resemble Figure 26.16. The S next to these new properties reminds you that they are stock properties.
    Figure 26.16 : Stock properties are highlighted with an S in the OLE Automation list of properties and methods.

  5. Click OK to close Class Wizard.

Setting up the property pages for these colors is almost as simple, because there is a prewritten page that you can use. Look through DierollCtl.cpp for a block of code like Listing 26.13.


Listing 26.13  DierollCtl.cpp-Property Pages

//////////////////////////////////////////////////////////////

// Property pages



// TODO: Add more property pages as needed. 

// Remember to increase the count!

BEGIN_PROPPAGEIDS(CDierollCtrl, 1)

     PROPPAGEID(CDierollPropPage::guid)

END_PROPPAGEIDS(CDierollCtrl)


Remove the TODO reminder, change the count to 2, and add another PROPPAGEID, so that the block looks like Listing 26.14.


Listing 26.14  DierollCtl.cpp-Property Pages

/////////////////////////////////////////////////////////////////////////////

// Property pages



BEGIN_PROPPAGEIDS(CDierollCtrl, 2)

     PROPPAGEID(CDierollPropPage::guid)

    PROPPAGEID(CLSID_CColorPropPage)

END_PROPPAGEIDS(CDierollCtrl)


CLSID_CColorPropPage is a class ID for a property page that is used to set colors. Now when the user brings up the property sheet, there will be two property pages: One to set colors and the general page that you already created. Both ForeColor and BackColor will be available on this page, so all that remains to be done is to use the values that the user sets. You'll get a chance to see that very soon, but first, your code needs to use these colors.

Changes to OnDraw()  In OnDraw(), your code can access the background color with GetBackColor(). This function was added by ClassWizard when you added the stock property, though you can't see it. The dispatch map for CDierollCtrl now looks like this:

Listing 26.15  DierollCtl.cpp-Dispatch map

BEGIN_DISPATCH_MAP(CDierollCtrl, COleControl)

     //{{AFX_DISPATCH_MAP(CDierollCtrl)

     DISP_PROPERTY_NOTIFY(CDierollCtrl, "Number", m_number, OnNumberChanged,›;VT_I2)

     DISP_PROPERTY_NOTIFY(CDierollCtrl, "Dots", m_dots, OnDotsChanged, VT_BOOL)

     DISP_STOCKPROP_BACKCOLOR()

     DISP_STOCKPROP_FORECOLOR()

     //}}AFX_DISPATCH_MAP

     DISP_FUNCTION_ID(CDierollCtrl, "AboutBox", DISPID_ABOUTBOX, AboutBox,

	                                                             ›;VT_EMPTY, VTS_NONE)

END_DISPATCH_MAP()


The macro DISP_STOCKPROP_BACKCOLOR() expands to these lines:


#define DISP_STOCKPROP_BACKCOLOR() \

    DISP_PROPERTY_STOCK(COleControl, "BackColor", \

    DISPID_BACKCOLOR,      COleControl::GetBackColor, \

    COleControl::SetBackColor, VT_COLOR)

This code is calling another macro, DISP_PROPERTY_STOCK, which ends up declaring the GetBackColor() function as a member of CDierollCtrl, which inherits from COleControl. So although you can't see it, this function is available to you. It returns an OLE_COLOR, which you translate to a COLORREF with TranslateColor(). You can pass this COLORREF to CreateSolidBrush() and use that brush to paint the background. Access the foreground color with GetForeColor() and give it the same treatment. (Use SetTextColor() in the digit part of the code.) Here's how OnDraw ends up (with most of the switch statement cropped out):


Listing 26.16  DierollCtl.cpp-CDierollCtrl::OnDraw()

void CDierollCtrl::OnDraw(CDC* pdc, const CRect& rcBounds,

                          const CRect& rcInvalid)

{

     COLORREF back = TranslateColor(GetBackColor());

     CBrush backbrush;

     backbrush.CreateSolidBrush(back);

     pdc->FillRect(rcBounds, &backbrush);



     if (!m_dots)

     {

        CString val; //character representation of the short value   

        val.Format("%i",m_number);

        pdc->SetTextColor(TranslateColor(GetForeColor()));

        pdc->ExtTextOut( 0, 0, ETO_OPAQUE, rcBounds, val, NULL );

     }

     else

     {

         //dots are 4 units wide and high, one unit from the edge

         int Xunit = rcBounds.Width()/16;

         int Yunit = rcBounds.Height()/16;



         int Top = rcBounds.top;

         int Left = rcBounds.left;



         COLORREF fore = TranslateColor(GetForeColor());

         CBrush forebrush;

         forebrush.CreateSolidBrush(fore);



         CBrush* savebrush = pdc->SelectObject(&forebrush);

               

         switch(m_number)

               ...

     }

}


Build the control once again, insert it into the test container, and again bring up the property sheet by choosing Edit, Dieroll Control Object Properties. As Figure 26.17 shows, the new property page is just fine for setting colors. Change the foreground and background colors a few times and experiment with both dots and digit display to exercise all your new code.

Figure 26.17 : Stock property pages make short work of letting the user set colors.

Future Improvements

The die-rolling control may seem complete, but it could be made even better.

Enable and Disable Rolling

In many dice games, you can roll the die only when it is your turn. At the moment, this control rolls whenever it is clicked, no matter what. Buy adding a custom property called RollAllowed, you can allow the container to control the rolling. When RollAllowed is FALSE, CDieCtrl::OnLButtonDown should just return without rolling and redrawing. Perhaps OnDraw should draw a slightly different die (gray dots?) when RollAllowed is FALSE. You decide; it's your control. The container would set this property like any OLE Automation property, according to the rules of the game in which the control is embedded.

Dice with Unusual Numbers of Sides

And why restrict yourself to six-sided dice? There are dice that have 4, 8, 12, 20, and even 30 sides; wouldn't they make an interesting addition to a dice game? You'll need to get one of these odd dice so that you can see what it looks like and change the drawing code in CDierollCtrl::OnDraw(). You'll then need to change the hard-coded 6 in Roll() to a custom property: An integer with the external name Sides and a member variable m_sides. Don't forget to change the property page to allow the user to set Sides, and add a line to CDieCtrl::DoPropExchange() to make Sides persistent and initialize it to 6.

TIP
There is such a thing as a two-sided die; it's commonly called a coin.

Arrays of Dice

If you were writing a backgammon game, you would need two dice. One approach would be to embed two individual die controls. But how would you synchronize them so that they both rolled at once with a single click? Why not expand the control to be an array of dice? The number of dice would be another custom property, and the control would roll the dice all at once. The RollAllowed flag would apply to all of the dice, as would Sides, so that you could have two six-sided dice or three 12-sided dice, but not two four-sided dice and a 20-sider. Number would become an array.

From Here…

The die-roll control presented in this chapter is not significantly different from the OLE controls of earlier versions of Visual C++. Even the documentation can't seem to decide whether to call them OLE controls or ActiveX controls. Still, you will want to incorporate ActiveX controls into your programming repertoire so that you can build a user interface that is just what you want. To learn more about some related topics, check these chapters: