ActiveX controls replace OLE controls, though the change affects the name more than anything else. (Much of the Microsoft documentation still refers to OLE controls.) The exciting behavior of these controls is powered by COM (the Component Object Model), which also powers OLE. This chapter draws, in part, on the work of the previous chapters. An ActiveX control is similar to an 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, enabling programmers to extend the control set provided by the compiler. The original purpose of VBX controls was to enable programmers to provide their users with unusual interface controls. Controls that look 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 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 use one or more dice in any game program.
The process of building this die control starts, as always, with AppWizard. Begin Developer Studio and then choose File, New. Click the Projects tab and 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 click OK. Figure 17.1 shows the completed dialog box, with the project name Dieroll.
FIG. 17.1 AppWizard makes creating an ActiveX control simple.
NOTE: Even though the technology is now called ActiveX, many classnames used throughout this chapter have Ole in their names, and comments refer to OLE. Though Microsoft has changed the technology's name, 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++.
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 LicensingMany developers produce controls as a salable product. Other programmers buy the rights to use such controls 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. Because 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 can't 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 enables 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's up to you to fill it.
FIG. 17.3 AppWizard's second step governs your control's appearance and behavior.
Nineteen files sound like a lot, but they aren'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 has a constructor and destructor, plus overrides for 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 of interest. 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 are explained in the "Message Maps" section of Chapter 3, "Messages and Commands." Dispatch maps are discussed in 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. Listing 17.1 shows the event map in the header file, and the source file event map is shown in Listing 17.2.
// 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()
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 appears in Listing 17.3.
///////////////////////////////////////////////////////////////////////////// // 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 domain 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. Listing 17.4 shows this code.
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 data exchange between CDierollPropPage, which represents the dialog box that is the property page, and the actual boxes on the user's screen. It, too, will have code added by ClassWizard--Listing 17.5 shows the empty map AppWizard made.
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 enables the ActiveX framework to call this code when a user edits the control's properties.
///////////////////////////////////////////////////////////////////////////// // 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);
}
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 are 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. That involves adding a property to the control and then writing the drawing code.
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.
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 enables 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 can't be read from a file, so they are set to the default values provided in this method. Controls don't have a Serialize() method.
AppWizard generated a skeleton DoPropExchange() method; this code is in Listing 17.7.
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 ensure 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()
PX_VBXFontConvert()
Filling in the property's default value is simple for some properties but not 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().
The code to display the number belongs in the OnDraw() method of the control class, CDierollCtrl. (Controls don't 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.
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 5, "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. You can keep the white rectangle for now, but 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 code lines 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 isn't run as a standalone application in order to register it. Build the project and fix any typing mistakes. 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.
NOTE: If the Tools menu in Developer Studio doesn't include an ActiveX 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 at the list of tools and make sure that ActiveX Control Test Container isn't there.
4. Go to the bottom of the list and double-click the empty entry.
5. Type Activ&eX Control Test Container in the entry and press Enter.
6. Click the ... button to the right of the Command box and browse to your Visual C++ 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\Microsoft Visual Studio\Common\Tools\TSTCON32.EXE. Your system may be different.
7. Click the rightward-pointing arrow beside the Initial Directory box and choose Target Directory from the list that appears.
8. Make sure that the three check boxes across the bottom of the directory are not selected.
9. Click the Close button.
If you haven't 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 debug version, you will receive 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 under the tools menu, 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 New 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.
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.
FIG. 17.6 By adding one property and changing two functions, you have transformed the empty shell into a control that displays a 3.
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.
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. 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. Bring up ClassWizard 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); }
m_number = Roll();
InvalidateControl();
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() will be a public function that takes no parameters and returns a short.
FIG. 17.9 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 is always 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. Listing 17.9 shows this code.
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. 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. n
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 ) );
Rather than hard-code 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());
Make sure the test container is not still open, build the control, and then test it 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.
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. 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 1 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.
The next step in building this die-roll control is to make the control look like a die. A nice 3D 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.
FIG. 17.10 The ResourceView of Visual C++ enables you to build your own icon to be added to the Control Palette in Visual Basic.
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.
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 handy, 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 control's upper-left coordinates, width, and height. The default code generated by AppWizard 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 Listing 17.11.
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 will 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, and then add the new lines from the code fragment in Listing 17.12.
//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, you center 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.") 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 become 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.
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 control's default behavior is to display the dot pattern.
In OnDraw(), uncomment those lines that displayed the digit. Wrap an if around them so that the digit is displayed if m_dots is FALSE and dots are displayed if it is TRUE. The code looks like Listing 17.13.
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.
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 doesn't 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 is changed. Because the OnDraw() function uses CDierollCtrl::m_dots, the control's appearance 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, and Properties; your own property page will appear, as shown in Figure 17.15. (The Extended tab is provided for you, but as you can see, it doesn't really do anything. Your General tab is the important one at the moment.) 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.
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.
FIG. 17.15 The control test container displays your own property page.
The die you've created will always have black dots on a white background, but giving the user control to change this is remarkably simple. You 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 can't 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 doesn't 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.
A number of useful DISPIDs are defined in olectl.h, including these:
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 still others might support properties not included in the preceding list.
The vartypes include those shown in Table 17.1.
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() are 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 doesn't support the requested property.
Implementing BackColor and ForeColor To add BackColor and ForeColor to the control, follow these steps:
FIG. 17.16 ClassWizard describes stock properties for you.
FIG. 17.17 An S precedes the stock properties 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.
////////////////////////////////////////////////////////////// // 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.
///////////////////////////////////////////////////////////////////////////// // 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 set by the user. You will have 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.
BEGIN_DISPATCH_MAP(CDierollCtrl, COleControl) //{{AFX_DISPATCH_MAP(CDierollCtrl) DISP_PROPERTY_NOTIFY(CDierollCtrl, "Number", m_number, [ccc] OnNumberChanged, VT_I2) DISP_PROPERTY_NOTIFY(CDierollCtrl, "Dots", m_dots, [ccc] OnDotsChanged, VT_BOOL) DISP_STOCKPROP_BACKCOLOR() DISP_STOCKPROP_FORECOLOR() //}}AFX_DISPATCH_MAP DISP_FUNCTION_ID(CDierollCtrl, "AboutBox", [ccc]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. 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).
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) { ... } pdc->SelectObject(savebrush); }
}
Build the control 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, and then click Add Method. Name the new function DoRoll, select Return Type of Void, and when it is added, click Edit Code and fill it in like this:
void CDierollCtrl::DoRoll() { m_number = Roll(); InvalidateControl(); }
This simple code rolls the die and requests a redraw. Not everything about ActiveX controls needs to be difficult!
You can test this code by building the project, opening the test container, inserting a dieroll control, then choosing Control, Invoke Methods. On the Invoke Methods dialog box, shown in Figure 17.19, select DoRoll(Method) from the upper drop-down box; then click Invoke. You will see the die roll.
The die-rolling control may seem complete, but it could be even better. The following sections discuss improvements that can be made to the control for different situations.
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.
FIG. 17.19 You can invoke your control's methods in the test container.
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 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 enable the user to set Sides, and don't forget to 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.
If you were writing a backgammon game, you would need two dice. One approach would be to embed two individual die controls. How would you synchronize them, though, 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 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.
TIP: In Chapter 20, "Building an Internet ActiveX Control," you discover one way to synchronize two or more separate dice within one control container, and you'll learn some of the difficulties involved.
© Copyright, Macmillan Computer Publishing. All rights reserved.