Leading your developers forward







Home

Programming

Web 
Development

E-Commerce

Web Services

Consulting

Mentoring

Training

Books &
Writing

 

 

 

Dieroll Changes

The dieroll control presented here is discussed in detail in Using Visual C++ 4.2, by Kate Gregory. Just as the book went to press, the ActiveX SDK and Microsoft Internet Explorer 3.0 were being released. After the book was printed the 4.2b patch to Visual C++ 4.2 was released. As a result there are a number of small changes that need to be made to the code presented there to build this control. If you own the book, consider this an addendum. If you don't own the book, this page will not teach you how to build the dieroll control.

Summary of changes

The changes presented here are:

If you don't feel like typing

All of the changes made here are incorporated in a zip file containing a complete copy of the source. The source requires that you have Microsoft Visual C++ version 4.2 with the 4.2b patch which you can get from the Microsoft Visual C++ pages.

Event map additions

Two events were not in the event map but were in the .ODL file. Add these events to the event map in DierollCtl.cpp, so that it looks like this:

BEGIN_EVENT_MAP(CDierollCtrl, COleControl)
//{{AFX_EVENT_MAP(CDierollCtrl)
EVENT_STOCK_CLICK()
EVENT_STOCK_READYSTATECHANGE()
//}}AFX_EVENT_MAP
END_EVENT_MAP() 

Registering as safe for scripting and initializing

For those of you who operate with a Medium Safety level, the control should be registered as safe for scripting and initializing. This assures anyone who wants to view a page containing the control that no matter what functions are called from a script, or what parameters are initialized through the PARAM attribute, nothing unsafe will happen. For an example of a control that is not safe, think of a control that deletes a file on your machine when it executes. The default file is one you won't miss or that probably won't exist. A page that put this control in a script, or that initialized the file name with PARAM attributes, might order the control to delete a very important file or files, based on guesses about where most people keep documents. It would be simple to delete C:\MSOFFICE\WINWORD\WINWORD.EXE, for example, and that would be annoying for Word users.

First, you need to add three functions to DierollCtl.cpp. (These come unchanged from the ActiveX SDK.) These functions are called by code presented below. Don't forget to add declarations of the these functions to the header file too. Here are the three functions to add:

////////////////////////////////////////////////////////////////
// Copied from the ActiveX SDK
// This code is used to register and unregister a
// control as safe for initialization and safe for scripting

HRESULT CreateComponentCategory(CATID catid, WCHAR* catDescription)
{

    ICatRegister* pcr = NULL ;
    HRESULT hr = S_OK ;

    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, 
	NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
	if (FAILED(hr))
		return hr;

    // Make sure the HKCR\Component Categories\{..catid...}
    // key is registered
    CATEGORYINFO catinfo;
    catinfo.catid = catid;
    catinfo.lcid = 0x0409 ; // english

	// Make sure the provided description is not too long.
	// Only copy the first 127 characters if it is
	int len = wcslen(catDescription);
	if (len>127)
		len = 127;
    wcsncpy(catinfo.szDescription, catDescription, len);
	// Make sure the description is null terminated
	catinfo.szDescription[len] = '\0';

    hr = pcr->RegisterCategories(1, &catinfo);
	pcr->Release();

	return hr;
}


HRESULT RegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{
	// Register your component categories information.
    ICatRegister* pcr = NULL ;
    HRESULT hr = S_OK ;
    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, 
	NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (SUCCEEDED(hr))
    {
       // Register this category as being "implemented" by
       // the class.
       CATID rgcatid[1] ;
       rgcatid[0] = catid;
       hr = pcr->RegisterClassImplCategories(clsid, 1, rgcatid);
    }

    if (pcr != NULL)
        pcr->Release();
  
	return hr;
}

HRESULT UnRegisterCLSIDInCategory(REFCLSID clsid, CATID catid)
{
    ICatRegister* pcr = NULL ;
    HRESULT hr = S_OK ;
    hr = CoCreateInstance(CLSID_StdComponentCategoriesMgr, 
	NULL, CLSCTX_INPROC_SERVER, IID_ICatRegister, (void**)&pcr);
    if (SUCCEEDED(hr))
    {
       // Unregister this category as being "implemented" by
       // the class.
       CATID rgcatid[1] ;
       rgcatid[0] = catid;
       hr = pcr->UnRegisterClassImplCategories(clsid, 1, rgcatid);
    }

    if (pcr != NULL)
        pcr->Release();
  
	return hr;
}

Add two #include statements at the top of DierollCtl.cpp:

#include "comcat.h"
#include "objsafe.h" 

Finally, modify UpdateRegistry() in DierollCtl.cpp to call these new functions. The new code calls CreateComponentCategory() to create a category called "CATID_SafeForScripting", then adds this control to that category. Then it creates a category called "CATID_SafeForInitializing" and adds the control to that category as well.

///////////////////////////////////////////////////////////////////
// CDierollCtrl::CDierollCtrlFactory::UpdateRegistry -
// Adds or removes system registry entries for CDierollCtrl

BOOL CDierollCtrl::CDierollCtrlFactory::UpdateRegistry(BOOL bRegister)
{
// TODO: Verify that your control follows apartment-model threading rules.
// Refer to MFC TechNote 64 for more information.
// If your control does not conform to the apartment-model rules, then
// you must modify the code below, changing the 6th parameter from
// afxRegInsertable | afxRegApartmentThreading to afxRegInsertable.

if (bRegister)
{
HRESULT hr = S_OK ;

// register as safe for scripting
hr = CreateComponentCategory(CATID_SafeForScripting, 
	L"Controls that are safely scriptable");

if (FAILED(hr))
return FALSE;

hr = RegisterCLSIDInCategory(m_clsid, CATID_SafeForScripting);

if (FAILED(hr))
return FALSE;

// register as safe for initializing
hr = CreateComponentCategory(CATID_SafeForInitializing, 
	L"Controls safely initializable from persistent data");

if (FAILED(hr))
return FALSE;

hr = RegisterCLSIDInCategory(m_clsid, CATID_SafeForInitializing);

if (FAILED(hr))
return FALSE;

return AfxOleRegisterControlClass(
AfxGetInstanceHandle(),
m_clsid,
m_lpszProgID,
IDS_DIEROLL,
IDB_DIEROLL,
afxRegInsertable | afxRegApartmentThreading,
_dwDierollOleMisc,
_tlid,
_wVerMajor,
_wVerMinor);
}
else
{
HRESULT hr = S_OK ;

hr = UnRegisterCLSIDInCategory(m_clsid, CATID_SafeForScripting);

if (FAILED(hr))
return FALSE;

hr = UnRegisterCLSIDInCategory(m_clsid, CATID_SafeForInitializing);

if (FAILED(hr))
return FALSE;

return AfxOleUnregisterClass(m_clsid, m_lpszProgID);
}
} 

Exposing the DoRoll() function

In the book, we present the dieroll control all alone on a page. For this page we wanted to have two dice, both of which would roll in response to a click on either one. Since we were already exposing the Click event, all that was needed was to expose a DoRoll() function for the other die to call. Bring up Class Wizard, click the OLE Automation tab, then click Add Method. Name the new function DoRoll, then when it is added, click Edit Code and fill it in like this:

void CDierollCtrl::DoRoll() 
{
m_number = Roll();
InvalidateControl();
} 

Appearance issues

The control divides the drawing area into a 16 by 16 grid for drawing the dots of the die. But as mentioned in the book, there can be up to 15 pixels that are not accounted for on the bottom and right of the die. When the control is small, these can be very noticeable. We could have taken care of this in several ways: centering the dots in the control is a fairly simple way to handle it. Find the lines that calculate Xunit and Yunit, and add the new lines from this code fragment:

//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.

Calling wrong member function

It turns out that under certain circumstances the data for the bitmap can be loaded and ready before the control has its window initialized. In the book, as soon as the bitmap was loaded we would invalidate the control to force a redraw. If the window was not ready this would violate an assertion.

In the OnDataAvailable() function in DierollDataPathProperty.cpp we called GetControl()->Invalidate(). This did invalidate the control window but it caused problems under certain circumstances. The call should have been to GetControl()->InvalidateControl().

Change

GetControl()->Invalidate(); 

to

GetControl()->InvalidateControl(); 

24 bit colour bitmaps

This code snippet from ReadBitmap() in DierollDataPathProperty.cpp tries to allocate an enormous colour map when dealing with 16 million colours:

	if (file.Read(lpbmi->bmiColors, 
		((1<<bmih.biBitCount) * sizeof(RGBQUAD))) 
		!= ((1<<bmih.biBitCount) * sizeof(RGBQUAD)) )
	{
		// return FALSE to indicate an error occured
		GlobalUnlock(hmem1); 
		GlobalFree(hmem1);
		return FALSE;
	}
 

Find those lines and replace them with these:

	/*
	 *   The colour array size is calculated by taking the size of
	 *   the file and subtracting the size of the image and the
	 *   size of the headers
	 */

	long colour_size = file_length - bmih.biSizeImage -
		    sizeof(BITMAPFILEHEADER) - bmih.biSize;

	if (colour_size > 0)
	{
		if (file.Read(lpbmi->bmiColors, colour_size) 
			!= (unsigned long)colour_size )
		{
			// return FALSE to indicate an error occured
			GlobalUnlock(hmem1); 
			GlobalFree(hmem1);
			return FALSE;
		}
	} 

As in the book, we are not going to explain how to draw bitmaps in this section: we are talking about controls here.

Cabinet files

The best way to distribute a control over the web is in a .cab (cabinet) file. The cab process bundles files together and compresses the bundle. The advantage of a cabinet file over a zip file is that they can be signed (though this one isn't) and Internet Explorer can open them and use the .inf file inside to install the other files in the cabinet. There is versioning information included so that if you have an old version of the control, the new one will be downloaded and installed. We decided to use a separate cab file for the MFC DLLs, so that those of you with Visual C++ 4.2b would not have to download the other cab files. This is a good idea now that the MFC DLLs are being distributed with Windows 95, installed by other controls, and so on. In fact Microsoft has gathered a cab file with the three DLLs that controls use. For more information on this, check their page on the cab files.

If you have built the control, and have followed the instructions on this page to update the code, you should now have a new dieroll.ocx. You need to make a .inf file and three .ddf files. The .inf file can be thought of as installation instructions. This is dieroll.inf:

;Dieroll.INF file for dieroll.ocx

[version] 
signature="$CHICAGO$"
AdvancedINF=2.0 

[Add.Code]
dieroll.ocx=dieroll.ocx
msvcrt.dll=msvcrt.dll
mfc42.dll=mfc42.dll
olepro32.dll=olepro32.dll

[dieroll.ocx]
; lines below specify that the specified dieroll.ocx (clsid, version) 
; needs to be installed on the system. 
; If doesn't exist already, can be downloaded from the given 
; location (a .CAB)
; note: "thiscab" indicates that instead of the file location, it 
; is assumed that the desired file is present in the same .CAB cabinet
; that the INF originated from
file-win32-x86=thiscab
clsid={46646B43-EA16-11CF-870C-00201801DDD6}
; Note that the {}s are required when entering the CLSID in the INF 
; file. This is slightly different from the HTML syntax for inserting 
; CLSIDs in an <OBJECT> tag.
FileVersion=1,0,0,2

; dependent DLLs
[msvcrt.dll]
; This is an example of conditional hook. The hook only gets processed
; if msvcrt.dll of the specified version is absent on client machine.
FileVersion=4,20,0,6164
hook=mfc42installer 

[mfc42.dll]
FileVersion=4,2,0,6256
hook=mfc42installer

[olepro32.dll]
FileVersion=4,2,0,6068
hook=mfc42installer

[mfc42installer]
file-win32-x86=http://activex.microsoft.com/controls/vc/mfc42.cab
; If dependent DLLs are packaged directly into the above cabinet file
; along with an .inf file, specify that .inf file to run as follows:
;InfFile=mfc42.inf 
; The mfc42.cab file actually contains a self extracting executable.
; In this case we specify a run= command.
run=%EXTRACT_DIR%\mfc42.exe

The FileVersion for the OCX is under your control. In Developer Studio's ResourceView, double click on the Version resource entry in the tree, then on the VS_VERSION_INFO resource. Double click on the FILEVERSION line if you wish to change it. You should change the final digit of the version each time you change the control: more significant changes would involve changing earlier digits. For example, version 1.0.1.0 has some meaningful improvements since version 1.0.0.1, whereas version 1.0.0.2 would probably just be a bugfix. (The last component of a Microsoft version will be the build number: they change it each time they build the EXE or DLL..) This version information should be provided in the OBJECT tag as well, as you see in this sample:

<OBJECT ID="Dieroll1" WIDTH="40" HEIGHT="40" 
CODEBASE="dieroll.cab#Version=1,0,0,2" 
CLASSID="CLSID:46646B43-EA16-11CF-870C-00201801DDD6"> 

Whenever you change the version information in your control, be sure to change your .inf file and rebuild the CAB, then change your HTML. (If an older version of the control would still work, you do not have to change the HTML. Leaving it unchanged saves the user from having to download.)

For the DLLs, you should use the version information that corresponds to the versions from Microsoft's cab file. Notice that the file name for the mfc-installer section is a URL on Microsoft's site. This is their cab file, and it's signed by them. Don't bother building your own cab of the DLLs, use theirs.

The .ddf file is packing instructions. This is how you tell diamond what to put into the cab file. Here is dieroll.ddf:

; DIAMOND directive file for dieroll.ocx & dieroll.inf
.OPTION EXPLICIT ; Generate errors on variable typos
.Set CabinetNameTemplate=dieroll.cab
;** Files to store and compress
.Set Cabinet=on
.Set Compress=on
dieroll.inf
dieroll.ocx 

The .inf file in the Dieroll cab file controls whether or not to download the MFC DLL cab file, and where to install the DLLs if the cab file is downloaded. So there is only one .inf file, but two .cab files: one on your site and one on Microsoft's. This .ddf file builds your .cab file. Put the .ddf file, the .inf file, and the dieroll.ocx all into one folder. Then use the diamond command like this:

diamond /f dieroll.ddf

You can get the CABinet Development Kit, which includes the diamond program, from http://www.microsoft.com. Cab files are a great way to distribute all kinds of executable material over the Web. You can also learn on that site how to sign cab files after they have been bundled and compressed.

 


Copyright Gregory Consulting Limited 1996-2007. All rights reserved.