📄 ch17.htm
字号:
<H3><A NAME="Heading9"></A>Notifying the Container</H3>
<P>Let's first tackle using an <I>event</I> 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:</P>
<UL>
<LI>Click is coded to indicate to the container that the user clicked.
<P>
<LI>DblClick is coded to indicate to the container that the user double-clicked.
<P>
<LI>Error is coded to indicate an error that can't be handled by firing any other
event.
<P>
<LI>KeyDown is coded to indicate to the container that a key has gone down.
<P>
<LI>KeyPress is coded to indicate to the container that a complete keypress (down
and then up) has occurred.
<P>
<LI>KeyUp is coded to indicate to the container that a key has gone up.
<P>
<LI>MouseDown is coded to indicate to the container that the mouse button has gone
down.
<P>
<LI>MouseMove is coded to indicate to the container that the mouse has moved over
the control.
<P>
<LI>MouseUp is coded to indicate to the container that the mouse button has gone
up.
</UL>
<P>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:</P>
<DL>
<DT></DT>
<DD><B>1. </B>Bring up ClassWizard by choosing View, ClassWizard, and click the ActiveX
Events tab. Make sure that the selected class is CDierollCtrl.
<P>
<DT></DT>
<DD><B>2. </B>Click the Add Event button and fill in the Add Event dialog box, as
shown in Figure 17.7.
<P>
<DT></DT>
<DD><B>3. </B>The external name is Click; choose it from the drop-down list box and
notice how the internal name is filled in as FireClick.
<P>
<DT></DT>
<DD><B>4. </B>Click OK to add the event, and your work is done. Close ClassWizard.
<P>
</DL>
<P><A HREF="javascript:popUp('17uvc07.gif')"><B>FIG. 17.7</B></A><B> </B><I>ClassWizard
helps you add events to your control.</I></P>
<P>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.</P>
<P><A HREF="javascript:popUp('17uvc08.gif')"><B>FIG. 17.8</B></A><B> </B><I>ClassView
displays events as well as classes.</I></P>
<P>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.</P>
<P>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:</P>
<DL>
<DT></DT>
<DD><B>1. </B>Select the Message Maps tab this time and make sure that your control
class, CDierollCtrl, is selected in the Class Name combo box.
<P>
<DT></DT>
<DD><B>2. </B>Scroll through the Messages list box until you find the WM_LBUTTONDOWN
message, which Windows generates whenever the left mouse button is clicked over your
control.
<P>
<DT></DT>
<DD><B>3. </B>Click Add Function to add a function that will be called automatically
whenever this message is generated--in other words, whenever the user clicks your
control. This function must always be named OnLButtonDown(), so ClassWizard doesn't
give you a dialog box asking you to confirm the name.
<P>
<DT></DT>
<DD><B>4. </B>ClassWizard has made a skeleton version of OnLButtonDown() for you;
click the Edit Code button to close ClassWizard, and look at the new OnLButtonDown()
code. Here's the skeleton:
<P>
</DL>
<PRE>void CDierollCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
// TODO: Add your message handler code here and/or call default
COleControl::OnLButtonDown(nFlags, point);
}
</PRE>
<DL>
<DT></DT>
<DD><B>5. </B>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.
<P>
</DL>
<PRE> m_number = Roll();
</PRE>
<DL>
<DT></DT>
<DD><B>6. </B>To force a redraw, next add this line:
<P>
</DL>
<PRE> InvalidateControl();
</PRE>
<DL>
<DT></DT>
<DD><B>7. </B>Leave the call to COleControl::OnLButtonDown() at the end of the function;
it handles the rest of the work involved in processing the mouse click.
<P>
</DL>
<H3><A NAME="Heading10"></A>Rolling the Die</H3>
<P>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.</P>
<P><A HREF="javascript:popUp('17uvc09.gif')"><B>FIG. 17.9</B></A><B> </B><I>Use the
Add Member Function dialog box to speed routine tasks.</I></P>
<P>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.</P>
<P>
<H4>Listing 17.9  DierollCtl.cpp--CDierollCtrl::Roll()</H4>
<PRE>short CDierollCtrl::Roll(void)
{
double number = rand();
number /= RAND_MAX + 1;
number *= 6;
return (short)number + 1;
</PRE>
<PRE>}
</PRE>
<BLOCKQUOTE>
<P>
<HR>
<strong>NOTE:</strong> 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.<BR>
</P>
<P>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
<HR>
</BLOCKQUOTE>
<P>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():</P>
<P>
<PRE> srand( (unsigned)time( NULL ) );
</PRE>
<P>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:</P>
<P>
<PRE>PX_Short( pPX, "Number", m_number, Roll());
</PRE>
<P>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?</P>
<P>
<H2><A NAME="Heading11"></A>Creating a Better User Interface</H2>
<P>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.</P>
<P>
<H3><A NAME="Heading12"></A>A Bitmap Icon</H3>
<P>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.</P>
<P>
<H3><A NAME="Heading13"></A>Displaying Dots</H3>
<P>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.</P>
<P><A HREF="javascript:popUp('17uvc10.gif')"><B>FIG. 17.10</B></A><B> </B><I>The
ResourceView of Visual C++ enables you to build your own icon to be added to the
Control Palette in Visual Basic.</I></P>
<P>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.</P>
<P>
<H4>Listing 17.10  DierollCtl.cpp--CDierollCtrl::OnDraw()</H4>
<PRE>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;
}
</PRE>
<PRE>}
</PRE>
<P>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.</P>
<P>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:</P>
<P>
<PRE> int Xunit = rcBounds.Width()/16;
int Yunit = rcBounds.Height()/16;
int Top = rcBounds.top;
int Left = rcBounds.left;
</PRE>
<P>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:</P>
<P>
<PRE> CBrush Black;
Black.CreateSolidBrush(RGB(0x00,0x00,0x00)); //solid black brush
CBrush* savebrush = pdc->SelectObject(&Black);
</PRE>
<P>After the switch statement, add this line to restore the old brush:</P>
<P>
<PRE> pdc->SelectObject(savebrush);
</PRE>
<P>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:</P>
<P>
<PRE> pdc->Ellipse(Left+Xunit, Top+Yunit,
Left+5*Xunit, Top + 5*Yunit);
</PRE>
<P>The coordinates for the other dots are determined similarly. The switch statement
ends up as show in Listing 17.11.</P>
<P>
<H4>Listing 17.11  DierollCtl.cpp--CDierollCtrl::OnDraw()</H4>
<PRE>switch(m_number)
{
case 1:
pdc->Ellipse(Left+6*Xunit, Top+6*Yunit,
Left+10*Xunit, Top + 10*Yunit); //center
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -