Program examples compiled using Visual C++ 6.0 compiler on Windows XP Pro machine with Service Pack 2. Topics and sub topics for this tutorial are listed below. Don’t forget to read Tenouk’s small disclaimer. The supplementary note for this tutorial is control class.
Developing the Control
Once the control is inserted into the server, you need to add some code to make the control do something. If you were to compile and load ATL's default control into a container, the results wouldn't be particularly interesting. You'd simply see a blank rectangle with the string "ATL 3.0: Myatldiceob." You'll want to add code to render the control, to represent the internal state of the control, to respond to events, and to generate events to send back to the container.
Deciding What to Draw
A good place to start working on a control is on its drawing code, you get instant gratification that way. This is a control that is visually represented by a couple of dice. The easiest way to render to the dice control is to draw bitmaps representing each of the six possible dice sides and then show the bitmaps on the screen. This implies that the dice control will maintain some variables to represent its state. For example, the control needs to manage the bitmaps for representing the dice as well as two numbers representing the first value shown by each die. Here is the code from MYATLDICEOBJ.H that represents the state of the dice:
#define MAX_DIEFACES 6 ... ... ... HBITMAP m_dieBitmaps[MAX_DIEFACES]; unsigned short m_nFirstDieValue; unsigned short m_nSecondDieValue;
Before diving headfirst into the control's drawing code, you need to do a bit of preliminary work; the bitmaps need to be loaded. Presumably each die rendered by the dice control will show any one of six dice faces, so the control needs one bitmap for each face. Figure 56 shows what one of the dice bitmaps looks like.
Figure 56: A bitmap for the dice control.
|
If you draw the bitmaps one at a time, they'll have sequential identifiers in the resource.h file. Giving the bitmaps sequential identifiers will make them easier to load. Otherwise, you might need to modify the resource.h file, which contains the following identifiers:
#define IDB_DICE1 207
#define IDB_DICE2 208
#define IDB_DICE3 209
#define IDB_DICE4 210
#define IDB_DICE5 211
#define IDB_DICE6 212
Loading bitmaps is fairly straightforward. Cycle through the bitmap array, and load the bitmap resources. When they're stored in an array like this, grabbing the bitmap out of the array and showing it is much easier than if you didn't use an array. Here is the function that loads the bitmaps into the array:
BOOL Cmyatldiceob::LoadBitmaps()
{
BOOL bSuccess = TRUE;
for(int i=0; i<MAX_DIEFACES; i++)
{
DeleteObject(m_dieBitmaps[i]);
m_dieBitmaps[i] = LoadBitmap(_Module.m_hInst, MAKEINTRESOURCE(IDB_DICE1+i));
if(!m_dieBitmaps[i])
{
::MessageBox(NULL,
"Failed to load bitmaps",
NULL,
MB_OK);
bSuccess = FALSE;
}
}
return bSuccess;
}
The best place to call LoadBitmaps() is from within the control's constructor, as shown in the following code. To simulate a random roll of the dice, set the control's state so that the first and second die values are random numbers between 0 and 5 (these numbers will be used when the dice control is drawn):
class Cmyatldiceob : // big inheritance list
{
Cmyatldiceob()
{
LoadBitmaps();
srand((unsigned)time(NULL));
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;
}
Once the bitmaps are loaded, you'll want to render them. The dice control should include a function for showing each die face based on the current internal state of the dice. Here's where you first encounter ATL's drawing machinery.
One of the most convenient things about ATL-based controls (and MFC-based controls) is that all the drawing code happens in one place: within the control's OnDraw() function. OnDraw() is a virtual function of COleControlBase. Here's OnDraw()'s signature:
virtual HRESULT OnDraw(ATL_DRAWINFO& di);
OnDraw() takes a single parameter: a pointer to an ATL_DRAWINFO structure. Among other things, the ATL_DRAWINFO structure contains a device context on which to render your control. Here's the ATL_DRAWINFO structure:
struct ATL_DRAWINFO {
UINT cbSize;
DWORD dwDrawAspect;
LONG lindex;
DVTARGETDEVICE* ptd;
HDC hicTargetDev;
HDC hdcDraw;
LPCRECTL prcBounds; //Rectangle in which to draw
LPCRECTL prcWBounds; //WindowOrg and Ext if metafile
BOOL bOptimize;
BOOL bZoomed;
BOOL bRectInHimetric;
SIZEL ZoomNum; //ZoomX = ZoomNum.cx/ZoomNum.cy
SIZEL ZoomDen;
};
As you can see, there's a lot more information here than a simple device context. While you can count on the framework filling it out correctly for you, it's good to know where the information in the structure comes from and how it fits into the picture. ActiveX Controls are interesting because they are drawn in two contexts. The first and most obvious context is when the control is active and it draws within the actual drawing space of the client. The other, less-obvious context in which controls are drawn is during design time (as when an ActiveX control resides in a Visual Basic form in design mode). In the first context, ActiveX Controls render themselves to a live screen device context. In the second context, ActiveX Controls render themselves to a metafile device context.
Many (though not all) ATL-based controls are composed of at least one window. So ActiveX Controls need to render themselves during the WM_PAINT message. Once the control receives the WM_PAINT message, the message routing architecture passes control to CComControlBase::OnPaint. Remember, CComControlBase is one of the control's base classes. CComControlBase::OnPaint performs several steps. The function begins by creating a painting device context (using BeginPaint()). Then OnPaint() creates an ATL_DRAWINFO structure on the stack and initializes the fields within the structure. OnPaint() sets up ATL_DRAWINFO to show the entire content (the dwDrawAspect field is set to DVASPECT_CONTENT). OnPaint() also sets the lindex field to _1, sets the drawing device context to the newly created painting device context, and sets up the bounding rectangle to be the client area of the control's window. Then OnPaint() goes on to call OnDrawAdvanced(). The default OnDrawAdvanced() function prepares a normalized device context for drawing. You can override this method if you want to use the device context passed by the container without normalizing it. ATL then calls your control class's OnDraw() method. The second context in which the OnDraw() function is called is when the control draws on to a metafile. The control draws itself on to a metafile whenever someone calls IViewObjectEx::Draw. IViewObjectEx is one of the interfaces implemented by the ActiveX control. ATL implements the IViewObjectEx interface through the template class IViewObjectExImpl. IViewObjectExImpl::Draw is called whenever the control needs to take a snapshot of its presentation space for the container to store. In this case, the container creates a metafile device context and hands it to the control. IViewObjectExImpl puts an ATL_DRAWINFO structure on the stack and initializes. The bounding rectangle, the index, the drawing aspect, and the device contexts are all passed in as parameters by the client. The rest of the drawing is the same in this case, the control calls OnDrawAdvanced(), which in turn calls your version of OnDraw().
Once you're armed with this knowledge, writing functions to render the bitmaps becomes fairly straightforward. To show the first die face, create a memory-based device context, select the object into the device context, and BitBlt the memory device context into the real device context. Here's the code:
void Cmyatldiceob::ShowFirstDieFace(ATL_DRAWINFO& di)
{
BITMAP bmInfo;
GetObject(m_dieBitmaps[m_nFirstDieValue-1], sizeof(bmInfo), &bmInfo);
SIZE size;
size.cx = bmInfo.bmWidth;
size.cy = bmInfo.bmHeight;
HDC hMemDC;
hMemDC = CreateCompatibleDC(di.hdcDraw);
HBITMAP hOldBitmap;
HBITMAP hbm = m_dieBitmaps[m_nFirstDieValue-1];
hOldBitmap = (HBITMAP)SelectObject(hMemDC, hbm);
if (hOldBitmap == NULL)
return; // destructors will clean up
BitBlt(di.hdcDraw,
di.prcBounds->left+1,
di.prcBounds->top+1,
size.cx,
size.cy,
hMemDC, 0,
0,
SRCCOPY);
SelectObject(di.hdcDraw, hOldBitmap);
DeleteDC(hMemDC);
}
Showing the second die face is more or less the same process; just make sure that the dice are represented separately. For example, you probably want to change the call to BitBlt() so that the two dice bitmaps are shown side by side.
void Cmyatldiceob::ShowSecondDieFace(ATL_DRAWINFO& di)
{
// This code is exactly the same as ShowFirstDieFace
// except the second die is positioned next to the first die.
BitBlt(di.hdcDraw,
di.prcBounds->left+size.cx + 2,
di.prcBounds->top+1,
size.cx,
size.cy,
hMemDC, 0,
0, SRCCOPY);
// The rest is the same as in ShowFirstDieFace
}
The last step is to call these two functions whenever the control is asked to render itself, during the control's OnDraw() function. ShowFirstDieFace() and ShowSecondDieFace() will show the correct bitmap based on the state of m_nFirstDieValue and m_nSecondDieValue:
HRESULT OnDraw(ATL_DRAWINFO& di)
{
RECT& rc = *(RECT*)di.prcBounds;
ShowFirstDieFace(di);
ShowSecondDieFace(di);
return S_OK;
}
At this point, if you compile and load this control into some ActiveX Control container (like a Visual Basic form or an MFC-based dialog), you'll see two die faces staring back at you. Now it's time to add some code to enliven the control and roll the dice.
Responding to Window Messages
Just looking at two dice faces isn't that much fun. You want to make the dice work. A good way to get the dice to appear to jiggle is to use a timer to generate events and then respond to the timer by showing a new pair of dice faces. Setting up a Windows timer in the control means adding a function to handle the timer message and adding a macro to the control's message map. Start by using ClassView to add a handler for WM_TIMER. Right-click on the Cmyatldiceob symbol in ClassView, and select Add Windows Message Handler from the context menu. This adds a prototype for the OnTimer() function and an entry into the message map to handle the WM_TIMER message. Add some code to the OnTimer() function to handle the WM_TIMER message. The OnTimer() function should look like the code shown below. |
LRESULT Cmyatldiceob::OnTimer(UINT msg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
if(m_nTimesRolled > 15)
{
m_nTimesRolled = 0;
KillTimer(1);
} else {
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;
FireViewChange();
m_nTimesRolled++;
}
bHandled = TRUE;
return 0;
}
This function responds to the timer message by generating two random numbers, setting up the control's state to reflect these two new numbers, and then asking the control to refresh itself by calling FireViewChange(). Notice the function kills the timer as soon as the dice have rolled a certain number of times. Also notice that the message handler tells the framework that it successfully handled the function by setting the bHandled variable to TRUE.
Notice there's an entry for WM_TIMER in the control's message map. Because WM_TIMER is just a plain vanilla window message, it's represented with a standard MESSAGE_HANDLER macro as follows:
BEGIN_MSG_MAP(Cmyatldiceob)
CHAIN_MSG_MAP(CComControl<Cmyatldiceob>)
DEFAULT_REFLECTION_HANDLER()
MESSAGE_HANDLER(WM_TIMER, OnTimer);
END_MSG_MAP()
As you can tell from this message map, the dice control already handles the gamut of Windows messages through the CHAIN_MSG_MAP macro. However, now the pair of dice has the ability to simulate rolling by responding to the timer message. Setting a timer causes the control to repaint itself with a new pair of dice numbers every quarter of a second or so. Of course, there needs to be some way to start the dice rolling. Because this is an ActiveX control, it's reasonable to allow client code to start rolling the dice via a call to a function in one of its incoming interfaces. Use ClassView to add a RollDice() function to the main interface. Do this by right-clicking on the IMyatldiceobj interface appearing in ClassView on the left side of the screen and selecting Add Method from the pop up menu. Then add a RollDice() function. Microsoft Visual C++ adds a function named RollDice() to your control. Implement RollDice() by setting the timer for a reasonably short interval and then returning S_OK. Add the following code:
STDMETHODIMP Cmyatldiceob::RollDice()
{
SetTimer(1, 250);
return S_OK;
}
If you load the dice into an ActiveX control container, you'll now be able to browse and call the control's methods and roll the dice.
In addition to using the incoming interface to roll the dice, the user might reasonably expect to roll the dice by double-clicking the control. To enable this behavior, just add a message handler to trap the mouse-button-down message by adding a function to handle a left-mouse double click.
LRESULT Cmyatldiceob::OnLButtonDblClick(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
RollDice();
bHandled = TRUE;
return 0;
}
Then be sure you add an entry to the message map to handle the WM_LBUTTONDOWN message:
BEGIN_MSG_MAP(Cmyatldiceob)
// ...
// Other message handlers
// ...
MESSAGE_HANDLER(WM_LBUTTONDBLCLK, OnLButtonDblClick)
END_MSG_MAP()
When you load the dice control into a container and double-click on it, you should see the dice roll. Now that you've added rendering code and given the control the ability to roll, it's time to add some properties.
Adding Properties and Property Pages
You've just seen that ActiveX controls have an external presentation state. The presentation state is the state reflected when the control draws itself. In addition, most ActiveX controls also have an internal state. The control's internal state is a set of variables exposed to the outside world via interface functions. These internal variables are also known as properties.
For example, imagine a simple grid implemented as an ActiveX control. The grid has an external presentation state and a set of internal variables for describing the state of the grid. The properties of a grid control would probably include the number of rows in the grid, the number of columns in the grid, the color of the lines composing the grid, the type of font used, and so forth.
As you saw in Module 28, adding properties to an ATL-based class means adding member variables to the class and then using ClassWizard to create get and put functions to access these properties. For example, two member variables that you might add to the dice control include the dice color and the number of times the dice are supposed to roll before stopping. Those two properties could easily be represented as a pair of short integers as shown here:
class ATL_NO_VTABLE Cmyatldiceob :
...
...
{
...
...
short m_nDiceColor;
short m_nTimesToRoll;
...
...
};
To make these properties accessible to the client, you need to add get and put functions to the control. Right-clicking on the interface symbol in ClassView brings up a context menu, giving you a choice to Add Property, which will present you with the option of adding these functions. Adding DiceColor() and TimesToRoll() properties to the control using ClassView will add four new functions to the control: get_DiceColor(), put_DiceColor(), get_TimesToRoll(), and put_TimesToRoll(). The get_DiceColor() function should retrieve the state of m_nDiceColor:
STDMETHODIMP Cmyatldiceob::get_DiceColor(short * pVal)
{
*pVal = m_nDiceColor;
return S_OK;
}
To make the control interesting, put_DiceColor() should change the colors of the dice bitmaps and redraw the control immediately. This example uses red and blue dice as well as the original black and white dice. To make the control show the new color bitmaps immediately after the client sets the dice color, the put_DiceColor() function should load the new bitmaps according to new color, and redraw the control:
STDMETHODIMP Cmyatldiceob::put_DiceColor(short newVal)
{
if(newVal < 3 && newVal >= 0)
m_nDiceColor = newVal;
LoadBitmaps();
FireViewChange();
return S_OK;
}
Of course, this means that LoadBitmaps() needs to load the bitmaps based on the state of m_nDiceColor, so we need to add the following code to our existing LoadBitmaps() function:
BOOL Cmyatldiceob::LoadBitmaps()
{
int i;
BOOL bSuccess = TRUE;
int nID = IDB_DICE1;
switch(m_nDiceColor)
{
case 0:
nID = IDB_DICE1;
break;
case 1:
nID = IDB_BLUEDICE1;
break;
case 2:
nID = IDB_REDDICE1;
break;
}
for(i=0; i<MAX_DIEFACES; i++)
{
DeleteObject(m_dieBitmaps[i]);
m_dieBitmaps[i] = LoadBitmap(_Module.m_hInst, MAKEINTRESOURCE(nID+i));
if(!m_dieBitmaps[i])
{
::MessageBox(NULL, "Failed to load bitmaps", NULL, MB_OK);
bSuccess = FALSE;
}
}
return bSuccess;
}
Just as the dice color property reflects the color of the dice, the number of times the dice rolls should be reflected by the state of the TimesToRoll property. The get_TimesToRoll() function needs to read the m_nTimesToRoll member, and the put_TimesToRoll() function needs to modify m_nTimesToRoll. Add code shown below.
STDMETHODIMP Cmyatldiceob::get_TimesToRoll(short * pVal)
{
*pVal = m_nTimesToRoll;
return S_OK;
}
STDMETHODIMP Cmyatldiceob::put_TimesToRoll(short newVal)
{
m_nTimesToRoll = newVal;
return S_OK;
}
Finally, instead of hard-coding the number of times the dice rolls, use the m_nTimesToRoll variable to determine when to kill the timer.
LRESULT Cmyatldiceob::OnTimer(UINT msg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
if(m_nTimesRolled > m_nTimesToRoll)
{
m_nTimesRolled = 0;
KillTimer(1);
Fire_DiceRolled(m_nFirstDieValue, m_nSecondDieValue);
if(m_nFirstDieValue == m_nSecondDieValue)
Fire_Doubles(m_nFirstDieValue);
if(m_nFirstDieValue == 1 && m_nSecondDieValue == 1)
Fire_SnakeEyes();
} else {
m_nFirstDieValue = (rand() % (MAX_DIEFACES)) + 1;
m_nSecondDieValue = (rand() % (MAX_DIEFACES)) + 1;
FireViewChange();
m_nTimesRolled++;
}
bHandled = TRUE;
return 0;
}
Now these two properties are exposed to the outside world. When the client code changes the color of the dice, the control loads a new set of bitmaps and redraws the control with the new dice faces. When the client code changes the number of times to roll, the dice control uses that information to determine the number of times the dice control should respond to the WM_TIMER message. So the next question is, "How are these properties accessed by the client code?" One way is through a control's property pages.
Further reading and digging:
DCOM at MSDN.
COM+ at MSDN.
COM at MSDN.
Win32 process, thread and synchronization story can be found starting from Module R.
MSDN MFC 9.0 class library online documentation - latest version.
Unicode and Multibyte character set: Story and program examples.