Take Control!

Al Williams

Al is a consultant and author based in the Houston area. His most recent book is Developing ActiveX Web Controls (Coriolis Group,1996). You can find Al on the Web at http://ourworld.compuserve.com/homepages/Al_Williams.


It wasn't long ago that most people thought OOP was the sound made by cartoon characters when they were hit in the stomach. Back then, one of the best examples of an object-oriented language was Smalltalk. Around that time, a friend of mine walked into my office with a magazine advertisement for a popular Smalltalk product. "Object-oriented programming will change everything," he said. "Look at this. A whole text editor in three lines of code."

Of course, that was possible not because of OOP, but because that Smalltalk product came with a nice library that wrapped a Windows edit control. To write a text editor from scratch in Smalltalk would be the usual nightmare, of course.

The same is true of Visual Basic. A great deal of VB's ease of use derives from the selection of components that you can easily incorporate into your programs. However, what happens when you can't find a VB control to suit your needs? Should you create your own?

Delphi allows you to write new components easily using Delphi. Alas, this isn't true with VB. Instead, you must write ActiveX controls (formerly known as OCXs), and that typically means using C or C++. In particular, Visual C++ has a C++ library (MFC) that greatly simplifies writing ActiveX controls. You can write them from scratch or subclass existing Windows controls.

Even if you have just a little experience with C++, you'll find it surprisingly easy to write your own custom controls. Custom controls allow you to write code in C++ that might be difficult to write (or slow to run) in VB. Also, you can distribute your controls to other programmers who might not be able to duplicate your efforts. Finally, creating a custom OCX control is a handy way to put your own code right on the VB palette for personal use.

The basic steps required for using MFC to create an ActiveX control are simple. First, you run a special wizard to create a basic control. The wizard automatically generates everything you need. If you build the project, you'll have a generic, useless (but functioning) control.

You'll want to add code to paint your control in a meaningful way. You'll also use a special tool, Class Wizard, to add properties, methods, and events. In some cases, Class Wizard will write all the code for you. In other cases, you'll have to write code to produce the desired effect. In either case, Class Wizard will at least start you off in the right place and keep everything in synch.

Of course, you'll also use the normal MFC mechanisms to handle messages, draw, and perform other mundane tasks. This is an advantage, unless you don't know MFC. Don't worry, though. The amount of MFC you need to know to write most controls is minimal compared to the amount of MFC you need for a regular application. If you've struggled with document/view architecture and other MFC oddities, you'll be happy to know you don't need all of that for a control.

Using Control Wizard

To start your control, select New from the File menu. In the resulting dialog box, select Project Workspace. This creates a new project with its own makefile and sample source files. Once you select Project Workspace, you'll see an odd dialog box that has a vertical strip of icons on the left side (Figure 1). The icon you want to select is the one labeled Control Wizard. You can select a project name and directory using the right side of the dialog. Press the Create button to proceed.

The Control Wizard uses two dialogs to collect information (Figures 2 and 3). The first dialog allows you to specify

Once you press the Next button, the final dialog box will appear (Figure 3). On this screen, you can edit the names of each class the wizard will create and change the file name it will use for each class (usually, you won't care). You can also select among the following options:

You can also elect to subclass a standard Windows control (for example, a button). This is usually not very useful, since VB already provides ordinary controls. However, if you have a custom control you'd like to use in VB, this option can be useful.

Code You Add

Often, the only code you'll directly add to the files that the Control Wizard generates is the OnDraw function. This function paints the control on a device context (DC). Instead of a normal Windows DC, this function receives an MFC CDC. This is just an object that encapsulates an ordinary device context. The CDC has member functions that correspond to ordinary device-context functions. For example, in Windows you might write TextOut(dc,0,0,"Help!",5);, while in MFC, you would write dc->TextOut(0,0,"Help!",5);. Since a CDC is a C++ object, it automatically frees itself when it goes out of scope. Therefore, you don't have to call ReleaseDC or worry about the device context's disposition.

MFC calls OnDraw when it needs to paint an active control or build a metafile for an inactive control. If you want to supply separate code for drawing the inactive control, you'll need to override OnDrawMetafile. In either case, you should realize that an inactive control's device context is in an unknown state. You'll need to initialize anything you use. It is also a good idea to use SaveDC to preserve the state of the DC so you can restore it before returning.

On occasion, you might want to add some custom code to the control's constructor or destructor. However, to add properties, methods, and events, you'll use Class Wizard to either write the code or to place stub functions in the code that you will finish writing.

The other code you might add to a control would handle ordinary window events. For example, a control that acts like a button might accept mouse events. You use Class Wizard to handle these events. This is exactly the same way that you use Class Wizard in the usual MFC program.

To handle a message in the control, start Class Wizard (you can press Control+W or select it from the View menu or the toolbar). Make certain that the Message Map tab is active and that the control object is selected (Figure 4). In the left-hand list box, make sure the object is selected, and not a menu ID. Next, select the message of interest in the right-hand list box.

With the correct items selected, click on the Add Function button (you can also double click the message in the right-side list box). This places a stub function in your code. If you want to immediately edit the code, just click on the Edit Code button. Make sure to select OK on the Class Wizard screen and not Cancel or Close. This ensures that the project retains your changes.

Adding Properties

Properties are the heart and soul of ActiveX controls. Adding them with Class Wizard couldn't be easier. Simply start Class Wizard, and select the OLE Automation tab, then press Add Property (see Figure 5). From here, you can select a stock property or name a new property. If you select a stock property, Class Wizard automatically arranges to store it and provides a notification function you can override to detect when the property changes. For example, when the stock Text property changes, the framework will call OnTextChanged. You can retrieve the text as a BSTR (an ordinary BASIC string) by using the GetText function, or as a CString (a C++ string) by calling InternalGetText. You need only inform Class Wizard that you want the stock property.

When adding a custom property, you can select between two types. The member-variable type allows you to define a member variable that corresponds to the property. If you like, you can also define a function that the framework will call when the property changes. Class Wizard automatically provides the variable and the notification function's skeleton.

Your other choice for properties is to use get and set member functions. In this case, you specify a member function that the framework calls when a container sets the property (the set function) and another function that supplies the value of the property (the get member function). You don't need to supply both functions if you want a read-only or write-only property.

When you create a member function property, Class Wizard automatically creates skeleton functions for you. The get function returns a value that has the same type as the property. The set function takes a value as an argument. You can also make pseudoarray properties by providing additional arguments to the get/set members. What you do with these functions (and any extra arguments) is up to you. If you need a variable to store the property, you'll have to define it manually.

By default, stock properties are persistent. That is, the container saves them when it saves the control, and MFC arranges to load them back in when the container loads the control. If you want any custom properties to be persistent, you need to add a special PX_ function to the DoPropExchange function. These functions (one for each common type) take the name of the property, the corresponding variable in your code, and a default value as arguments. Of course, some run-time properties may not require persistence, in which case, you don't need to take any special action in the DoPropExchange function.

Using Ambient Properties

You don't define ambient properties in your code. These are properties the container supplies to the control. You may use them, but controls don't define them. MFC provides simple functions to retrieve common ambient properties (Table 1). You can also get any arbitrary ambient property by calling GetAmbientProperty. Of course, then you need to know the property's DISPID--a unique ID that identifies each property.

Adding Methods

When you want to add a method, simply select the Add Method button from Class Wizard's OLE Automation tab. This brings up a dialog you can use to create methods (Figure 6). Each method can have up to 16 parameters. Once Class Wizard creates a stub, you can fill in the appropriate code.

Adding Events

Class Wizard makes adding events trivial. Select the OLE Events tabs. Just name the event (or select a stock event name). Events can have up to 15 arguments that you specify near the bottom of the dialog (Figure 7).

Class Wizard automatically writes a complete function that will trigger the event (the function will start with Fire, as in FireClick). You don't need to take any further action. When you want to fire the event, just call this firing function. Of course, you have to pass any arguments specified when you created the event.

The default MFC code will fire some stock events by default. For example, when the control detects a character (a WM_CHAR message), it calls FireKeyPress unless you override the OnChar message and don't call the base class. You'll find a list of window messages that MFC converts to events in Table 2.

Adding Property Sheets

MFC makes it easy to add property sheets. However, since VB uses an integral property browser, you don't have to add a property sheet to a control that you plan to use with VB. You can find more information about property sheets in the online help.

More Sophisticated Additions

MFC uses the COleControl class to represent all ActiveX controls. You can find a wealth of functions in this base class that you can override to handle certain conditions. For example, if you want to know when ambient properties change, you can override OnAmbientPropertyChange. If you want to know when the container will ignore events, override OnFreezeEvents.

Testing and Using the Control

You can embed an MFC control in any appropriate container. However, you may find it especially useful to use the test container that comes with the Visual C++ tools. This container (Figure 8) allows you to embed any control in it. Then you can examine its properties, monitor its events, and set conditions like ambient properties.

To insert your control in the test container, you'll need to run TSTCON32.EXE (usually available on the Tools menu of the Visual C++ menu). Then, select the Edit menu's Insert Control item. You can use items on the Edit, View, and Options menus to exercise and monitor the control. In particular, you can use the event-log window (found on the View menu) to monitor events that the control fires.

If you want to use your control with VB, you have to add it to the tool palette by invoking the Custom Control command from the Tools menu. Once it's on the palette, you can use it like any other VB control.

You'll find a complete ActiveX control example in the online listings. You'll see it is quite straightforward. Class Wizard adds all of the properties, methods, and events. The only code that I manually wrote is in the OnDraw function.

Subclassing A Custom Control

Although the Control Wizard will help you subclass built-in Windows controls, you won't often use this feature. VB already supplies most ordinary controls and you can only select the built-in controls from the list. However, what about custom controls that might already exist as C or C++ DLLs?

In truth, any ordinary Windows control can become an ActiveX control via subclassing. The only requirement is that the control must be able to paint itself to an arbitrary device context. There was an undocumented feature in many old-style controls that allowed for this. If you send an ordinary control a WM_PAINT message, but set the wParam parameter to an HDC, the control would paint to that device context instead of using the usual BeginPaint call to obtain an HDC.

Microsoft never officially documented this anywhere (although several MFC samples relied on this fact). With Windows 95, however, there is an official way to ask a control to paint to a specific device context. This is the WM_PRINT message. WM_PRINT takes the HDC in its wParam argument, and a function code in the lParam argument. MFC can use either of these methods to force the control to paint to a metafile. This metafile becomes the presentation for the subclassed control when it is in the inactive state.

If you need to work with a custom control, you'll have to make sure it supports these painting features. If not, you may have to do a good bit of work to draw the control. If you have the source code for the control, it may be a very simple modification to make the control aware of this protocol. For example, suppose your control's source had a section of code that looked like Example 1(a). It would be simple to change it to look like Example 1(b).

Note that this does not provide a full implementation of the WM_PRINT command, but it is adequate, at least for creating an ActiveX control. Also, this code assumes you don't use any of the information out of the PAINTSTRUCT (like the rectangle to redraw). This is often the case. If it isn't, you'll have to come up with suitable fake values for the PAINTSTRUCT when wParam is not NULL. Usually, this means using the client rectangle for the bounding box and appropriate defaults for the flags.

A Custom Control

Listing One (listings begin on page 57) is a simple control written in C. (The complete package is available electronically; see "Programmer's Services," page 3.) This control creates a countdown timer not unlike the one you see on movie reels (you know--that stuff we used back before videotape). The DLL registers a custom window class, and that class implements the countdown timer. Notice that the timer may not be accurate in certain situations. If the system is slow, the timer will count down every time it receives a WM_TIMER message. This may cause the timer to count more slowly than it would if it acted correctly, but it does allow the user to see the entire time elapse.

The protocol for using the timer is a combination of Windows messages and window extra words. You'll find several messages and constants in Listing Two. In addition to the messages, you can set the control's background color by manipulating its extra window bytes starting at offset 8. The DWORD at this address sets the color in the usual RGB format. You can use SetWindowLong and GetWindowLong to read or change the value.

When you want to set the timer to a specific number of seconds, use the TC_SET message (place the number of seconds in wParam). The TC_GETTIME message returns the current number of seconds. To start or stop the timer, send a TC_GO message. If the wParam parameter is 1, the timer starts. If it is 0, the timer stops.

When each second elapses (which may not be a true second), the timer sends a WM_CONTROL message with the TC_TICK notification code in the high part of wParam. The current time count is in lParam. When the count reaches zero, the period is complete and the timer automatically stops.

To use the control in a C or C++ program, you can load its DLL using the LoadLibrary call. Alternately, you can link with the corresponding IMPLIB. The dummy function TControl_Init provides you with a function to call so the linker will include the DLL if you elect to use the latter method.

You'll notice in the source code that this control properly handles WM_PAINT and WM_PRINT. However, if it didn't (and, in fact, it originally did not), it would be trivial to change the code.

So how can you use this control from Visual Basic? You can form an ActiveX control that subclasses the timer, and use it as you would any ActiveX control.

Subclassing the Control

Although Control Wizard won't directly allow us to subclass a custom control, we can ask it to subclass a standard control and make changes. I usually choose to subclass from a STATIC on the theory that this should be innocuous, but it doesn't seem to make any real difference. There are nine steps you'll need to take after you generate a standard subclassed control.

1. Add the TCONTROL.H file to the control's CPP file.

2. Find the name of the subclass in the PreCreateWindow method and change it to reflect the custom control's name.

3. Load the library (or make the dummy call to load the library) in the application's InitInstance member.

4. Add properties (use Class Wizard).

5. Add PX_ functions to implement persistent properties.

6. Add methods (use Class Wizard).

7. Add events (use Class Wizard).

8. Add message-map entries and message functions to fire events.

9. Modify the property page, if desired.

For this control, I decided to implement a Count property that corresponds to the current count, Go and Stop methods, and a Tick event that fires with the TC_TICK message. You can find the results in Listing Three.

There are two major twists to subclassing a control like this. First, MFC doesn't create the window until the first call to OnDraw. That means you can't do anything that requires a window handle (for example, send messages) until after that point.

The other issue is that the TC_TICK notification goes to the control's parent window, not the control itself. This poses a problem, since you need to handle the TC_TICK notification in the COleControl-derived class that represents the control, not in some class that represents the parent window.

Luckily, the solutions to these problems are straightforward. Refrain from sending messages or otherwise manipulating the window during DoPropExchange, OnResetState, or any other time when the window may not be valid. Instead, remember the state of the control and initialize it during WM_CREATE processing. You can use Class Wizard to handle WM_CREATE. Naturally, when WM_CREATE occurs, the window will be valid.

The notification problem is even simpler. MFC arranges to reflect certain messages (including WM_COMMAND) from the parent window to the control window. When the parent window receives a WM_COMMAND, it reflects an OCM_ COMMAND message to the control. You can handle this with an ON_MESSAGE message map entry (although Class Wizard will not make it for you). The Control Wizard automatically sets this up for you in the case of WM_COMMAND.

There are several other messages that MFC reflects (Table 3). You can handle these using ON_MESSAGE in a fashion similar to the OCM_COMMAND solution.

The Count property requires special handling since the window is not valid until the first paint operation occurs. The special variable m_count holds a local counter that the control object maintains in the usual way. Then, during WM_CREATE processing, the code sends the TC_SET message to the control to synchronize the control object's property with the real value in the custom control.

Using the Control

Once the control is working, it operates like any other ActiveX control (Figure 9). If you plan to ship a subclassed control, however, be aware that you have to ship both the control and the DLL that contains the original control. (Assuming you have the right to distribute that DLL, of course.)

When you install your files on a different machine, you must be sure that the custom control's DLL resides where the ActiveX control can find it. The standard SYSTEM directory is often a good choice. Without access to the base control's DLL, any attempts to use the ActiveX control will fail.

Other Places

Don't forget: ActiveX controls are not just for VB. You can use them in Visual C++, Delphi, and many other places as well. Perhaps the most exciting use for them is to provide active content for web pages. The latest Internet browsers can accept ActiveX controls using the <OBJECT> HTML tag.

Table 1: MFC functions to retrieve common ambient properties.

Ambient Property Functions

AmbientAppearance
AmbientBackColor
AmbientDisplayName
AmbientFont
AmbientForeColor
AmbientLocaleID
AmbientScaleUnits
AmbientTextAlign
AmbientUserMode
AmbientUIDead
AmbientShowGrabHandles
AmbientShowHatching

Table 3: Window message reflection (*MFC reflects this for all Win32 CTLCOLOR messages).

 
Message to Parent   Reflected Control Message

WM_COMMAND     	    OCM_COMMAND
WM_CTLCOLOR*        OCM_CTLCOLOR
WM_DRAWITEM         OCM_DRAWITEM
WM_MEASUREITEM      OCM_MEASUREITEM
WM_DELETEITEM       OCM_DELETEITEM
WM_VKEYTOITEM       OCM_VKEYTOITEM
WM_CHARTOITEM       OCM_CHARTOITEM
WM_COMPAREITEM      OCM_COMPAREITEM
WM_HSCROLL          OCM_HSCROLL
WM_VSCROLL          OCM_VSCROLL
WM_NOTIFY           OCM_NOTIFY
WM_PARENTNOTIFY     OCM_PARENTNOTIFY

Table 2: Automatic events.

 
Message	            Event

WM_KEYUP            FireKeyUp
WM_KEYDOWN          FireKeyDown
WM_CHAR             FireKeyPress
WM_?BUTTONDOWN	    FireMouseDown
WM_?BUTTONUP	    FireMouseUp
WM_?BUTTONDOWN      FireClick
WM_MOUSEMOVE        FireMouseMove

Figure 1: Starting a new ActiveX control.

Figure 3: The second Wizard dialog.

Figure 2: The first Wizard dialog.

Figure 4: Class Wizard's message map screen.

Figure 5: Adding a property.

Figure 6: Adding a method.

Figure 7: Adding an event.

Figure 8: Test container.

Figure 9: Countdown control.

Example 1: Using MFC to change the code in (a) to (b).

(a)
case WM_PAINT:
    dc=BeginPaint(hWnd,&ps);
    . . .
    EndPaint(hWnd,&ps);

(b)
case WM_PAINT:
case WM_PRINT:
  if (!wParam)
    dc=BeginPaint(hWnd,&ps);
  else
    dc=(HDC)wParam;
    . . .
  if (!wParam)
    EndPaint(hWnd,&ps);

Listing One


#include <windows.h>
#include <stdlib.h>
#include "tcontrol.h"
HANDLE hInst;
/* Dummy entry point to force DLL load */
__declspec(dllexport) void FAR PASCAL TControl_Init(void)
  {
  }
LONG FAR PASCAL ControlProc(HWND w,UINT cmd,
  WPARAM wParam,LPARAM lParam)
  {
  switch (cmd)
    {
    case WM_PAINT:
    case WM_PRINT:
      {
      RECT r,rct;
          int x,y;
      int tick,amt;
      char tmp[5];
      HDC dc;
      HBRUSH br,old,back;
      PAINTSTRUCT ps;
      if (!wParam)
                  dc=BeginPaint(w,&ps);
          else
                  {
                  dc=(HDC)wParam;
                  SaveDC(dc);
                  }
      br=CreateSolidBrush(RGB(0xFF,0,0));
          back=CreateSolidBrush(GetWindowLong(w,BACK));
      SetMapMode(dc,MM_ISOTROPIC);
      GetClientRect(w,&r);
      SetWindowExtEx(dc,20,20,NULL);
      SetViewportExtEx(dc,r.right,r.bottom,NULL);
      tick=GetWindowLong(w,TICK);
      amt=GetWindowLong(w,TIMELEFT);
          old=SelectObject(dc,back);
          PatBlt(dc,0,0,20,20,PATCOPY);
      if (tick!=8) Ellipse(dc,0,0,20,20);
      /* Select new brush */
      SelectObject(dc,br);
      switch (tick)
          {
          case 0:
            x=10;
            y=0;
            break;
          case 1:
            x=15;
            y=2;
            break;
          case 2:
            x=20;
            y=10;
            break;
          case 3:
            x=15;
            y=18;
            break;
          case 4:
            x=10;
            y=20;
            break;
          case 5:
            x=5;
            y=18;
            break;
          case 6:
            x=0;
            y=10;
            break;
          case 7:
            x=5;
            y=2;
            break;
          }
      if (tick==8)
        Ellipse(dc,0,0,20,20);
      else
        if (!Pie(dc,0,0,20,20,x,y,10,0)) MessageBeep((UINT)-1);
      SelectObject(dc,old);
      DeleteObject(br);
      DeleteObject(back);
/* Write text */
      rct.top=rct.left=0;
      rct.right=rct.bottom=20;
      itoa(amt,tmp,10);
      SetBkMode(dc,TRANSPARENT);
      DrawText(dc,tmp,-1,&rct,
        DT_CENTER|DT_VCENTER|DT_SINGLELINE);
      if (!wParam)
                  EndPaint(w,&ps);
          else
                  RestoreDC(dc,-1);
      return 0;
      }
    case WM_TIMER:  /* Do timer stuff */
      {
      int tick,amt;
      HWND parent=GetParent(w);
      tick=GetWindowLong(w,TICK);
      if (++tick==9)
        {
        tick=0;
        amt=GetWindowLong(w,TIMELEFT);
        SetWindowLong(w,TIMELEFT,--amt);
        PostMessage(parent,WM_COMMAND,MAKELONG(0,TC_TICK),amt);
        if (!amt)
          {
          KillTimer(w,1);
          }
        }
      SetWindowLong(w,TICK,tick);
      InvalidateRect(w,NULL,FALSE);
      return 0;
      }
    case TC_SET:
      SetWindowLong(w,TIMELEFT,wParam);
      SetWindowLong(w,TICK,0);
      InvalidateRect(w,NULL,FALSE);
      return 0;
    case TC_GETTIME:
      return GetWindowLong(w,TIMELEFT);
    case TC_GO:
      if (wParam)
        SetTimer(w,1,125,NULL); /* 1/8th second */
      else
        KillTimer(w,1);
      return 0;
    }
  return DefWindowProc(w,cmd,wParam,lParam);
  }
/* Register our class or unregister it */
BOOL WINAPI DllMain(HANDLE _hInst,ULONG reason,LPVOID na)
  {
  WNDCLASS wc;
  if (reason==DLL_PROCESS_ATTACH)
    {
    hInst=_hInst;
    wc.lpszClassName=TIMECTRL;
    wc.hInstance=hInst;
    wc.style=CS_GLOBALCLASS;
    wc.lpfnWndProc=ControlProc;
    wc.hbrBackground=(HBRUSH)(COLOR_WINDOW+1);
    wc.hCursor=LoadCursor(NULL,IDC_ARROW);
    wc.hIcon=NULL;
    wc.lpszMenuName=NULL;
    wc.cbClsExtra=0;
    wc.cbWndExtra=12;
    if (!RegisterClass(&wc)) return FALSE;
    }
  if (reason==DLL_PROCESS_DETACH)
    UnregisterClass(TIMECTRL,hInst);
  return TRUE;
  }



Listing Two


#define TIMECTRL  "TimeCtrl"
#define TC_SET (WM_USER)
#define TC_GO (WM_USER+1)
#define TC_TICK (WM_USER+2)
#define TC_GETTIME (WM_USER+3)

#define TIMELEFT 0
#define TICK  4
#define BACK 8

__declspec(dllimport) void FAR PASCAL TControl_Init(void);




Listing Three


// countdn.cpp : Implementation of CCountdnApp and DLL registration.

#include "stdafx.h"
#include "countdn.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

CCountdnApp NEAR theApp;

const GUID CDECL BASED_CODE _tlid = { 0xa7e2a560, 0xe914, 0x11cf, { 0xa7, 0xb2,
                                              0x44, 0x45, 0x53, 0x54, 0, 0 } };
const WORD _wVerMajor = 1;
const WORD _wVerMinor = 0;
////////////////////////////////////////////////////////////////////////////
// CCountdnApp::InitInstance - DLL initialization

BOOL CCountdnApp::InitInstance()
{
     dll=LoadLibrary("TCONTROL.DLL");
     if (!dll) return FALSE;
     BOOL bInit = COleControlModule::InitInstance();
     if (bInit)
     {
     }
     return bInit;
}
////////////////////////////////////////////////////////////////////////////
// CCountdnApp::ExitInstance - DLL termination

int CCountdnApp::ExitInstance()
{
     int rv;
     rv=COleControlModule::ExitInstance();
//     if (dll) FreeLibrary(dll);
     return rv;
}
/////////////////////////////////////////////////////////////////////////////
// DllRegisterServer - Adds entries to the system registry
STDAPI DllRegisterServer(void)
{
     AFX_MANAGE_STATE(_afxModuleAddrThis);
     if (!AfxOleRegisterTypeLib(AfxGetInstanceHandle(), _tlid))
          return ResultFromScode(SELFREG_E_TYPELIB);
     if (!COleObjectFactoryEx::UpdateRegistryAll(TRUE))
          return ResultFromScode(SELFREG_E_CLASS);
     return NOERROR;
}
/////////////////////////////////////////////////////////////////////////////
// DllUnregisterServer - Removes entries from the system registry
STDAPI DllUnregisterServer(void)
{
     AFX_MANAGE_STATE(_afxModuleAddrThis);
     if (!AfxOleUnregisterTypeLib(_tlid))
          return ResultFromScode(SELFREG_E_TYPELIB);
     if (!COleObjectFactoryEx::UpdateRegistryAll(FALSE))
          return ResultFromScode(SELFREG_E_CLASS);
     return NOERROR;
}