ActiveX controls replace OLE controls, though the change affects more the name than 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 previous chapters. An ActiveX control is similar to an ActiveX Automation server, but an ActiveX control also exposes events, and those enable the control to direct the containerís behavior.
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 their users with unusual interface controls. Controls that looked like gas gauges or volume knobs became easy to develop. Almost immediately, however, 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.
The sample application for this chapter rolls a die. This section describes the control and starts the building process.
Drawing a control and adding a property to a control are explained in this section.
ActiveX controls notify their containers of user activities with events. This section explains events and works you through the process of getting your control to roll a new number whenever a user clicks the control.
Adding more properties gives the user more flexibility. This section shows you how.
Adding property sheets to a control is quite simple, with much of the work already done for you. This section demonstrates the process.
The sample application for this chapter is 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, randomly-chosen number 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. Click the Project tab, then click MFC ActiveX ControlWizard, which is in the list at the left of the dialog box; fill in a project name at the top, choose an appropriate folder for the project files, and then click OK. Figure 17.1 shows the completed dialog box, with the project name Dieroll.
Even though the technology is now called ActiveX, many of the class names that are used throughout this chapter have Ole in their names, and comments refer to OLE. Though Microsoft has changed the name of the technology, it has not yet propagated that change throughout Visual C++. You will have to live with these contradictions until the next release of Visual C++.
Fig. 17.1 AppWizard makes creating an ActiveX control simple.
There are two steps in the ActiveX control wizard. Fill out the first dialog box as shown in Figure 17.2: you want one control, no runtime licensing, source-file comments, and no help files. After you have completed the dialog box, click Next.
Fig. 17.2 AppWizard's first step sets your controlís basic parameters.
Runtime Licensing
Many developers produce controls as a product that they sell. Other programmers buy the rights to use such a control in their programs. 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 DIEROLL.OCX file 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 new controlís features. Make sure that Activates When Visible, Available in "Insert Object" Dialog, and Has an "About Box" are selected, as shown in Figure 17.3, and then click Finish. AppWizard summarizes your settings in a final dialog box. Click OK, and AppWizard creates 19 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; it is up to you to fill it.
Fig. 17.3 AppWizard's second step governs the appearance and behavior of your control.
AppWizard's Code
Nineteen 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 13 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 =
{ 0x914b21a5, 0x7946, 0x11d0, { 0x9b, 0x1, 0, 0x80, 0xc8, 0x1a, 0x39, 0x7c } };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, which is ready to accept new entries, and an empty dispatch map, which is ready for the properties and methods that you choose to expose.
Message maps were explained in the "Message Maps" section of Chapter 4, "Messages and Commands." Dispatch maps are discussed the "AppWizard's Automation Boilerplate" section in Chapter 16, "Building an Automation Server."
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 17.1, and the source file event map is shown in Listing 17.2.
Listing 17.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 17.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, such as 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 Automation server or ActiveX control. Event maps direct notifications from an ActiveX control to the application that contains the control (and are discussed in more detail later in this chapter).
There's one more piece of code worth noting in DierollCtl.cpp. It is shown in Listing 17.3.
Listing 17.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 17.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. This code is shown in Listing 17.4.
Listing 17.4óDierollPpg.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 17.5.
Listing 17.5óDierollPpg.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 17.6), that will enable the ActiveX framework to call this code when a user edits the controlís properties.
Listing 17.6óDierollPpg.cppóCDierollPropPage::UpdateRegistry()
/////////////////////////////////////////////////////////////////////////////
// Initialize class factory and guid
IMPLEMENT_OLECREATE_EX(CDierollPropPage, "DIEROLL.DierollPropPage.1",
0x914b21a8, 0x7946, 0x11d0, 0x9b, 0x1, 0, 0x80, 0xc8, 0x1a, 0x39, 0x7c)
/////////////////////////////////////////////////////////////////////////////
// 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 user provides input to the control to change 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 is covered later, is a single integer between 1 and 6 that represents 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; add the code to roll the die later, while dealing with input from the user.
Before the value can be displayed, 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
ActiveX 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:
Fig. 17.4 ClassWizard simplifies the process of adding a custom property to your die-rolling control.
Type Number into the External Name combo box and notice how ClassWizard fills in suggested values for the Variable name and Notification function boxes.
Before you can write code to display the value of the Number property, the property must have a value to display. Control 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 17.7.
Listing 17.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() (OLE_COLOR)
PX_Currency()
PX_DATAPATH (CDataPathProperty)
PX_Double()
PX_Float()
PX_Font()
PX_IUnknown() (for LPUNKNOWN types, COM interface pointer)
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, you 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 17.8.
Listing 17.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 6, "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 17.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 ExtTextOut() function 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 effort it takes to make a control that does something. Unlike the other ActiveX 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, ActiveX Control Test Container to bring up the control test container, shown in Figure 17.5.
Fig. 17.5 The ActiveX control test container is the ideal place to test your control.
If the Tools menu in Developer Studio does not include an ActiveX Control Test Container item, you can add it to the menu by following these steps:
- Choose Tools, Customize.
- Click the Tools tab.
- Look over the list of tools and make sure that ActiveX Control Test Container isn't there.
- Go to the bottom of the list and double-click the empty entry.
- Type Activ&eX Control Test Container in the entry and press Enter.
- Click the ... button to the right of the Command box and browse to your CD, or to the hard drive on which you installed Visual C++, and to the BIN folder beneath the Developer Studio folder. Highlight tstcon32.exe and click OK to finish browsing. On many systems the full path will be C:\Program Files\DevStudio\VC\BIN\TSTCON32.EXE. Your system may be different.
- Click the rightward-pointing arrow beside the Initial Directory box and choose Target Directory from the list that appears.
- Make sure that the three check boxes across the bottom of the directory are not selected.
- Click the Close button.
If you have not built a release version, and your target is a release version, or if you have not built a debug version and your target is a release version, you will get an error message when you choose Tools, ActiveX Control Test Container. Simply build the control and you will be able to choose the menu item.
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 displayed list. As Figure 17.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.
Fig. 17.6 By adding one property and changing two functions, you have transformed the empty shell into a control that displays a 3.
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 first tackle using an event to notify a container. 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:
Fig. 17.7 ClassWizard helps you add events to your control.
The external name is Click; choose it from the drop-down list box and notice how the internal name is filled in as FireClick.
You may notice the ClassView pane has a new addition: two icons resembling handles. Click the + next to DDierollEvents to see that Click is now listed as an event for this application, as shown in Figure 17.8.
Fig. 17.8 ClassView displays events as well as classes.
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; if it is not, bring it up and follow these steps:
void CDierollCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
COleControl::OnLButtonDown(nFlags, point);
}
Replace the TODO comment with a call to a new function, Roll(), that you will write in the next section. This function will return a random number between 1 and 6.
m_number = Roll();
To force a redraw, next add this line:
InvalidateControl();
Leave the call to COleControl::OnLButtonDown() at the end of the function; handles the rest of the work involved in processing the mouse click.
Rolling the Die
To add Roll() to CDierollCtrl, right-click on CDierollCtrl in the ClassView pane, and then choose Add Member Function from the shortcut menu that appears. As shown in Figure 17.9, Roll() should be a public function that takes no parameters and returns a short.
Fig. 17.9 Use the Add 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 17.9.
Listing 17.9óDierollCtl.cppóCDierollCtrl::Roll()
short CDierollCtrl::Roll(void)
{
double number = rand();
number /= RAND_MAX + 1;
number *= 6;
return (short)number + 1;
}
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. However, 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 3, 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 the control, the displayed number 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?
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 die-roll control users might want to add this control to the Control Palette in Visual Basic or Visual C++, you should have an icon to represent it. Actually, AppWizard has already created one, but it is simply an MFC logo that doesn't represent your control in particular. You can create a more specialized 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 17.10 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.
Fig. 17.10 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 17.10.
Listing 17.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 adding 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 to you, 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 with which to draw. 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 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 show in in Listing 17.11.
Listing 17.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 similar to Figure 17.11, which actually looks like a die!
Fig. 17.11 Your rolling-die control now looks like a die.
If you're sharp-eyed or if you stretch the die very small, you might notice that the pattern of dots is just slightly off-center. That's because the controlís height and width 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.
To fix this, center the dots in the control. Find the lines that calculate Xunit and Yunit, then add the new lines from the code fragment in Listing 17.12.
Listing 17.12óDierollCtl.cppóAdjusting Xunit and Yunit
//dots are 4 units wide and high, one unit from the edge
int Xunit = rcBounds.Width()/16;
int Yunit = rcBounds.Height()/16;
int Xleft = rcBounds.Width()%16;
int Yleft = rcBounds.Height()%16;
// adjust top left by amount left over
int Top = rcBounds.top + Yleft/2;
int Left = rcBounds.left + Xleft/2;
Xleft and Yleft are the "leftovers" in the X and Y direction. By moving Top and Left over by half the leftover, we centre the dots in the control without having to change any other code.
ActiveX controls have property sheets that enable the user to set properties without any change to the container application. (Property sheets and pages are discussed in Chapter 12, ìProperty Pages and Sheets and Wizards.î) 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:
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 select the Automation tab. Make sure that the CDierollCtrl 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 17.13.
Listing 17.13ó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 Xleft = rcBounds.Width()%16;
int Yleft = rcBounds.Height()%16;
// adjust top left by amount left over
int Top = rcBounds.top + Yleft/2;
int Left = rcBounds.left + Xleft/2;
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:
Fig. 17.12 AppWizard generates an empty property page.
Remove the static control with the TODO reminder by highlighting it and pressing Delete.
Fig. 17.13 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:
Fig. 17.14 You connect the property page to the properties of the control with ClassWizard.
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 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 makes it 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, and Properties; your own property page should appear, as shown in Figure 17.15. Prove to yourself that the control displays dots or a digit, depending on the pageís setting, by changing the setting, clicking OK, and then watching the control redraw.
Fig. 17.15 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 controlís current width and height and centered within the control. That's a relatively simple modification to OnDraw(), which you can investigate on your own.
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 the value is placed. This variable's type must match the vartype. If GetAmbientProperty() returns FALSE, bUserMode is set to a default value.
The following dispids are displayed in olectl.h. :
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 of these properties; some might not support any, and still others might support properties not included in the preceding list.
The vartypes include those shown in Table 17.1.
Table 17.1óVariable Types for Ambient Properties
Vartype |
Description |
VT_BOOL |
BOOL |
VT_BSTR | CString |
VT_I2 | short |
VT_I4 | long |
VT_R4 | float |
VT_R8 | double |
VT_CY | CY |
VT_COLOR | OLE_COLOR |
VT_DISPATCH | LPDISPATCH |
VT_FONT |
LPFONTDISP |
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:
Fig. 17.16 Stock properties are described for you by Class Wizard.
Click OK and then add ForeColor in the same way. After you click OK, ClassWizardís Automation tab should resemble Figure 17.17. The S next to these new properties reminds you that they are stock properties.
Fig. 17.17 Stock properties are highlighted with an S in the OLE Automation list of properties and methods.
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 17.14.
Listing 17.14ó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 17.15.
Listing 17.15ó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 using the values that are set by the user. You 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(). Though you canít see it, this function was added by ClassWizard when you added the stock property. The dispatch map for CDierollCtrl now looks like Listing 17.16.
Listing 17.16ó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.) Listing 17.17 shows the completed OnDraw (with most of the switch statement cropped out).
Listing 17.17ó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 17.18 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.
Fig. 17.18 Stock property pages make short work of letting the user set colors.
ActiveX controls expose methods (functions) just as Automation servers do. This control rolls when the user clicks it, but you might want the container application to request a roll without the userís intervention. To do this, you add a function called DoRoll() and expose it.
Bring up ClassWizard, click the Automation tab, then click Add Method. Name the new function DoRoll, select Return type of void, then when it is added, click Edit Code and fill it in like this:
void CDierollCtrl::DoRoll()
{
m_number = Roll();
InvalidateControl();
}
This simple code just rolls the die and requests a redraw. Not everything about ActiveX controls needs to be difficult!
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. By 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 Automation property, according to the rules of the game in which the control is embedded.
Dice with Unusual Numbers of Sides
Why restrict yourself to six-sided dice? There are dice that have four, eight, 12, 20, and even 30 sides; wouldn't they make an interesting addition to a dice game? You'll need to get one pair of these odd dice so that you can see what they look like and change the drawing code in CDierollCtrl::OnDraw(). You 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.
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.
In Chapter 20, ìBuilding an Internet ActiveX Control,î you discover one way to synchronize two or more separate dice within one control container, and some of the difficulties involved.
The die-roll control presented in this chapter is not significantly different from the OLE controls of earlier versions of Visual C++. Even in the documentation there has not been a decision as to whether they should be called OLE controls or ActiveX controls. No matter what they are called, 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:
© 1997, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.