|
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.
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()
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);
}
}
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();
}
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.
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();
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.
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:
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.
|