In Chapter 17, "Building an ActiveX Control," you learned how to build your own controls and include them in forms-based applications written in Visual Basic, Visual C++, and the VBA macro language. There's one other place those controls can go--on a Web page. However, the ActiveX controls generated by older versions of Visual C++ were too big and slow to put on a Web page. This chapter shows you how to place these controls on your Web pages and how to write faster, sleeker controls that will make your pages a pleasure to use.
It's a remarkably simple matter to put an ActiveX control on a Web page that you know will be loaded by Microsoft Internet Explorer 3.0 or later. You use the <OBJECT> tag, a relatively new addition to HTML that describes a wide variety of objects that you might want to insert in a Web page: a moving video clip, a sound, a Java applet, an ActiveX control, and many more kinds of information and ways of interacting with a user. Listing 20.1 shows the HTML source for a page that displays the Dieroll control from Chapter 17.
<HEAD> <TITLE>A Web page with a rolling die</TITLE> </HEAD> <BODY> <OBJECT ID="Dieroll1" CLASSID="CLSID:46646B43-EA16-11CF-870C-00201801DDD6" CODEBASE="dieroll.cab#Version=1,0,0,1" WIDTH="200" HEIGHT="200"> <PARAM NAME="ForeColor" VALUE="0"> <PARAM NAME="BackColor" VALUE="16777215"> If you see this text, your browser does not support the OBJECT tag. <BR> </OBJECT> <BR> Here is some text after the die </BODY>
</HTML>
The only ugly thing here is the CLSID, and the easiest way to get that, because you're a software developer, is to cut and paste it from dieroll.odl, the Object Description Library. Open the dieroll project you built in Chapter 17 and use FileView to open dieroll.odl quickly. Here's the section in dieroll.odl that includes the CLSID:
// Class information for CDierollCtrl [ uuid(46646B43-EA16-11CF-870C-00201801DDD6), helpstring("Dieroll Control"), control ]
This section is at the end of dieroll.odl--the earlier CLSIDs do not refer to the whole control, only to portions of it. Copy the uuid from inside the brackets into your HTML source.
TIP: Microsoft has a product called the Control Pad that gets CLSIDs from the Registry for you and makes life easier for Web page builders who are either intimidated by instructions like "open the ODL file" or don't have the ODL file because it's not shipped with the control. Because you're building this control and know how to open files in Developer Studio, this chapter will not describe the Control Pad tool. If you're curious, see Microsoft's Control Pad Web page at http://www.microsoft.com/workshop/author/cpad/ for more details.
The CODEBASE attribute of the OBJECT tag specifies where the OCX file is kept, so if the user doesn't have a copy of the ActiveX control, one will be downloaded automatically. The use of the CLSID means that if this user has already installed this ActiveX control, there is no download time; the control is used immediately. You can simply specify an URL to the OCX file, but to automate the DLL downloading, this CODEBASE attribute points to a CAB file. Putting your control in a CAB file will cut your download time by nearly half. You can learn more about CAB technology at http://www.microsoft.com/intdev/cab/. That page is written for Java developers, but the technology works just as well to cut the download time for ActiveX controls.
TIP: If you don't have access to a Web server in which to put controls while you're developing them, use a file:// URL in the CODEBASE attribute that points to the control's location on your hard drive.
The remaining OBJECT tag attributes will be intuitive if you've built a Web page before: ID is used by other tags on the page to refer to this control; WIDTH and HEIGHT specify the size, in pixels, of the control's appearance; and HSPACE and VSPACE are horizontal and vertical blank spaces, in pixels, around the entire control.
Everything after the <OBJECT ...> tag and before the </OBJECT> tag is ignored by browsers that understand the OBJECT tag. (The <OBJECT...> tag is usually many lines long and contains all the information to describe the object.) Browsers that don't understand the OBJECT tag ignore the <OBJECT ...> tag and the </OBJECT> tag and display the HTML between them (in this case, a line of text pointing out that this browser doesn't support the tag). This is part of the specification for a Web browser: It should ignore tags it doesn't understand.
Figure 20.1 shows this page displayed in Microsoft Explorer 3.0. Clicking the die rolls it, and everything works beautifully. Things certainly look simple and amazing, but two flaws appear immediately:
FIG. 20.1 Microsoft Internet Explorer can show ActiveX controls.
Figure 20.2 shows the same page displayed in Netscape Navigator 3.0. It doesn't support the OBJECT tag, so it doesn't show the die. Also, Netscape Navigator is used by more than half the people who browse the Web! Does that mean it's not worth writing ActiveX controls for Web pages? Not at all. As you'll see in the very next section, there's a way that Navigator users can use the same controls as Explorer users.
FIG. 20.2 Netscape Navigator can't show ActiveX controls.
The size issue is a bigger worry. The release version of the Dieroll control, as built for Chapter 17, is 26KB. Many designers put a 50KB limit per Web page for graphics and other material to be downloaded, and this simple control uses half that limit. A more powerful control would easily exceed it. The majority of this chapter deals with ways to reduce that size or otherwise minimize the download time for ActiveX controls. Web page designers can then tap the controls' full power without worrying that users will label their pages as slow, one of the worst knocks against any Web site.
There's a third flaw that you won't notice because you have Visual C++ installed on your computer. The control requires the MFC DLL. The user must download it and install it before the controls can run. The mechanism that automatically downloads and installs controls doesn't automatically download and install this DLL, though using a CAB file as discussed earlier can make it possible.
TIP: For an example of a Web page that includes a CAB file for the Dieroll control and the MFC DLLs, come to http://www.gregcons.com/dieroll.htm.
NOTE: It might occur to you to try linking the MFC Library statically into your control. It seems easy enough to do: Choose Project, Settings, and on the General tab there is a drop-down list box inviting you to choose static linking. If you do that and build, you'll get hundreds of linker errors: The COleControl and CPropPage functions are not in the DLL that is linked statically. (That's because Microsoft felt it would be foolish to link the MFC functions statically in a control.) Setting up another library to link in those functions is beyond the scope of this chapter, especially because all this work would lead to an enormous (more than 1MB) control that would take far too long to download the first time.
NCompass Labs (www.ncompasslabs.com) has produced a Netscape plug-in, called ScriptActive, that enables you to embed an ActiveX control in a page to be read with Netscape Navigator. The HTML for the page must be changed, as shown in Listing 20.2. (Resist the temptation to get the plug-in and load this HTML into Netscape yourself until you have registered the control as safe for initializing and scripting in the next section.)
TIP: You can download a demonstration version of the plug-in for a free 30-day trial from the NCompass Labs Web site.
<HTML> <HEAD> <TITLE>A Web page with a rolling die</TITLE> </HEAD> <BODY> <OBJECT ID="Dieroll1" CLASSID="CLSID:46646B43-EA16-11CF-870C-00201801DDD6" CODEBASE="dieroll.cab#Version=1,0,0,1" WIDTH="200" HEIGHT="200"> <PARAM NAME="ForeColor" VALUE="0"> <PARAM NAME="BackColor" VALUE="16777215"> <PARAM NAME="Image" VALUE="beans.bmp"> <EMBED LIVECONNECT NAME="Dieroll1" WIDTH="200" HEIGHT="200" CLASSID="CLSID:46646B43-EA16-11CF-870C-00201801DDD6" TYPE="application/oleobject" CODEBASE="dieroll.cab#Version=1,0,0,1" PARAM_ForeColor="0" PARAM_BackColor="16777215"> </OBJECT> <BR> Here is some text after the die </BODY>
</HTML>
It is the <EMBED> tag that brings up the plug-in. Because it's inside the <OBJECT>...</OBJECT> tag, Microsoft Internet Explorer and other browsers that know the OBJECT tag will ignore the EMBED. This means that this HTML source will display the control equally well in Netscape Navigator and in Explorer. You'll probably want to include a link on your page to the NCompass page to help your readers find the plug-in and learn about it.
Microsoft is committed to establishing ActiveX controls as a cross-platform, multibrowser solution that will, in the words of its slogan, "Activate the Internet." The ActiveX control specification is no longer a proprietary document but has been released to a committee that will maintain the standard. Don't pay any attention to people who suggest you should only build these controls if your readers use Internet Explorer!
For any of your readers 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 isn't 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 filename 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. Figure 20.3 shows the error message displayed in Explorer when you are using the Medium safety level and load a page featuring a control that isn't registered as script-safe or init-safe. The NCompass Labs plug-in, ScriptActive, also refuses to load controls that are not registered as script-safe and init-safe.
FIG. 20.3 Explorer alerts you to controls that might run amok.
First, you need to add three functions to DierollCtl.cpp. (They come unchanged from the ActiveX SDK.) These functions are called by code presented later in this section. Don't forget to add declarations of these functions to the header file, too. The code is in Listing 20.3.
//////////////////////////////////////////////////////////////// // 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;
}
Second, 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 and adds this control to that category. Then it creates a category called CATID_SafeForInitializing and adds the control to that category as well. Listing 20.4 shows the new version of UpdateRegistry().
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); }
}
To confirm that this works, open Explorer and set your safety level to Medium. Load the HTML page that uses the control; it should warn you the control is unsafe. Then make these changes, build the control, and reload the page. The warning will not reappear.
Java is an application development language as well as an applet development language, which means you can develop ActiveX controls in Java if you choose to, using a tool like Microsoft's Visual J++ integrated into Developer Studio. When most people frame a showdown like ActiveX versus Java, though, they mean ActiveX versus Java applets, which are little, tightly contained applications that run on a Web page and can't run standalone.
Many people are concerned about the security of running an application they did not code, when they do not know the person or organization supplying the application. The Java approach attempts to restrict the actions that applets can perform so that even malicious applets can't do any real damage. However, regular announcements of flaws in the restriction approach are damaging Java's credibility. Even if a Java applet were guaranteed to be safe, these same restrictions prevent it from doing certain useful tasks, since they cannot read or write files, send email, or load information from other Internet sites.
The approach taken by Microsoft with ActiveX is the trusted supplier approach, which is extendable to Java and any other code that can execute instructions. Code is digitally signed so that you are sure who provided it and that it has not been changed since it was signed. This won't prevent bad things from happening if you run the code, but it will guarantee that you know who is to blame if bad things do occur. This is just the same as buying shrink- wrapped software from the shelf in the computer store. For more details, look at http://www.microsoft.com/ie/most/howto/trusted.htm and follow some of the links from that page.
Probably the biggest difference between the ActiveX approach and the Java applet approach is downloading. Java code is downloaded every time you load the page that contains it. ActiveX code is downloaded once, unless you already have the control installed some other way (perhaps a CD-ROM was sent to you in a magazine, for example) and then never again. A copy is stored on the user's machine and entered in the Registry. The Java code that is downloaded is small because most of the code involved is in the Java Virtual Machine installed on your computer, probably as part of your browser.
The ActiveX code that's downloaded can be much larger, though the optimizations discussed in the next section can significantly reduce the size by relying on DLLs and other code already on the user's computer. If users come to this page once and never again, they might be annoyed to find ActiveX controls cluttering their disk and Registry. On the other hand, if they come to the same page repeatedly, they will be pleased to find that there is no download time: The control simply activates and runs.
There are still other differences. Java applets can't fire events to notify the container that something has happened. Java applets can't be licensed and often don't distinguish between design-time and runtime use. Java applets can't be used in Visual Basic forms, VC++ programs, or Word documents in the same way that ActiveX controls can. ActiveX controls are nearly 10 times faster than Java applets. In their favor, Java applets are genuinely multiplatform and typically smaller than the equivalent ActiveX control.
Microsoft did not develop OCX controls to be placed in Web pages, and changing their name to ActiveX controls didn't magically make them faster to load or smaller. So the AppWizard that comes with Visual C++ has a number of options available to achieve those ends. This chapter changes these options in the Dieroll control that was already created, just to show how it's done. Because Dieroll is already a lean control and loads quickly, these simple changes won't make much difference. It's worth learning the techniques, though, for your own controls, which will surely be fatter than Dieroll.
The first few options to reduce your control's size have always been available on Step 2 of the ActiveX ControlWizard:
If you are developing your control solely for the Web, many of these settings won't matter anymore. For example, it doesn't matter whether your control has an About box; users won't be able to bring it up when they are viewing the control in a Web page.
The Activates When Visible option is very important. Activating a control takes a lot of overhead activity and should be postponed as long as possible so that your control appears to load quickly. If your control activates as soon as it is visible, you'll add to the time it takes to load your control. To deselect this option in the existing Dieroll code, open the Dieroll project in Developer Studio if it isn't still open, and open DierollCtl.cpp with FileView. Look for a block of code like the one in Listing 20.5.
///////////////////////////////////////////////////////////////////////////// // Control type information static const DWORD BASED_CODE _dwDierollOleMisc = OLEMISC_ACTIVATEWHENVISIBLE | OLEMISC_SETCLIENTSITEFIRST | OLEMISC_INSIDEOUT | OLEMISC_CANTLINKINSIDE | OLEMISC_RECOMPOSEONRESIZE;
IMPLEMENT_OLECTLTYPE(CDierollCtrl, IDS_DIEROLL, _dwDierollOleMisc)
Delete the OLEMISC_ACTIVATEWHENVISIBLE line. Build a release version of the application. Though the size of the Dieroll OCX file is unchanged, Web pages with this control should load more quickly because the window isn't created until the user first clicks on the die. If you reload the Web page with the die in it, you'll see the first value immediately, even though the control is inactive. The window is created to catch mouse clicks, not to display the die roll.
There are more optimizations available. Figure 20.4 shows the list of advanced options for ActiveX ControlWizard, reached by clicking the Advanced button on Step 2. You can choose each of these options when you first build the application through the ControlWizard. They can also be changed in an existing application, saving you the trouble of redoing AppWizard and adding your own functionality again. The options are
FIG. 20.4 The Advanced button on Step 2 of the ActiveX ControlWizard leads to a choice of optimizations.
Windowless activation is going to be very popular because of the benefits it provides. If you want a transparent control or one that isn't a rectangle, you must use windowless activation. However, because it reduces code size and speeds execution, every control should consider using this option. Modern containers provide the functionality for the control. In older containers, the control creates the window anyway, denying you the savings but ensuring that the control still works.
To implement the Windowless Activation option in Dieroll, override CDierollCtrl::GetControlFlags() like this:
DWORD CDierollCtrl::GetControlFlags() { return COleControl::GetControlFlags()| windowlessActivate; }
Add the function quickly by right-clicking CDierollCtrl in ClassView and choosing Add Member Function. If you do this to Dieroll, build it, and reload the Web page that uses it, you'll notice no apparent effect because Dieroll is such a lean control. You'll at least notice that it still functions perfectly and doesn't mind not having a window.
The next two options, Unclipped Device Context and Flicker-Free Activation, are not available to windowless controls. In a control with a window, choosing Unclipped Device Context means that you are completely sure that you never draw outside the control's client rectangle. Skipping the checks that make sure you don't means your control runs faster, though it could mean trouble if you have an error in your draw code. If you were to do this in Dieroll, the override of GetControlFlags() would look like this:
DWORD CDierollCtrl::GetControlFlags() { return COleControl::GetControlFlags()& ~clipPaintDC; }
Don't try to combine this with windowless activation: It doesn't do anything.
Flicker-free activation is useful for controls that draw their inactive and active views identically. (Think back to Chapter 15, "Building an ActiveX Server Application," in which the server object was drawn in dimmed colors when the objects were inactive.) If there is no need to redraw, because the drawing code is the same, you can select this option and skip the second draw. Your users won't see an annoying flicker as the control activates, and activation will be a tiny bit quicker. If you were to do this in Dieroll, the GetControlFlags() override would be
DWORD CDierollCtrl::GetControlFlags() { return COleControl::GetControlFlags()| noFlickerActivate; }
Like unclipped device context, don't try to combine this with windowless activation: It doesn't do anything.
Mouse pointer notifications, when inactive, enable more controls to turn off the Activates When Visible option. If the only reason to be active is to have a window to process mouse interactions, this option will divert those interactions to the container through an IPointerInactive interface. To enable this option in an application that is already built, you override GetControlFlags()again:
DWORD CDierollCtrl::GetControlFlags() { return COleControl::GetControlFlags()| pointerInactive; }
Now your code will receive WM_SETCURSOR and WM_MOUSEMOVE messages through message map entries, even though you have no window. The container, whose window your control is using, will send these messages to you through the IPointerInactive interface.
The other circumstance under which you might want to process window messages while still inactive, and so without a window, is if the user drags something over your control and drops it. The control needs to activate at that moment so that it has a window to be a drop target. You can arrange that with an override to GetActivationPolicy():
DWORD CDierollCtrl::GetActivationPolicy() { return POINTERINACTIVE_ACTIVATEONDRAG; }
Don't bother doing this if your control isn't a drop target, of course.
The problem with relying on the container to pass on your messages through the IPointerInactive interface is that the container might have no idea such an interface exists and no plans to pass your messages on with it. If you think your control might end up in such a container, don't remove the OLEMISC_ACTIVATEWHENVISIBLE flag from the block of code shown previously in in Listing 20.5
Instead, combine another flag, OLEMISC_IGNOREACTIVATEWHENVISIBLE, with these flags using the bitwise or operator. This oddly named flag is meaningful to containers that understand IPointerInactive and means, in effect, "I take it back-- don't activate when visible after all." Containers that don't understand IPointerInactive don't understand this flag either, and your control will activate when visible and thus be around to catch mouse messages in these containers.
Optimized drawing code is only useful to controls that will be sharing the container with a number of other drawing controls. As you might recall from Chapter 5, "Drawing on the Screen," the typical pattern for drawing a view of any kind is to set the brush, pen, or other GDI object to a new value, saving the old. Then you use the GDI object and restore it to the saved value. If there are several controls doing this in turn, all those restore steps can be skipped in favor of one restore at the end of all the drawing. The container saves all the GDI object values before instructing the controls to redraw and afterwards restores them all.
If you would like your control to take advantage of this, you need to make two changes. First, if a pen or other GDI object is to remain connected between draw calls, it must not go out of scope. That means any local pens, brushes, and fonts should be converted to member variables so that they stay in scope between function calls. Second, the code to restore the old objects should be surrounded by an if statement that calls COleControl::IsOptimizedDraw() to see whether the restoration is necessary. A typical draw routine would set up the colors and proceed like this:
... if(!m_pen.m_hObject) { m_pen.CreatePen(PS_SOLID, 0, forecolor); } if(!m_brush.m_hObject) { m_brush.CreateSolidBrush(backcolor); } CPen* savepen = pdc->SelectObject(&m_pen); CBrush* savebrush = pdc->SelectObject(&m_brush); ... // use device context ... if(!IsOptimizedDraw()) { pdc->SelectObject(savepen); pdc->SelectObject(savebrush); } ...
The device context has the addresses of the member variables, so when it lets go of them at the direction of the container, their m_hObject member becomes NULL. As long as it isn't NULL, there is no need to reset the device context, and if this container supports optimized drawing code, there is no need to restore it either.
If you select this optimized drawing code option from the Advanced button in AppWizard Step 2, the if statement with the call to IsOptimizedDraw() is added to your draw code, with some comments to remind you what to do.
The last optimization option, Loads Properties Asynchronously, is covered in the next section.
Asynchronous refers to spreading out activities over time and not insisting that one activity be completed before another can begin. In the context of the Web, it's worth harking back to the features that made Netscape Navigator better than Mosaic, way back when it was first released. The number one benefit cited by people who were on the Web then was that the Netscape browser, unlike Mosaic, could display text while pictures were still loading. This is classic asynchronous behavior. You don't have to wait until the huge image files have transferred, to see what the words on the page are and whether the images are worth waiting for.
Faster Internet connections and more compact image formats have lessened some of the concerns about waiting for images. Still, being asynchronous is a good thing. For one thing, waiting for video clips, sound clips, and executable code has made many Web users long for the good old days when they had to wait only 30 seconds for pages to find all their images.
The die that comes up in your Web page is the default die appearance. There's no way for the user to access the control's properties. The Web page developer can, using the <PARAM> tag inside the <OBJECT> tag. (Browsers that ignore OBJECT also ignore PARAM.) Here's the PARAM tag to add to your HTML between <OBJECT> and </OBJECT> to include a die with a number instead of dots:
<PARAM NAME="Dots" value="0">
The PARAM tag has two attributes: NAME provides a name that matches the external ActiveX name (Dots), and value provides the value (0, or FALSE). The die displays with a number.
To demonstrate the value of asynchronous properties, Dieroll needs to have some big properties. Because this is a demonstration application, the next step is to add a big property. A natural choice is to give the user more control over the die's appearance. The user (which means the Web page designer if the control is being used in a Web page) can specify an image file and use that as the background for the die. Before you learn how to make that happen, imagine what the Web page reader will have to wait for when loading a page that uses Dieroll:
When Dieroll gains another property--an image file that might be quite large--there will be another delay while the image file is retrieved from wherever it is kept. If nothing happens in the meantime, the Web page reader will eventually tire of staring at an empty square and go to another page. Using asynchronous properties means that the control can roughly draw itself and start to be useful, even while the large image file is still being downloaded. For Dieroll, drawing the dots on a plain background, using GetBackColor(), will do until the image file is ready.
A BLOB is a binary large object. It's a generic name for things like the image file you are about to add to the Dieroll control. The way a control talks to a BLOB is through a moniker. That's not new. It's just that monikers have always been hidden away inside OLE. If you already understand them, you still have a great deal more to learn about them because things are changing with the introduction of asynchronous monikers. If you've never heard of them before, no problem. Eventually there will be all sorts of asynchronous monikers, but at the moment only URL monikers have been implemented. These are a way for ActiveX to connect BLOB properties to URLs. If you're prepared to trust ActiveX to do this for you, you can achieve some amazing things. The remainder of this subsection explains how to work with URL monikers to load BLOB properties asynchronously.
Remember, the idea here is that the control will start drawing itself even before it has all its properties. Your OnDraw() code will be structured like this:
// prepare to draw if(AllPropertiesAreLoaded) { // draw using the BLOB } else { // draw without the BLOB } //cleanup after drawing
There are two problems to solve here. First, what will be the test to see whether all the properties are loaded? Second, how can you arrange to have OnDraw() called again when the properties are ready, if it's already been called and has already drawn the control the BLOBless way?
The first problem has been solved by adding two new functions to COleControl. GetReadyState()returns one of these values:
The function InternalSetReadyState() sets the ready state to one of these values.
The second problem, getting a second call to OnDraw() after the control has already been drawn without the BLOB, has been solved by a new class called CDataPathProperty and its derived class CCachedDataPathProperty. These classes have a member function called OnDataAvailable() that catches the Windows message generated when the property has been retrieved from the remote site. The OnDataAvailable() function invalidates the control, forcing a redraw.
Make a copy of the Dieroll folder you created in Chapter 17 and change it to windowless activation as described earlier in this chapter. Now you're ready to begin. There is a lot to do to implement asynchronous properties, but each step is straightforward.
Add the CDierollDataPathProperty Class Bring up ClassWizard, click the Automation tab, and click the Add Class button. From the drop-down menu that appears under the button, choose New. This brings up the Create New Class dialog box. Name the class CDierollDataPathProperty. Click the drop-down box for Base Class and choose CCachedDataPathProperty. The dialog box will resemble Figure 20.5. Click OK to create the class and add it to the project.
FIG. 20.5 Create a new class to handle asynchronous properties.
The reason that the new class should inherit from CCachedDataProperty is that it will load the property information into a file, which is an easier way to handle the bitmap. If the control has a property that was downloaded because it changed often (for example, current weather), CDataPathProperty would be a better choice.
Add the Image Property to CDierollCtrl With the new CDierollDataPathProperty class added to the Dieroll control, add the property to the original CDierollCtrl class that you copied: In ClassWizard, on the Automation tab, make sure that CDierollCtrl is selected in the far right drop-down box. Click Add Property and fill out the dialog as shown in Figure 20.6. The external name you choose is the one that will appear in the HTML: Image is simple and doesn't require a lot of typing. The type should be BSTR--that choice won't be in the drop-down box for type until you change the Implementation to Get/Set Methods.
FIG. 20.6 The image file is added as a BSTR property.
ClassWizard adds the Get and Set functions to your control class, but the TODO comments (see Listing 20.6) are cryptic.
BSTR CDierollCtrl::GetImage() { CString strResult; // TODO: Add your property handler here return strResult.AllocSysString(); } void CDierollCtrl::SetImage(LPCTSTR lpszNewValue) { // TODO: Add your property handler here SetModifiedFlag();
}
As with other Get and Set properties, you'll have to add a member variable to the control class and add code to these functions to get or set its value. It is an instance of the new CDierollDataPathProperty class. Right-click CDierollCtrl in ClassView and choose Add Member Variable. Figure 20.7 shows how to fill in the dialog box to declare the member variable mdpp_image. (The dpp in the name is to remind you that this is a data path property.)
FIG. 20.7 The image file member variable is an instance of the new class.
Now you can finish the Get and Set functions, as shown in Listing 20.7.
BSTR CDierollCtrl::GetImage() { CString strResult; strResult = mdpp_image.GetPath(); return strResult.AllocSysString(); } void CDierollCtrl::SetImage(LPCTSTR lpszNewValue) { Load(lpszNewValue, mdpp_image); SetModifiedFlag();
}
At the top of the header file for CDierollCtrl, add this include statement:
#include "DierollDataPathProperty.h"
Now there are some bits and pieces to deal with because you are changing an existing control rather than turning on asynchronous properties when you first built Dieroll. First, in CDierollCtrl::DoPropExchange(), arrange persistence and initialization for mdpp_image by adding this line:
PX_DataPath( pPX, _T("Image"), mdpp_image);
Second, add a line to the stub of CDierollCtrl::OnResetState() that ClassWizard provided, to reset the data path property when the control is reset. Listing 20.8 shows the function.
///////////////////////////////////////////////////////////////////////////// // CDierollCtrl::OnResetState - Reset control to default state void CDierollCtrl::OnResetState() { COleControl::OnResetState(); // Resets defaults found in DoPropExchange mdpp_image.ResetData();
}
Add the ReadyStateChange Event and the ReadyState Property Use ClassWizard to add the stock event ReadyStateChange. In ClassWizard, click the ActiveX Events tab, then the Add Event button. Choose ReadyStateChange from the drop-down box and click OK. Figure 20.8 shows the Add Event dialog box for this event. Events, as discussed in Chapter 17, notify the control's container that something has happened within the control. In this case, what has happened is that the rest of the control's data has arrived and the control's state of readiness has changed.
FIG. 20.8 Add a stock event to notify the container of a change in the control's readiness.
Use ClassWizard to add a property to CDierollCtrl for the ready state. In ClassWizard, click the Automation tab, then the Add Property button. Choose ReadyState from the drop-down box, and because this is a stock property, the rest of the dialog box is filled in for you, as shown in Figure 20.9. Click OK to finish adding the property and then close ClassWizard. ClassWizard doesn't add a stub function for GetReadyState() because CDierollCtrl will inherit this from COleControl.
FIG. 20.9 Add a stock property to track the control's readiness.
Add code to the constructor to connect the cached property to this control and to initialize the member variable in COleControl that is used in COleControl::GetReadyState() and set by COleControl::InternalSetReadyState(). Because the control can be used right away, the readiness state should start at READYSTATE_INTERACTIVE. Listing 20.9 shows the new constructor.
CDierollCtrl::CDierollCtrl() { InitializeIIDs(&IID_DDieroll, &IID_DDierollEvents); mdpp_image.SetControl(this); m_lReadyState = READYSTATE_INTERACTIVE;
}
Implement CDierollDataPathProperty There is some work to do in CDierollDataPathProperty before changing CDierollCtrl::OnDraw(). This class loads a bitmap, and this chapter isn't going to explain most of what's involved in reading a BMP file into a CBitmap object. The most important function is OnDataAvailable(), which is in Listing 20.10. Add this function to the class by right-clicking CDierollCtrl in ClassView and choosing Add Virtual Function. Select OnDataAvailable from the list on the left, and click Add and Edit; then type this code.
void CDierollDataPathProperty::OnDataAvailable(DWORD dwSize, DWORD grfBSCF) { CCachedDataPathProperty::OnDataAvailable(dwSize, grfBSCF); if(grfBSCF & BSCF_LASTDATANOTIFICATION) { m_Cache.SeekToBegin(); if (ReadBitmap(m_Cache)) { BitmapDataLoaded = TRUE; // safe because this control has only one property: GetControl()->InternalSetReadyState(READYSTATE_COMPLETE); GetControl()->InvalidateControl(); } }
}
Every time a block of data is received from the remote site, this function is called. The first line of code uses the base class version of the function to deal with that block and set the flag called grfBSCF. If, after dealing with the latest block, the download is complete, the ReadBitmap() function is called to read the cached data into a bitmap object that can be displayed as the control background. (The code for ReadBitmap() isn't presented or discussed here, though it is on the Web site for you to copy into your application.) After the bitmap has been read, the control's ready state is complete and the call to InvalidateControl() arranges for a redraw.
Revise CDierollCtrl::OnDraw() The structure of CDierollCtrl::OnDraw() was laid out long ago. In this block of code, the background is filled in before the code that checks whether to draw dots or a number:
COLORREF back = TranslateColor(GetBackColor()); CBrush backbrush; backbrush.CreateSolidBrush(back); pdc->FillRect(rcBounds, &backbrush);
Replace that block with the one in Listing 20.11.
CBrush backbrush; BOOL drawn = FALSE; if (GetReadyState() == READYSTATE_COMPLETE) { CBitmap* image = mdpp_image.GetBitmap(*pdc); if (image) { CDC memdc; memdc.CreateCompatibleDC(pdc); memdc.SelectObject(image); BITMAP bmp; // just for height and width image->GetBitmap(&bmp); pdc->StretchBlt(0, // upper left 0, // upper right rcBounds.Width(), // target width rcBounds.Height(), // target height &memdc, // the image 0, // offset into image -x 0, // offset into image -y bmp.bmWidth, // width bmp.bmHeight, // height SRCCOPY); // copy it over drawn = TRUE; } } if (!drawn) { COLORREF back = TranslateColor(GetBackColor()); backbrush.CreateSolidBrush(back); pdc->FillRect(rcBounds, &backbrush);
}
The BOOL variable drawn ensures that if the control is complete, but something goes wrong with the attempt to use the bitmap, the control will be drawn the old way. If the control is complete, the image is loaded into a CBitmap* and then drawn into the device context. Bitmaps can only be selected into a memory device context and then copied over to an ordinary device context. Using StretchBlt() will stretch the bitmap during the copy, though a sensible Web page designer will have specified a bitmap that matches the HEIGHT and WIDTH attributes of the OBJECT tag. The old drawing code is still here, used if drawn remains FALSE.
Having made all those changes, build the control, which will register it. One way to test it would be to bring up that HTML page in Explorer again, but you might prefer to debug the control. It is possible to debug a control even though you can't run it standalone. Normally, a developer would arrange to debug the control in the test container, but you can use any application that can contain the control.
In Developer Studio, choose Project Settings. Click the Debug tab and make sure that all the lines in the far left list box are selected. Select General in the top drop-down box, and in the edit box labeled Executable for Debug Session, enter the full path to Microsoft Internet Explorer on your computer. (If there's a shorcut to Microsoft Internet Explorer on your desktop, right-click it and choose Properties to get the path to the executable. Otherwise, use the Find utility on the Start menu to find iexplore.exe. Figure 20.10 shows an example.) Now when you choose Build, Start Debug, Go or click the Go toolbar button, Explorer will launch. Open a page of HTML that loads the control, and the control will run in the debugger. You can set breakpoints, step through code, and examine variables, just as with any other application.
FIG. 20.10 Arrange to run Explorer when you debug the control.
Here's the syntax for an OBJECT tag that sets the Image property:
<OBJECT CLASSID="clsid:46646B43-EA16-11CF-870C-00201801DDD6" CODEBASE="http://www.gregcons.com/test/dieroll.ocx" ID=die1 WIDTH=200 HEIGHT=200 ALIGN=center HSPACE=0 VSPACE=0 > <PARAM NAME="Dots" VALUE="1"> <PARAM NAME="Image" VALUE="http://www.gregcons.com/test/beans.bmp"> If you see this text, your browser does not support the OBJECT tag. </BR> </OBJECT>
TIPP: Remember, don't just copy these HTML samples to your own machine if you are building Dieroll yourself. You need to use your own CLSID, an URL to the location of your copy of the OCX, and the image file you are using.
Figure 20.11 shows the control with a background image of jelly beans. It takes 30-60 seconds to load this 40KB image through the Web, and while it is loading, the control is perfectly usable as a plain die with no background image. That's the whole point of asynchronous properties, and that's what all the effort of the previous sections achieves.
FIG. 20.11 Now the die displays on a field of jelly beans or on any other image you choose.
© Copyright, Macmillan Computer Publishing. All rights reserved.