April 1999
Write ActiveX Controls Using Custom Interfaces Provided by ATL 3.0, Part III |
Code for this article: Container.exe (106KB)
This third and final installment on ActiveX controls examines the steps necessary to host these controls using ATL 3.0. You'll learn how to host controls both in standalone applications as well as from within COM servers. |
This article assumes you're familiar with C++, ATL, and COM |
This article is adapted from the forthcoming book, ATL Internals, by Brent Rector and Chris Sells (Addison-Wesley, 1999). Brent is founder of Wise Owl Consulting Inc., a COM consulting firm. Reach Brent at http://www.wiseowl.com/brent.htm. Chris is an independent consultant specializing in designing and building distributed systems using COM. Chris can be reached at http://www.sellsbrothers.com.
|
Full coverage of the interaction between controls and containers is beyond the scope of this article. For more information, refer to the Platform SDK. We will go over what you need to know to host controls both in standalone applications and inside of COM servers. Before diving into the details of dialogs or controls hosting other controls, let's start with the basics: control containment in a simple frame window. COM control containers can take many forms. A window can contain any number of COM controls, as can a dialog or another control (called a composite control). To contain a COM control, a container must provide a window to act as the parent for the child COM control and implement a set of COM interfaces for communication between the control and the container. |
Figure 1 Integrating Containers and Controls |
The window provided by the container may be a parent window of the control or, in the case of a windowless control, it may be shared by the control. The control will use the window in its interaction with the user. The interfaces implemented by the container are used for integration with the control and mirror those implemented by the control (see Figure 1).
Control Creation Internals
The control creation process in ATL exposes the core of how ATL hosts controls. Figure 2 shows the overall process. Let's take a detailed look at the relevant bits of code involved.
Figure 2 Control Creation Process |
ATL's implementation of the required container interfaces is called CAxHostWindow:
|
As you can see, a CAxHostWindow is two things: a window (from CWindowImpl) and a COM implementation (from CComObjectRootEx). When the container wants to host a control, it will create an instance of CAxHostWindow, but not directly. Instead, it will create an instance of a window class defined by ATL called AtlAxWin. This window will act as the parent window for the control and will eventually be subclassed by an instance of CAxHostWindow. Before an instance of this window class can be created, the window class must first be registered. ATL provides a function to register the AtlAxWin window class called AtlAxWinInit (see Figure 3).
Once the AtlAxWin class has been registered, creating a window based on this class will also create an instance of CAxHostWindow. The CAxHostWindow object will then use the title of the window as the name of the control to create and to host. For example, the following code will create a CAxHostWindow and cause it to host a new instance of the BullsEye control developed in previous issues of MSJ: |
|
Creation of the CAxHostWindow object and the corresponding control is initiated in the WM_CREATE handler of the AtlAxWin WndProc, AtlAxWindowProc (see Figure 4). Notice that the window's text, as passed to the call to CWindow::Create, is used as the name of the control to create. The call to AtlAxCreateControl passes the name of the control forward to AtlAxCreateControlEx, which furthers things along by creating a CAxHostWindow object and asking it to create and host the control: |
|
AtlAxCreateControlEx uses the IAxWinHostWindow interface to create the control. IAxWinHostWindow is one of the few interfaces that ATL defines, and one of the interfaces that CAxHostWindow implements. Its job is to allow for management of the control that it's hosting: |
|
To create a new control in the CAxHostWindow, IAxWinHostWindow provides CreateControl or CreateControlEx, which is what AtlAxCreateControlEx uses after the CAxHostWindow object is created.
The first parameter of CreateControl[Ex] is lpTricsData, the name of the control to create. It can take the form of a CLSID, a ProgID, a URL, a file name, or raw HTML. We'll discuss more about this later. hWnd holds the parent window to host control. This window will be sub-classed by CAxHostWindow. pStream is the stream that holds object initialization data. The control will be initialized via IPersistStreamInit. If pStream is non-NULL, Load will be called. Otherwise, InitNew will be called. ppUnk will be filled with an interface to the newly created control. If riidAdvise is not IID_NULL, CAxHostWindow will attempt to set up a connection point connection between the control and the sink object represented by the punkAdvise parameter. CAxHostWindow will manage the resultant cookie and tear down the connection when the control is destroyed. punkAdvise is an interface to the sink object that implements the sink interface specified by riidAdvise. The AttachControl method of IAxWinHostWindow attaches a control that has already been created and initialized to an existing CAxHostWindow object. The QueryControl method allows access to the control's interfaces hosted by the CAxHostWindow object. Both SetExternalDispatch and SetExternalUIHandler are for use when hosting the Microsoft® Internet Explorer HTML Control, and they're beyond the scope of this article. CAxHostWindow's implementation of CreateControlEx subclasses the parent window for the new control, creates a new control, and then activates it. If an initial connection point is requested, AtlAdvise is used to establish that connection. If the newly created control is to be initialized from raw HTML or navigated to via a URL, CreateControlEx does that, too (see Figure 5). How the control name is interpreted depends on another function called CreateNormalized- Object. The activation is handled by the ActivateAx function. The CreateNormalizedObject will create an instance of a COM object using strings of the form shown in Figure 6. Since CAxHostWindow uses the title of the window to obtain the name passed to CreateNormalizedObject, you can use any of these string formats when creating an instance of the AtlWinInit window class. The ActivateAx function really performs the magic of the control creation process. It takes an interface pointer from the object that CreateNormalizedObject creates and activates it as a COM control in the parent window. ActivateAx is responsible for the following:
This completes the activation of the control. However, creating an instance of the CAxHostWindow via direct reference to the AtlAxWin window class is not typical. The implementation details of AtlAxWin and CAxHostWindow are meant to be hidden from the average ATL programmer. The usual way a control is hosted under ATL is via an instance of a wrapper class called CAxWindow. CAxWindow simplifies the use of CAxHostWindow with a set of wrapper functions. The initial creation part of CAxWindow class is defined like so: |
|
Notice that both Create functions still require the parent window and the name of the control, but they do not require passing the name of the CAxHostWindow window class. Instead, CAxWindow knows the name of the appropriate class itself (available via the static member function GetWndClassName) and passes it to the CWindow base class. Using CAxWindow reduces the code required to host a control to the following: |
|
Two-step Control Creation You may have noticed that AtlAxCreateControlEx takes some interesting parameters, like an IStream interface pointer and an interface ID/interface pointer pair to specify an initial connection point. However, while the window name can be used to pass the name of the control, there are no extra parameters to CreateWindow except for a couple of interface pointers and a GUID. Instead, CAxWindow provides a few extra wrapper functions, CreateControl and CreateControlEx (see Figure 7). CreateControl and CreateControlEx allow for the extra parameters that AtlAxCreateControlEx supports. The extra parameter that the CAxWindow wrappers support beyond those passed to AtlAxCreateControlEx is dwResID, which serves as an ID of an HTML page embedded in the resources of the module. This parameter will be formatted into a string of the format "res://<module path>/<dwResID>" before being passed to AtlAxCreateControlEx. These functions seem to be designed for use in a two-stage construction of first the host and then its control: |
|
We'll show you how to persist a control and how to handle events from a control later on. If you've already created a control and initialized it, you can still use the hosting functionality of ATL by attaching the existing control to a host window via the AttachControl function: |
|
AttachControl is meant to be used like so: |
|
As attractive as the CreateControl, CreateControlEx, and AttachControl member functions are, you should avoid using them like this as of ATL 3.0. They leak. Phase one creates an instance of CAxHostWindow, even if the window text is empty. All three of the second-phase control creator functions also create an instance of CAxHostWindow and never destroy the first one. You're leaking the size of a CAxHostWindow object (224 bytes) every time you call one of these three functions. Luckily, there's a workaround. Using the QueryHost member function of CAxWindow, you can obtain the IAxWinHostWindow interface on the existing CAxHostWindow object: |
|
|
QueryHost uses the AtlAxGetHost function to send a custom window message to the AtlAxWin window to obtain an interface pointer on the host. Once you have the IAxWinHostWindow interface, you can call the interface member functions CreateControl, CreateControlEx, or AttachControl without worry of leaking because now you're reusing the existing CAxHostWindow instead of creating a new one. |
|
Because it's kind of a pain to do this all manually, the source code for this article, which you can find here, contains an alternative to CAxWindow called CAxWindow2. CAxWindow2 derives from CAxWindow and provides implementations of CreateControl, CreateControlEx, and AttachControl that reuse the existing CAxHostWindow object instead of creating a new one. The CAxWindow2 class allows you to use the CAxWindow member functions CreateControl, CreateControlEx, and AttachControl as we've shown previously, but without the leak. Using the Control Once you've created the control, it's really two things: a window and a control. The window is an instance of AtlAxWin and hosts the control, which may or may not have its own window. (CAxHostWindow provides full support for windowless controls.) Since CAxWindow derives from CWindow, you can treat it like a window (move it, resize it, or hide it), and AtlAxWin will handle those messages by translating them into the appropriate COM calls on the control. For example, if you'd like the entire client area of a frame window to contain a control, you can handle the WM_SIZE message like so: |
|
Unlike Windows® controls, COM controls do not accept functionality requests via Windows messages (remember, a COM control may not even have a window). Instead, because COM controls are COM objects, they expect to be programmed via their COM interfaces. If you want to obtain an interface on the control, CAxWindow provides the QueryControl method: |
|
Like QueryHost, QueryControl uses a global function (AtlAxGetControl in this case) that sends a window message to the AtlAxWin window to retrieve an interface, this time from the hosted control itself. Once the control has been created, QueryControl can be used to get at the interfaces of the control: |
|
Notice the use of the #import statement to pull in the definitions of the interfaces of the control you're programming against. This is necessary if you've only got the control's server DLL and the bundled type library, but no original IDL (a common occurrence when programming against controls). Notice also the use of the #import statement attributes, such as raw_interfaces_only. These attributes are used to mimic as closely as possible the C++ language mapping you would have gotten had you used midl.exe on the server's IDL file. Without these attributes, Visual C++ will create a language mapping that uses the compiler-provided wrapper classes, such as _bstr_t, _variant_t, and _com_ptr_t, which are different than the ATL-provided types, such as CComBSTR, CComVariant, and CComPtr. While the compiler-provided classes have their place, we find it's best not to mix them with the ATL-provided types if possible. Apparently the ATL team agrees with me, as the ATL object Wizard-generated #import statements also use these attributes. (I'll talk more about the control-containment-related wizards later.) Sinking Control Events Not only are you likely going to want to program against the interfaces that the control implements, you're likely going to want to handle events fired by the control. Most controls have an event interface which, for maximum compatibility with the largest number of clients, is often a dispinterface. For example, the BullsEye control defines the following event interface: |
|
For a control container to handle events fired on a dispinterface would require an implementation of IDispatch. Implementations of IDispatch are easy if the interface is defined as a dual, but much harder if defined as a raw dispinterface. ATL provides a helper class for implementing an event dispinterface called IDispEventImpl: |
|
IDispEventImpl uses a data structure called a sink map established via the following macros: |
|
The gory details of these are beyond the scope of this article, but the sink maps provide a mapping between a specific object/iid/dispid that defines an event and a member function to handle that event. If the object is a nonvisual one, the sink map can be a bit involved. If the object is a COM control, usage of IDispEventImpl and the sink map are quite simple, as you're about to see.
To handle events, the container of the controls will derive from one instance of IDispEventImpl per control. Notice that the first template parameter of IDispEventImpl is an ID. This ID is going to match to the contained control via the child window ID, the nID parameter to Create. This same ID is going to be used in the sink map to route events from a specific control to the appropriate event handler. The child window ID is what makes the IDispEventImpl so simple in the control case. Handling the events of the BullsEye control merely requires an IDispEventImpl base class and an appropriately constructed sink map (see Figure 8). Notice that the child window control ID (ID_ BULLSEYE) is used in four places. The first is the IDispEventImpl base class. The second is the call to Create, marking the control as the same one that will be sourcing events. The last two uses of ID_BULLSEYE are the entries in the sink map, which route events from the ID_BULLSEYE control to their appropriate handlers. Notice also that the event handlers are marked __stdcall. Remember that we're using IDispEventImpl to implement IDispatch for a specific event interface (as defined by the DIID_IBullsEyeEvents interface identifier). That means IDispEventImpl must unpack the array of VARIANTs passed to Invoke, push them on the stack, and call our event handler. It does this using type information at runtime, but it still has to know about the calling convention: in what order should the parameters be passed on the stack and who's responsible for cleaning them up. To alleviate any confusion, IDispEventImpl requires that all event handlers have the same calling convention, which __stdcall defines. Once we've got IDispEventImpl and the sink map set up, we're still not finished. Unlike Windows controls, COM controls have no real sense of their parent. This means that instead of implicitly knowing to whom to send events, like an edit control does, a COM control must be told who wants the events. Because events are established between controls and containers with the connection point protocol, somebody's got to call QueryInterface for IConnectionPointContainer, FindConnectionPoint to obtain the IConnectionPoint interface, and finally Advise to establish the container as the sink for events fired by the control. That's not so much work for one control, and ATL even provides a function called AtlAdvise to help. For multiple controls, managing the communication with each of them can become a chore. And since we've got a list of all the controls with which we'd like to establish communications in the sink map, it makes sense to use that knowledge to automate the chore. Luckily, we don't even have to do this much, because ATL's already done it for us with AtlAdviseSinkMap: |
|
The first argument to AtlAdviseSinkMap is a pointer to the object that wants to set up the connection points with the objects listed in the sink map. The second parameter is a Boolean determining if we are setting up or tearing down communication. Because AtlAdviseSinkMap depends on the child window ID to map to a window that already contains a control, setting up and tearing down connection points must occur when the child windows are still living and contain controls. Handlers for the WM_CREATE and WM_DESTROY messages are excellent for this purpose: |
|
IDispEventImpl, the sink map, and the AtlAdviseSinkMap function are all that you need to sink events from a COM control. But things can be simplified even further. Most controls implement only a single event interface and publish this fact in one of two places. The default source interface can be provided by an implementation of IProvideClassInfo2 and it can be published in the coclass statement in the IDL (and, therefore, as part of the type library): |
|
In the event that IDispEventImpl is used with IID_NULL as the template parameter (which is the default value) describing the interface to be sinked, ATL will do its best to establish communications with the default source interface via a function called AtlGetObjectSourceInterface. This function will attempt to obtain the object's default source interface, using the type information obtained via the GetTypeInfo member function of IDispatch. It first attempts to use IProvideClassInfo2, and if that's not available, it will dig through the coclass looking for the [default, source] interface. The upshot is that if you want to source the default interface of a control, the parameters to IDispEventImpl are fewer and you can use the simpler SINK_ENTRY: |
|
Property Changes In addition to a custom event interface, controls will often source events on the IPropertyNotifySink interface: |
|
IPropertyNotifySink is used by the control to ask the container if it's OK to change a property (OnRequestEdit) and to notify the container that a property has been changed (OnChanged). OnRequestEdit is used for data binding. OnChanged can be a handy notification, especially if the container expects to persist the control and wants to use OnChanged as an is-dirty notification. Even though IPropertyNotifySink is a connection point interface, it's not a dispinterface, so neither IDispEventImpl nor a sink map are requirednormal C++ inheritance and AtlAdvise will do: |
|
The simplest way to establish the IPropertyNotifySink connection is to use the CAxWindow member function CreateControlEx, which allows for a single connection point interface to be established and the cookie to be managed by the CAxHostWindow object, as shown here: |
|
The connection point cookie for IPropertyNotifySink will be managed by the CAxHostWindow object. When the control is destroyed, the connection will be torn down automatically. While this trick only works for one connection point interface, this and the sink map are likely all you'll ever need when handling events from controls. In addition to programming the properties of the control, you may wish to program the properties of the control's environment, known as ambient properties. For this purpose, CAxHostWindow implements the IAxWinAmbientDispatch interface (see Figure 9). QueryHost can be used on a CAxWindow to obtain the IWinAmbientDispatch interface so that these ambient properties can be changed. |
|
Whenever an ambient property is changed, the control is notified via its implementation of the IOleControl member function OnAmbientPropertyChange. The control can then QueryInterface any of its container interfaces for IDispatch to obtain the interface for retrieving the ambient properties (which is why IWinAmbientDispatch is a dual interface). Persisting a Control and Accelerator Translations It may be that the control's state is something that you'd like to persist between application sessions. This can be done with any number of persistence interfaces, of which most controls implement IPersistStreamInit (although IPersistStream is a common fallback). For example, saving a control to a file can be done with a stream in a structured storage document (see Figure 10). Restoring a control from a file is somewhat easier because both the CreateControl and the CreateControlEx member functions of CAxWindow take an IStream interface pointer to use for persistence (see Figure 11). If a NULL IStream interface pointer is provided to either CreateControl or CreateControlEx, ATL will attempt to call the IPersistStreamInit member function InitNew to make sure that either InitNew or Load is called as appropriate. It's common for contained controls to contain other controls. For keyboard accelerators such as the Tab key to provide for navigation between controls, the main message loop must be augmented with a call to each window hosting a control to allow it to pretranslate the message as a possible accelerator. This functionality must ask the host of the control with focus if it wants to handle the message. If the control does handle the message, no more handling need be done on that message. Otherwise, the message processing can proceed as normal. A typical implementation of a function to attempt to route messages from the container window to the control itself (whether it's a windowed or a windowless control) is shown here: |
|
The crux of this function forwards the message to the AtlAxWin via the WM_FORWARDMSG message. This message is interpreted by the host window as an attempt to let the control handle the message, if it so desires. This message will be forwarded to the control via a call to the IOleInPlaceActiveObject member function TranslateAccelerator. The PreTranslateAccelerator function should be called from the application's main message pump like so: |
|
The use of a PreTranslateAccelerator function on every window that contains a control will give the keyboard navigation keys a much greater chance of working, although the individual controls have to cooperate, too. Control Initialization So far, we've discussed the basics of control containment using a frame window as a control container. An even more common place to contain controls is the ever-popular dialog. For quite a while, the Visual C++ resource editor has allowed a control to be inserted into a dialog resource by right-clicking on a dialog resource and choosing Insert ActiveX Control. As of Visual C++ 6.0, ATL supports creating dialogs that host the controls inserted into a dialog resource. The Insert ActiveX Control dialog is shown in Figure 12. |
Figure 12 Visual C++ 6.0 Resource Editor |
Our container sample, which you can find at the link at the top of this article, has a simple dialog box with a BullsEye control, along with a couple of static controls and a button. This is what that dialog resource looks like in the .rc file:
|
Notice that the window text part of the CONTROL resource is a CLSID, specifically the CLSID of the BullsEye control. This window text will be passed to an instance of the AtlAxWin window class to determine the type of the control to create. In addition, another part of the .rc file maintains a separate resource called a DLGINIT resource, which is identified with the same ID as the BullsEye control on the dialog, IDC_ BULLSEYE. This resource contains the persistence information, converted to text format, that will be handed to the BullsEye control at creation time (via IPersistStreamInit): |
|
As most folks prefer not to enter this information directly, right-clicking on a COM control and choosing Properties will show the control's property pages, along with the custom property pages of the resource editor. Figure 13 shows the BullsEye properties dialog. |
Figure 13 BullsEye Properties |
The DLGINIT resource for each control is constructed by asking each control for IPersistStreamInit, calling Save, converting the result to a text format, and dumping it into the .rc file. In this way, all information set at design time will be automatically restored at runtime.
Sinking Control Events in a Dialog
Remember that sinking control events requires adding one IDispEventImpl per control to the list of base classes of your dialog class and populating the sink map. While this has to be done by hand if a window is the container, it can be performed automatically if a dialog is to be the container. By right-clicking on the control and choosing Events, you can choose the events to handle, and the IDispEventImpl and sink map entries will be added for you. Figure 14 shows the Event Handlers dialog.
Figure 14 Handling BullsEye Events |
While the wizard will add the IDispEventImpl classes and manage the sink map, it will not insert code to call AtlAdviseSinkMap, either in WM_INITDIALOG to establish connection points with the controls or in WM_DESTROY to tear down the connection points. You have to remember to do this yourself.
Since the built-in dialog box manager window class has no idea how to host controls, ATL has to perform some magic on the dialog resource. It must preprocess the dialog box resource looking for CONTROL entries and replacing them with entries that will create an instance of an AtlAxWin window. Once this is done, the AtlAxWin uses the name of the window to create the control and the DLGINIT data to initialize it, providing all the control-hosting functionality we've spent most of this article dissecting. To hook up this preprocessing step when hosting controls in dialogs, use the CAxDialogImpl base class (see Figure 15). Notice that the DoModal and Create wrapper functions call AtlAxDialogBox and AtlAxCreateDialog instead of DialogBoxParam and CreateDialogParam, respectively. These functions perform the preprocessing necessary to add swap instances of AtlAxWin for each CONTROL entry in the dialog resource.
Using CAxDialogImpl as the base class, we can have a dialog that hosts COM controls as shown in Figure 16. Notice that, just like a normal dialog, the message map handles messages for the dialog itself (like WM_INITDIALOG and WM_DESTROY) as well provides a mapping between the class and the dialog resource id (via the IDD symbol). The only thing new is that, because we've used CAxDialogImpl as the base class, the COM controls will be created as the dialog is created.
Attaching a CAxWindow
During the life of the dialog, you will likely need to program against the interfaces of the contained COM controls, which means you'll need some way to obtain an interface on a specific control. One way to do this is with an instance of CAxWindow. Since ATL has created an instance of the AtlAxWin window class for each of the COM controls on the dialog, you use the Attach member function of a CAxWindow to attach to a COM control, and thereafter use the CAxWindow object to manipulate the host window. Once you've attached a CAxWindow object to an AtlAxWin window, you can use the member functions of CAxWindow to communicate with the control host window. Remember that you use the QueryControl member function to obtain an interface from a control:
|
In this example, we've cached both the HWND to the AtlAxWin for continued communication with the control host window and one of the control's interfaces for communication with the control itself. If you don't need the HWND, but only an interface, you may want to consider using GetDlgControl instead.
The CDialogImpl class, because it derives from CWindow, provides the GetDlgItem function to retrieve the HWND of a child window given the ID of the child. CWindow also provides a GetDlgControl member function, but it's for retrieving an interface pointer instead of an HWND: |
|
The GetDlgControl member function calls the AtlAxGetControl function, which uses the HWND of the child window to retrieve an IUnknown interface. AtlAxGetControl does this by sending the WM_GETCONTROL window message that windows of the class AtlAxWin understand. In the event that the child window is not an instance of the AtlAxWin window class, or if the control does not support the interface being requested, GetDlgControl will return a failed HRESULT. Using GetDlgControl simplifies the code to cache an interface on a control considerably: |
|
The combination of the CAxDialogImpl class, the control containment wizards in Visual C++, and the GetDlgControl member function makes managing COM controls in a dialog much like managing Windows controls. Composite Controls There's beauty in using a dialog resource for managing the UI of a window. Instead of writing pages of code to create, initialize, and place controls on a rectangle of gray, you can use the resource editor to do it for you. At design time you lay out the size and location of the elements of the UI; the ATL-augmented dialog manager is responsible for the heavy lifting. This is an extremely useful mode of UI developmentand it can also be used for composite controls. A composite control is a COM control that uses a dialog resource to lay out its UI elements. These UI elements can be Windows controls or other COM controls. To the Windows controls, a composite control appears as a parent window. To a COM control, the composite control appears as a control container. To a control container, the composite control appears as a control itself. To the developer of the control, a composite control is all three. ATL provides support for composite controls via the CComCompositeControl base class: |
|
Notice that CComCompositeControl derives both from CComControl and CAxDialogImpl, combining the functionality of a control and the drawing of the dialog manager, augmented with the COM control hosting capabilities of AtlAxWin. Both of the wizard-generated composite control types (Composite Control and Lite Composite Control) derive from CComCompositeControl instead of CComControl and provide an IDD symbol mapping to the control's dialog resource: |
|
Notice that the construction of the composite control sets the m_bWindowOnly flag, disabling windowless operation. The control's window needs to be of the same class as that managed by the dialog manager. Also notice that the m_sizeExtent member variable is set by a call to CalcExtent, a helper function provided in CComCompositeControl. CalcExtent is used to set the initial preferred size of the control to be exactly that of the dialog box resource. Composite Control Drawing Since a composite control is based on a dialog resource, and its drawing will be managed by the dialog manager and the child controls, no real drawing chores have to be performed. Instead, setting the state of the child controls, which will cause them to redraw, is all that's required to update the visual state of a control. Using a dialog resource and deriving from CComCompositeControl are the only differences between a control that manages its own UI elements and one that leans on the dialog manager. If you'd like even more powerful layout management, there's a special kind of composite control that hosts only one control, the Internet Explorer HTML Control, which can be generated with the HTML Control and Lite HTML Control object types in the ATL Object Wizard. This control, instead of being based on a dialog resource, is based on an HTML resource. Summary ATL provides the ability to host controls in windows, dialogs, or other controls. Control containment under ATL is based on a new window class, AtlAxWin. As a wrapper around this window class, ATL provides the CAxWindow class. Once a control hosting window has been created, it can be treated as a window, using the functionality of the CWindow base class. It can also be used as a COM object, using the interfaces available with the QueryControl member function of the CAxWindow class. The interfaces of the control can be used to sink events, persist the control's state, or program against the control's custom interfaces.
Also see
|
From the April 1999 issue of Microsoft Systems Journal
|