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. But the ActiveX controls generated by older versions of Visual C++ were too big and slow to put on a web page. This chapter shows how to get these controls onto your web pages, and how to write faster, sleeker controls that will make your pages a pleasure to use.
When you see a web page with an ActiveX control in it, your jaw will drop!
Yes, it's true, Netscape Navigator can display an ActiveX control. This section points you to the plug-in your users will need.
There's more than one way to allow a user to interact with a web page. We compare ActiveX controls to Java applets.
AppWizard's ActiveX ControlWizard, can add some nice optimizations to your controls.
The last thing you want your control to do is slow down your web page. The second to last thing you want is for your control to be boring.
It's a remarkably simple matter to put an ActiveX control onto a web page that you know will be loaded by Microsoft Explorer 3.0. You use the <OBJECT> tag, a relatively new addition to HTML that describes a wide variety of objects that you might want to insert into 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, ìBuilding an ActiveX Control."
Listing 20.1ófatdie.htmlóUsing <OBJECT>
<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, since you're a software developer, is to cut and paste it from dieroll.odl, the Object Description Library. 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.
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 intimidated by instructions like "open the ODL file" or who don't have the ODL file, since it's not shipped with the control. Since 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 that if the user does not 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 simply used right away. You can simply specify a URL to the OCX file, but in order to automate the DLL downloading, this CODEBASE attribute points to a CAB file. Putting your control into a CAB file will cut your download time to as little as half what it otherwise would be. 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.
If you don't have access to a web server in which you can put controls while you're developing them, use a file:// URL in the CODEBASE attribute that points to the location of the control on your hard drive.
The remainder of the attributes of the OBJECT tag should be fairly 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, that the control should appear, 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> tags 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 do not 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 does not 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 on 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 2.0. It doesn't support the OBJECT tag, so it doesn't show the die. And Netscape Navigator is used by well over half of 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 cannot show ActiveX controls.
The size issue is a bigger worry. The release version of the Dieroll control, as built for Chapter 17, "Building an ActiveX Control," is 26 K. Many designers put a 50K 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 will then be able to 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 does not automatically download and install this DLL, though using a CAB file as discussed earlier can make it possible.
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.
It might occur to you to try linking the MFC Library statically into your control. It seems easy enough to do: Choose Build 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 into a control.) Setting up another library to link in those functions is beyond the scope of this chapter, especially since all this work would lead to an enormous (over 1M) control that would take far too long to download the first time.
NCompass Labs (www.ncompasslabs.com) has produced a Netscape plug-in, called ControlActive, that allows you to embed an ActiveX control into a page that will be read with Netscape Navigator. Just look at Figure 20.3, which shows the Dieroll in Netscape. The HTML for this page is shown in Listing 20.2. (Resist the temptation to load this HTML into Netscape yourself until you have registered the control as safe for initializing and scripting in the next section.)
Fig. 20.3 ActiveX controls can be displayed in Netscape Navigator with the Ncompass Labs plug-in.
Listing 20.2ófatdie2.htmlóUsing <OBJECT> and <EMBED>
<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 will probably want to include a link on your page to the NCompass page to help your readers get and learn about the plug-in.
Microsoft is committed to establishing ActiveX controls as a cross-platform, multi-browser 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. So 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 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. Figure 20.4 shows the error message displayed in Explorer when you are using the Medium safety level and load a page featuring a control that is not registered as script-safe or init-safe. The NCompassLabs plug-in, ControlActive, also refuses to load controls that are not registered as script-safe and init-safe.
Fig. 20.4 Explorer alerts you to controls that might run amok.
First, you need to add three functions to DierollCtl.cpp. (These come unchanged from the ActiveX SDK.) These functions are called by code presented later in this section. Don't forget to add declarations of the these functions to the header file too. The code is in Listing 20.3.
Listing 20.3óDierollCtl.cppónew functions to mark the control as safe
////////////////////////////////////////////////////////////////
// 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", then 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().
Listing 20.4óDierollCtl.cppóCDierollCtrl::CDierollCtrlFactory::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. But when most people frame a showdown like "ActiveX vs. Java," they mean ActiveX vs. Java applets, which are little tightly contained applications that run on a Web page and cannot run stand-alone.
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 cannot do any real damage. But regular announcements of flaws in the restriction approach are damaging the credibility of Java. Even if a Java applet was guaranteed to be safe, these same restrictions keep applets from doing certain useful tasks.
The approach taken by Microsoft with ActiveX is the trusted supplier approach, which is extendable to Java and any other code that can run. 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 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 was sent to you in a magazine, for example) and then never again. A copy is stored on the user's machine and entered into the Registry. The Java code that is downloaded is small, because most of the code that's 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 reduce the size significantly by relying on DLLs and other code already on the user's computer. If users will come to this page once and never again, they may be annoyed to find ActiveX controls cluttering up 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 cannot fire events to notify the container that something has happened. Java applets cannot be licensed, and often donít distinguish between design-time and run-time use. Java applets canít be used in Visual Basic forms or VC++ programs or Word documents in the same way that ActiveX controls can. ActiveX controls are about 10 times faster than Java applets. In their favor, Java applets are genuinely multi-platform and typically smaller than the equivalent ActiveX control.
Microsoft did not develop OCX controls to be placed into web pages, and changing their name to ActiveX controls didn't magically make them smaller, or faster to load. So the AppWizard that comes with Visual C++ has a number of options available to achieve those ends. This chapter will change these options in the Dieroll control that was aleady created just to show how it's done. Since Dieroll is a fairly lean control already, 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 the size of your control have always been available on Step 2 of the ActiveX Control Wizard:
If you are developing your control entirely for the web, many of these settings won't matter anymore. For example, it doesnít matter whether or not 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 will 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, and open DierollCtl.cpp with FileView. Look for a block of code like the one in Listing 20.5.
Listing 20.5óExcerpt from DierollCtl.cppóSetting Activates When Visible
/////////////////////////////////////////////////////////////////////////////
// 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, since the window is not 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.5 shows the list of advanced options for ActiveX Control Wizard, reached by clicking the Advanced button on Step 2. You can choose each of these options when you first build the application through the Control Wizard. 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.5 The Advanced button on Step 2 of the ActiveX Control Wizard 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 is not 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 will provide the functionality for the control. In older containers, the control will create the window anyway, denying you the savings but ensuring 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 Class View and choosing Add Function. If you do this to Dieroll, build it, and reload the web page that uses it, you will notice no apparent effect, because Dieroll is such a lean control. You will 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 client rectangle of the control. 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 which 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 activate 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 may have no idea such an interface exists, and have no plans to pass your messages on with it. If you think your control might end up in such a container, then don't remove the OLEMISC_ACTIVATEWHENVISIBLE flag from the block of code, like the one in Listing 20.6
Listing 20.6óExcerpt from DierollCtl.cppóFinetuning activates when visible
/////////////////////////////////////////////////////////////////////////////
// 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)
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 may recall from Chapter 6, "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 use the GDI object, then restore it to the saved value. If there are a number of controls doing this in turn, all those restore steps could be skipped in favour 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 restores them all afterwards.
If you would like your control to take advantage of this, there are two changes to be made. 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 if the restoration is necessary. A typical draw routine would set up the colors, then 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 is not 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 with AppWizard, 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 of the optimization options, loads properties asynchronously, and(?) is covered in the next section.
Asynchronous refers to spreading out activities over time, and not insisting that one activity be complete 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 only had to wait 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 properties of the control. 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 rather than dots:
<PARAM NAME="Dots" value="0">
The PARAM tag has two attributes: NAME provides a name that matches the external ActiveX name(Dots in this case)and value provides the value(0, or FALSE, in this case.)The die displays with a number.
In order to demonstrate the value of asynchronous properties, Dieroll needs to have some big properties. So, since 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 appearance of the die. 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 see 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 away to another page. Using asynchronous properties means that the control can draw itself roughly 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 we 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 understood them, you will 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 of 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 if all the properties are loaded? And 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()which 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, "Building an ActiveX Control," (or the Chapter 17 code from the CD) and change it to windowless activation as described earlier in this chapter. Now you're ready to begin. There is a lot to be done to implement asynchronous properties, but each step is quite straightforward.
Add the CDierollDataPathProperty class
Bring up Class Wizard, 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 should resemble Figure 20.6. Click OK to create the class and add it to the project.
Fig. 20.6 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, and that 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) then 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, like this: in Class Wizard, on the Automation tab, make sure that CDierollCtrl is selected in the rightmost drop-down box. Click Add Property, and fill out the dialog as shown in Figure 20.7. 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 OLE_BSTRóthat choice won't be in the drop-down box for type until you change the Implementation to Get/Set method.
Fig. 20.7 The image file is added as a BSTR property.
Class Wizard adds the Get and Set functions to your control class, but the TODO comments (see Listing 20.7) are a little cryptic.
Listing 20.7óDierollCtl.cppóGet and Set functions
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 will 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 onCDierollCtrl in Class View and choose Add Member Variable. Figure 20.8 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.8 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.8.
Listing 20.8óDierollCtl.cppóCompleted Get and Set functions
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 Class Wizard provided, to reset the data path property when the control is reset. The function is shown in Listing 20.9.
Listing 20.9óDierollCtl.cppóCDierollCtrl::OnResetState()
/////////////////////////////////////////////////////////////////////////////
// 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 Class Wizard to add the stock event ReadyStateChange. In Class Wizard, click the ActiveX Events tab, then the Add Event button. Choose ReadyStateChange from the drop-down box and click OK. Figure 20.9 shows the Add Event dialog box for this event. Events, as discussed in Chapter 17, notify the container of the control 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.9 Add a stock event to notify the container of a change in the readiness of the control.
Use Class Wizard to add a property to CDierollCtrl for the ready state. In Class Wizard, click the Automation tab, then the Add Property button. Choose ReadyState from the dropdown box, and since this is a stock property, the rest of the dialog box is filled in for you, as shown in Figure 20.10. ClassWizard doesn't add a stub function for GetReadyState()because CDierollCtrl will inherit this from COleControl.
Fig. 20.10 Add a stock property to track the readiness of the control.
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(). Since the control can be used right away, the readiness state should start at READYSTATE_INTERACTIVE. Listing 20.10 shows the new constructor:
Listing 20.10óDierollCtl.cppóCDierollCtrl::GetImage()
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 is not 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.11. 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 in this code.
Listing 20.11óDierollDataPathProperty.cppóOnDataAvailable()
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() will not be presented or discussed here, though it is on the CD for those who would like to read it. Once the bitmap has been read, the control's ready state is complete and the call to InvalidateControl() arranges for a redraw.
The structure of CDierollCtrl::OnDraw()was laid out long ago. The background is filled in before the code that checks whether to draw dots or a number, in this block of code:
COLORREF back = TranslateColor(GetBackColor());
CBrush backbrush;
backbrush.CreateSolidBrush(back);
pdc->FillRect(rcBounds, &backbrush);
Replace that block with the one in Listing 20.12.
Listing 20.12óDierollDataPathProperty.cppónew code for OnDraw()
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 may prefer to debug the control. It is possible to debug a control even though you cannot 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 leftmost 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. (Figure 20.9 shows an example.) Now when you choose Build 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.11 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>
Remember, don't just copy these HTML samples to your own machine if you are bulding Dieroll yourself. You need to use your own CLSID, a URL to the location of your copy of the OCX, and the image file you are using.
Figure 20.12 shows the control with a background image of jelly beans. It takes thirty seconds to a minute to load this 40K 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.12 Now the die displays on a field of jelly beans, or any other image you choose.
ActiveX controls are changing fast. Watch for more announcements from Microsoft of development kits and other add-ons to make building Internet-ready controls faster and easier.
If youíd like to read more background about ActiveX programming, be sure to check the electronic copy of ìActiveX Programming with Visual C++î which is included in its entirety on the CD included with this book.
If you'd like to learn more about programming for the Internet, try one of these chapters:
© 1997, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company.