| sean's happy programming place | The Journals of Captain
COM: |
Sean Baxter |
| Control Functionality |
With the core interfaces IOleObject, IViewObject2, and IPersistStreamInit (plus the interface IPersistPropertyBag for web happiness) out of the way, we can finally get to Message Display's primary interface and some interesting control-related functionality. Here's the complete IDL for our control's primary interface.
| from captcom.idl import "ocidl.idl"; |
The first interesting thing here is the inclusion of the "olectl.h" header. Defined within are dozens of standard DISPIDs relevant to OLE Controls. The IMessageDisplay interface defines methods over the DISPID_FILLCOLOR, DISPID_BORDERCOLOR, DISPID_BORDERWIDTH, DISPID_FORECOLOR, DISPID_FILLSTYLE, DISPID_TEXT, DISPID_FONT, and DISPID_ABOUTBOX standard DISPIDs. Using the standard DISPIDs is in no way mandatory, but certain Automation controllers may better support properties and methods defined over them.
The Font property takes an IFontDisp interface pointer as a parameter. This dispinterface exposes essentially the same functionality as the easier-to-use vtable interface IFont. Both interfaces are implemented by the standard and extremely popular OLE font object, so you can almost always successfully query an IFontDisp for an IFont.
IMessageDisplay defines only one method (actually they're all methods, but VB programmers and others acting in a high-level delusion would claim the other methods to be "properties"): AboutBox. When called, this method simply DialogBoxs the appropriate resource template to present a modal About Box dialog to the user. This method will also be called when the About Box verb is invoked, as we'll see later on.
![]() |
Figure 1: The About Box |
The mutator functions of Message Display's primary interface perform bounds checking on the parameter data. If the parameter is out of the range (for example, isn't a valid enumeration, or a negative border width) the function returns E_FAIL. Alternatively the function could have set the control's data members to something "close" to what was requested (a negative value might be rounded up to zero). In addition to the primary interface, IProvideClassInfo is implemented by Message Display. This allows the client to grab the control's type information to learn the primary interface's definition (the client can also retrieve the type information through IDispatch::GetTypeInfo).
| from mdisplay.cpp class CMessageDisplay : public
IMessageDisplay, public IProvideClassInfo, ... { |
Notice that AboutBox attempts to retrieve the container's HWND before invoking DialogBox. It does this by acquiring the container interface from the site (the container contains sites, and each site interracts with a control) and querying it for IOleWindow. This interface plays a key role in inplace activation, as it serves as a base for a number of other interfaces.
Each of the mutators above (i.e. the put_ methods) adheres to the same strategy: check to make sure the parameter is valid; save the parameter to the data member; set the dirty bit (so that the change will be serialized); and redraw the control with SendViewChange.
![]() |
Figure 2: The Message Display Property Page |
The property page of Figure 2 is provided by our control, but is in fact, a complete COM object on its own. Our control declares that it wants the Message Display "General" property page, the stock font property page (shown in Figure 3), and the stock color property page (of Figure 4) shown in the property page frame. Introduced with Windows 95 were three stock property pages which prove very useful (actually, these three pages existed prior to Windows 95, but in extremely ugly incarnations). The stock picture property page of Figure 5, in addition to the two already mentioned, is handed an IUnknown pointer through its IPropertyPage::SetObjects method (implemeted below). From this interface is queried IDispatch, and GetTypeInfo is called to retrieve the primary interface's type information (alternatively, IProvideClassInfo::GetClassInfo could be used; the actual implementation of the stock property pages is hidden from the programmer). The stock picture property page iterates through the method declarations and searches for IPicture* and IPictureDisp* parameters. It lists the hits in the box labeled "properties" and provides the user with a mechanism for choosing pictures to send to the control. The stock font property page works the same way, but it searches for IDispFont* or IFont* parameters, and the stock color property page seeks OLE_COLOR arguments.
![]() |
![]() |
Figure 3: The Stock Font Property Page |
Figure 4: The Stock Color Property Page |
![]() |
Figure 5: The Stock Picture Property Page |
The source below defines our property page COM object. The property page implements the single interface IPropertyPage and is instantiated by the property frame using the module's DllGetClassObject export function. Message Display's property page doesn't support aggregation, so it is instantiated using the CClassFactoryNoAgg factory. This class is defined in "helper.h," but omitted in the source below for brevity. The control implements ISpecifyPropertyPages's single method GetPages, which returns the class identifiers of the three property pages requested. The CLSIDs of the stock pages can be found in the header "msstkppg.h."
| from mdisplay.cpp #include <msstkppg.h> from exports.cpp //
{BD55A7EE-F883-4a51-B771-21F049C0B326} from mdprop.cpp #include <windows.h> |
The first method invoked on our property page by its frame is likely GetPageInfo. The method's parameter, a PROPPAGEINFO structure, is filled with information about the page, including its size in dialog units and the title that will appear on the page's tab. MSDN Library comments on the size of property page: "If a control does implement property pages, then those pages should conform to one of the standard sizes: 250x62 or 250x110 dialog units (DLUs)." While this may have been true at one point in time, it's certainly false today. Our property page is 259x141 units, and fits in the standard frame just fine. However, a page can't get much bigger than that. I suggest that you size your page to whatever works in the standard frame. If you have a lot of controls to display, there's no problem in supplying several custom property pages for your control.
Prior to requesting page activation, the property frame invokes IPropertyPage::SetObjects, and passes an array of IUnknown pointers. Our page queries each pointer for our control's primary interface, and returns E_NOINTERFACE on failure. The function caches the IMessageDisplay pointers in a vector before returning. IPropertyPage::SetPageSite is also invoked early on. This method is the propery page equivalent of IOleObject::SetClientSite. Activate is the function called next. Here the property page window is created. Two flags of interest are cleared here: the dirty flag represents the state of the apply button; the _canGoDirty flag prevents the apply button from being enabled before the user has a chance to make any changes. On WM_INITDIALOG, which executes entirely within the scope of IPropertyPage::Activate, controls are set to reflect the state of the control in question. Each of these control changes sends a WM_COMMAND message, and the procedure interprets the message as a changed state and attempts to set the dirty bit and enable the apply button. It's prevented, however, because _canGoDirty is cleared. Only after WM_INITDIALOG completes it course and the CreateDialog returns can the apply button be enabled. The window procedure attempts to enable the apply button by calling the class method Dirty, which delegates to the page site's OnStatusChange method.
The host is given complete control over hiding and displaying the page (in reaction to selecting a tab in the frame) by accessing the IPropertyPage::Show method. Implementation of this method is easy: we just call the familiar API ShowWindow. Move is just as simple, delegating to MoveWindow. When the frame's apply button is clicked, and our property page is currently selected, Apply is invoked. This method simply iterates through each control pointer and sets their states to match that of the property page. Afterwards, the dirty flag is cleared.
The WM_STYLECHANGING case in our dialog's procedure warrants comment. Naturally, we'd like to be able to tab through the controls on our property page as well as the controls on the form. This requires that we set the WS_EX_CONTROLPARENT style on our dialog. For some silly reason, the standard property frame will turn off this very property, and the tab key will cease to visit our controls! On WM_STYLECHANGING we prevent the frame from mucking with us this way; the WS_EX_CONTROLPARENT style is turned back on. Another keyboard-related issue is addressed with the IPropertyPage::TranslateAccelerator method, which calls IsDialogMessage. See the MSDN Library docs on this API for more details.
![]() |
Figure 6: Verbs Displayed in a Popup Menu |
This installment of The Journals of Captain COM is nearly complete, but we still haven't completed the first OLE Control interface we encountered: IOleObject! Our control supports two verbs, both are listed in the container's popup menu, as shown in Figure 6. In order for the client to retrieve the names of our verbs, we need to provide a verb enumerator, which is in fact, its own COM object (sans class factory). ATL does not provide a verb enumerator. Instead, it delegates EnumVerbs to OleRegEnumVerbs. Therefore, if you are using ATL and want to let the client know of the verbs you support, you must add the verbs to the registry under your CLSID key. This is a task that many ATL programmers fail to complete, and their controls suffer because of it.
Below is the definition of our verb enumerator. It implements the single interface IEnumOLEVERB. See the MSDN Library for details on this interface.
| from helpers.h class CEnumOLEVERB : public
IEnumOLEVERB { from mdisplay.cpp class CMessageDisplay : public IMessageDisplay, public IOleObject, ...
{ |
The two element OLEVERB array contains flags for our verbs. OLEVERBATTRIB_ONCONTAINERMENU is self-explanatory. OLEVERBATTRIB_NEVERDIRTIES, which is set for the About Box verb, indicates that executing the verb will not alter the state of the control and result in a need to save. The DoVerb method switches over the verb identifier. On OLEIVERB_PROPERTIES, a standard verb identifier (from "olectl.h"), we query the client site for IOleControlSite and invoke ShowPropertyFrame on the resulting pointer. This method tells the site to create a property frame (COM provides a property frame. See OleCreatePropertyFrame in MSDN Library) and invoke ISpecifyPropertyPages::GetPages to retrieve the list of property pages to instantiate.
Well, that wraps up our discussion of simple OLE Controls. As you can tell, there's no magic involved here, just lots of code. ActiveX isn't some fabulous new communication software recovered from alien spacecraft... it's just Microsoft's latest attempt to re-market COM. So this I say: go forth and program controls. But do so with skepticism of magic mechanisms. By cutting through the hype and getting down to the nitty-gritty low level code, we can really master this misunderstood technology.
| - Captain COM |