Programming Windows with MFC, Second Edition

[Previous] [Next]

No chapter can possibly cover everything there is to know about ActiveX controls. You've learned the essentials, but there are some additional issues that every control developer should be aware of. The following sections present three such issues.

Windowless Controls

Although ActiveX controls have existed in one form or another since 1994, 1996 was the year that they came of age. OCX 94 defined the baseline control architecture; OCX 96 introduced a number of enhancements designed to allow controls to run faster and more efficiently and consume fewer resources while doing so. One of those enhancements was the windowless control.

OCX 94_style controls are always created with windows of their own. This is consistent with the behavior of Windows controls, which also have their own windows. That's fine when you're hosting just one or two controls, but if a container creates tens, perhaps hundreds, of ActiveX controls, giving each a window of its own introduces an appreciable amount of overhead in terms of resource requirements and instantiation time.

It turns out that most controls don't really need windows of their own if their containers are willing to help out by performing a few small chores such as simulating the input focus and forwarding mouse messages. This interaction between the control and the container—the physical mechanisms that permit a windowless ActiveX control to borrow a portion of its container's window—was standardized in OCX 96.

Here, in a nutshell, is how it works. A windowless control implements the COM interface IOleInPlaceObjectWindowless; a container that supports windowless controls implements IOleInPlaceSiteWindowless. When a windowless control is created, it has no window, which means it can't receive mouse or keyboard input. Therefore, the container forwards mouse messages and, if the control has the conceptual input focus, keyboard messages to the control by calling its IOleInPlaceObjectWindowless::OnWindowMessage method. Another IOleInPlaceObjectWindowless method named GetDropTarget permits the container to get a pointer to the control's IDropTarget interface if the control is an OLE drop target. (Recall from Chapter 19 that registering as an OLE drop target requires a CWnd pointer or a window handle.) To draw a windowless control, the container calls the control's IViewObjectEx::Draw method and passes in a device context for the container window. A windowless control receives no WM_PAINT messages, so it's up to the container to tell the control when to draw.

Clearly, the container shoulders an extra burden when it supports windowless controls, but the control also has to do certain things differently, too. For example, if it wants to repaint itself, a windowless control can't just call Invalidate because it has no window to invalidate. Nor can it get a device context by calling GetDC. So instead it calls IOleInPlaceSiteWindowless::InvalidateRect on its container to invalidate itself, or IOleInPlaceSiteWindowless::GetDC to get a device context. There's more, but you probably get the picture.

Obviously, windowlessness requires extra effort on the part of both the control and the control container. The good news is that COleControl supports both windowed and windowless controls, and it abstracts the differences between them so that you write to one programming model and the control will work either way. Many COleControl functions behave differently in a windowed control than they do in a windowless control. COleControl::InvalidateControl, for example, calls CWnd::Invalidate if the control is windowed or IOleInPlaceSiteWindowless::InvalidateRect if it isn't. COleControl::GetClientRect calls CWnd::GetClientRect for a windowed control, but for a nonwindowed control, it obtains the control rectangle from the control's m_rcPos data member.

Another example demonstrating how COleControl masks the differences between windowed and windowless controls relates to the message map. When a container forwards a message to a windowless control via IOleInPlaceObjectWindowless::OnWindowMessage, COleControl routes the message through the message map. This means that if you register an OnLButtonDown handler in a control's message map, the handler will be called when a windowed control receives a WM_LBUTTONDOWN message or when a windowless control receives a pseudo-WM_LBUTTONDOWN message. In fact, COleControl does such a good job making windowless controls behave just like windowed controls that you almost have to deliberately set out to write COleControl code that won't work in windowless controls. Even so, you'll want to avoid these three common mistakes:

Another common problem is using the rcBounds parameter passed to OnDraw or the rectangle returned by GetClientRect and forgetting that the upper left corner might not be (0,0) for a windowless control. The following code, which computes the center point of the control, is fine in a windowed control, but flawed in a windowless control:

CRect rect; GetClientRect (&rect); int x = rect.Width () / 2; int y = rect.Height () / 2;

Why is this code in error? Because if rect's upper left corner is anywhere other than (0,0), the calculation must take that into account. Here's the corrected code:

CRect rect; GetClientRect (&rect); int x = rect.left + (rect.Width () / 2); int y = rect.top + (rect.Height () / 2);

Windows programmers have been conditioned to treat client rectangles as if the upper left corner coordinates are always (0,0). You must break this habit if you want to write windowless controls that work.

Don't forget that a control created using ControlWizard's default options is strictly a windowed control. To enable the windowless option, check the Windowless Activation box in ControlWizard's Advanced ActiveX Features dialog box. You can convert a windowed control to a windowless control after the fact by overriding GetControlFlags in the derived control class and implementing it like this:

DWORD CMyControl::GetControlFlags () { return COleControl::GetControlFlags () œ windowlessActivate; }

If GetControlFlags is already overridden, simply add windowlessActivate to the list of control flags.

What happens if you build a windowless control and it's instantiated in a container that doesn't support windowless controls? An MFC control will simply fall back and run in a window. Which brings up an interesting point: even though MFC has supported windowless controls for a while now, the test container that ships with Visual C++ didn't support windowless controls prior to version 6.0. Don't make the mistake I once did and attempt to test a windowless control's windowless behavior in the Visual C++ 5.0 test container, because the control will always be created in a window. You can selectively enable and disable windowless support in version 6.0's test container using the Options command in the Container menu.

Control Subclassing

One of the questions that ControlWizard asks you before creating an ActiveX control project is, Which Window Class, If Any, Should This Control Subclass? (See Figure 21-5.) The default is none, but if you select a WNDCLASS name, ControlWizard will create an ActiveX control that wraps a Windows control. This makes it relatively easy to write ActiveX controls that look and act like tree view controls, slider controls, and other built-in control types.

Subclassing a control seems simple, but there's more to it than meets the eye. When a "subclassing" control is created, MFC automatically creates the corresponding Windows control. To do that, MFC must know the control's type—that is, its WNDCLASS. ControlWizard makes the WNDCLASS name you selected in the Which Window Class, If Any, Should This Control Subclass box known to MFC by overriding COleControl::PreCreateWindow and copying the class name to CREATESTRUCT's lpszClass data member. Programmers frequently modify ControlWizard's implementation to apply default styles to the Windows control, as shown here:

BOOL CMyControl::PreCreateWindow (CREATESTRUCT& cs) { cs.lpszClass = _T ("SysTreeView32"); // Tree view control // Apply default control styles here. return COleControl::PreCreateWindow (cs); }

Furthermore, at certain points in an ActiveX control's lifetime, MFC needs to know whether the control has subclassed a Windows control. Consequently, a subclassing control must override COleControl::IsSubclassedControl and return TRUE:

BOOL CMyControl::IsSubclassedControl() { return TRUE; }

ControlWizard writes this function for you if you check the Which Window Class, If Any, Should This Control Subclass box.

When a subclassing control is created, MFC silently creates a "reflector" window that bounces notification messages emitted by the Windows control back to the ActiveX control. The message IDs change en route: WM_COMMAND becomes OCM_COMMAND, WM_NOTIFY becomes OCM_NOTIFY, and so on. To process these messages in your control, you must modify the derived control class's message map to direct OCM messages to the appropriate handling functions. The following code demonstrates how an ActiveX control that subclasses a push button control might turn button clicks into Click events by processing BN_CLICKED notifications:

BEGIN_MESSAGE_MAP (CMyControl, COleControl) ON_MESSAGE (OCM_COMMAND, OnOcmCommand) END_MESSAGE_MAP () . . . LRESULT CMyControl::OnOcmCommand (WPARAM wParam, LPARAM lParam) { WORD wNotifyCode = HIWORD (wParam); if (wNotifyCode == BN_CLICKED) FireClick (); return 0; }

ControlWizard adds an empty OnOcmCommand handler similar to this one to the control class, but if you want to process other types of OCM messages, you must add the handlers and message map entries yourself.

It's also your job to add any methods, properties, and events you would like the control to have. They're added to a subclassing control the same way they're added to a regular control. If you need to access the Windows control from the ActiveX control's method implementations, you can get to it through the m_hWnd data member. To illustrate, the following code fragment adds strings to a list box that has been subclassed by an ActiveX control:

static const CString strMonthsOfTheYear[] = { _T ("January"), _T ("February"), _T ("March"), _T ("April"), _T ("May"), _T ("June"), _T ("July"), _T ("August"), _T ("September"), _T ("October"), _T ("November"), _T ("December") }; for (int i=0; i<12; i++) ::SendMessage (m_hWnd, LB_ADDSTRING, 0, (LPARAM) (LPCTSTR) strMonthsOfTheYear[i]);

Subclassing Windows controls has a dark side; it has to do with painting. ControlWizard writes an OnDraw function that looks like this:

void CMyControl::OnDraw(CDC* pdc, const CRect& rcBounds, const CRect& rcInvalid) { DoSuperclassPaint(pdc, rcBounds); }

DoSuperclassPaint is a COleControl function that paints a subclassed control by simulating a WM_PAINT message. It works fine as long as the ActiveX control is active. But if the control goes inactive, the Windows control it wraps is asked to paint itself into a metafile device context. Some controls don't render well into a metafile, which leaves you to write the code that paints an inactive control by overriding OnDrawMetafile. That can be quite a lot of work. As a rule, subclassing Windows common controls works better than subclassing classic controls—list boxes, combo boxes, and so on—because the common controls' internal painting logic is more metafile-friendly.

You can mostly avoid this problem by checking ControlWizard's Activates When Visible box. If the container honors your request (remember, however, that this isn't guaranteed), the control will be active whenever it's visible on the screen. This means the control's metafile will never be seen. And if it's never seen, it doesn't matter how good (or bad) it looks.

Incidentally, a similar set of issues arises when writing ActiveX controls that create child controls. Rather than open a new can of worms, I'll refer you to an excellent discussion of this matter in the "Converting a VBX and Subclassing Windows Controls" chapter of Adam Denning's ActiveX Controls Inside Out (1997, Microsoft Press).

Control Licensing

Most COM objects in the world today are accompanied by class factories that implement COM's IClassFactory interface. Anyone who has the DLL or EXE (or OCX) that houses such an object can create object instances by calling IClassFactory::CreateInstance or a wrapper function such as ::CoCreateInstance. This is exactly what happens under the hood when a container instantiates an ActiveX control.

If you check the Yes, Please box in response to the question Would You Like The Controls In This Project To Have A Runtime License? in ControlWizard's Step 1 dialog box, ControlWizard inserts a special class factory that implements IClassFactory2 instead of IClassFactory. IClassFactory2 adds licensing methods to IClassFactory and affords the control's implementor veto power over instantiation requests. Exercised properly, this feature can be used to restrict the use of an ActiveX control to individuals (or applications) who are legally authorized to use it.

Here's how licensing works in an MFC ActiveX control. When the licensing option is selected, ControlWizard overrides a pair of virtual functions named VerifyUserLicense and GetLicenseKey in the control's embedded class factory class. Exactly when these functions are called depends on the context in which the control is being used. Here's what happens when you drop a licensed control into a dialog in Visual C++'s dialog editor:

  1. VerifyUserLicense is called. Its job is to verify that the control is licensed for use in a design-time environment. Returning 0 prevents the control from being instantiated; a nonzero return allows instantiation to proceed.
  2. If VerifyUserLicense returns a nonzero value, GetLicenseKey is called. Its job is to return a licensing string that Visual C++ can embed in the compiled executable. This string becomes the control's run-time license.

Now suppose you compile and run the application. When the application is executed and the container attempts to instantiate the ActiveX control, a different series of events ensues:

  1. The MFC-provided class factory's VerifyLicenseKey function is called and passed the licensing string embedded in the control container. Its job: to check the string and determine whether it represents a valid run-time license.
  2. VerifyLicenseKey calls GetLicenseKey to retrieve the run-time licensing string from the control and compares that string to the one supplied by the container. If the strings don't match, VerifyLicenseKey returns 0 and the class factory will refuse to create the control. If the strings match, instantiation proceeds as normal.

VerifyLicenseKey is a third virtual function that plays a role in licensing. ControlWizard doesn't override it because the default implementation compares the strings and does the right thing, but you can override it manually if you want to implement a custom run-time verification algorithm.

These semantics might vary somewhat from container to container, but the result is the same. In effect, the control supports two separate licenses: a design-time license and a run-time license. At first, this dichotomy might seem odd, but it makes terrific sense when you think about it. If a developer uses your control in an application, you can make sure that he or she has the right to do so. But once the application is built, you probably don't want to force individual users to be licensed, too. Design-time licenses and run-time licenses are treated separately for precisely this reason.

The licensing scheme implemented by ControlWizard is exceedingly weak, so if you really want to build a licensed control, you must do some work yourself. Here's how ControlWizard implements VerifyUserLicense and GetLicenseKey:

static const TCHAR BASED_CODE _szLicFileName[] = _T("License.lic"); static const WCHAR BASED_CODE _szLicString[] = L"Copyright (c) 1999 "; BOOL CMyControl::CMyClassFactory::VerifyUserLicense() { return AfxVerifyLicFile(AfxGetInstanceHandle(), _szLicFileName, _szLicString); } BOOL CMyControl::CMyClassFactory::GetLicenseKey(DWORD dwReserved, BSTR FAR* pbstrKey) { if (pbstrKey == NULL) return FALSE; *pbstrKey = SysAllocString(_szLicString); return (*pbstrKey != NULL); }

AfxVerifyLicFile is an MFC helper function that scans the first n characters in the file whose name is specified in the second parameter for the string passed in the third, where n is the string length. Visual C++ places a text file named License.lic in the same folder as the control's OCX. Inside that file is the text string "Copyright (c) 1999." If the file is present and it begins with the expected string, AfxVerifyLicFile returns a nonzero value, allowing the control to be instantiated. Otherwise, it returns 0. In other words, the control's design-time license amounts to a simple text file that anyone could create with Notepad.

There are endless ways to modify ControlWizard's code to strengthen the licensing policy. You could, for example, have VerifyUserLicense display a dialog box prompting the developer to enter a password. Or you could have it check the registry for an entry created by the control's setup program. However you choose to do it, keep in mind that determined users will be able to circumvent just about any scheme you come up with, and many will steer clear of copy-protected products, period, if at all possible. For these reasons, most ActiveX control writers have opted not to license their controls. You be the judge.

Категории