Figure 4 The ATL Class Tree
While the "OLE Controls and Container Guidelines" is silent about the required implementation of site objects, ATL does provide a windowed site object implementation. In general, a site object does not have its own window, instead sharing the window of the container (which is commonly called a form). The OC96 specification, on the other hand, urges control writers to create windowless controls to eliminate the overhead of the CreateWindow call, which can make windowed control activation up to two orders of magnitude slower than windowless control activation. Unfortunately, by giving each site its own window, ATL puts the CreateWindow call right back into the mix.
In defense of ATL, neither of the two popular windowless site implementations, Visual Basic® and Microsoft Internet Explorer, has to integrate with the Dialog Manager like ATL does. The Dialog Manager is the window class responsible for showing dialogs. However, in spite of the popularity of COM controls, it still does not support control containment. To add control containment support to the Dialog Manager, the CAxDialogImpl class takes extraordinary measures to preprocess extended dialog resources, removing the CONTROL entries that represent COM controls and replacing them with a custom window class called AtlAxWin that will act as the control's site. Thanks to the Dialog Manager, ATL needs to give each site an HWND. Because of the HWNDs, you're able
to treat a site as a window, making it easier for you to manipulate them. The HWNDs also provide another hook on which we'll build our extensions, as you'll see shortly.
Bear in mind that CAxHostWindow is an undocumented class, and instances are never created by the outside world directly. Instead, as each AtlAxWin window is created, its WM_CREATE handler creates an instance of CAxHostWindow. This works well when extending the Dialog Manager, but isn't very straightforward when you'd like to create a control manually. For these situations, there's a wrapper class called CAxWindow (see Figure 4). CAxWindow inherits from CWindow and wraps the site's HWND, providing wrapper methods to create and access the site and the corresponding CAxHostWindow.
|
Figure 5 CAxWindow
Figure 5 shows the relationship between CAxHostWindow and CAxWindow. Unfortunately, CAxWindow has a leak under certain conditions, so we'll use our derived class, CAxWindow2, as described in the April 1999 article. CAxWindow2's member functions are identical to CAxWindow, but its implementation removes the potential leak.
Using either class, we can implement simple control containment in a few lines of code:
LRESULT CMyWindow::OnCreate(UINT,
WPARAM, LPARAM, BOOL&) {
// Assume m_ax is of type
// CAxWindow or CAxWindow2
LPCTSTR pszControlName =
_T("mscal.calendar");
RECT rect = { 10, 10, 200, 200 };
HWND hwnd = m_ax.Create(m_hWnd,
rect, pszControlName, WS_CHILD |
WS_VISIBLE);
return (hwnd ? 0 : -1);
}
Extending the ATL Containment Classes
While the level of containment support provided by ATL is sufficient for most of the normal day-to-day containment implementations, it has certain shortcomings. First, while the IOleContainer interface is generally implemented on the Document or Form object of a container, ATL implements this interface on its site object (CAxHostWindow). Also, instead of sharing the instances of the UI and Frame window objects for all sites, each instance of CAxHostWindow creates associated instances of the CAxUIWindow and CAxFrameWindow objects. This means when hosting one control, ATL may create up to three extra windows.
CAxHostWindow creates hardcoded instances of CAxUIWindow and CAxFrameWindow in its implementation of CAxHostWindow::GetWindowContext. Thus, to override the default ATL UI and Frame window behavior (say you wanted to implement IOleInPlaceFrame::EnableModeless), you also need to override CAxHostWindow and GetWindowContext.
ATL control-creation code (the CreateNormalizedObject routine) does not check if the control supports licensing, and always creates controls via IClassFactory instead of IClassFactory2.
Controls have read-only access to the Ambient Properties, which define the container's environmental aspects. While most standard containers don't allow the controls to change the ambient properties, the ATL implementation of ambient dispatch on the site (IAxWinAmbientDispatch) allows the controls to do so.
ATL lacks a basic implementation for Extended controls, and the CAxHostWindow::GetExtendedControl implementation returns the control's own IDispatch.
CAxHostWindow does not support SimpleFrame controls, as it does not inherit from ISimpleFrameSite.
COM controls mark some of the UI preferences using the OLEMISC status bits. Though the ATL containment code checks for OLEMISC_SETCLIENTSITEFIRST during control activation (CAxHostWindow::ActivateAx), it completely neglects all other OLEMISC bits including OLEMISC_INVISIBLEATRUNTIME, OLEMISC_
NOUIACTIVATE, and OLEMISC_
ACTIVATEWHENVISIBLE.
ATL containment code lacks advanced container keyboard handling support. That means you can't check for special type library-based attributes like OLE_TRISTATE and OLE_
OPTEXCLUSIVE to see if the controls have checkbox and radio-button-like behavior.
You also can't determine how the control handles Enter and Escape keys, accelerators, and mnemonics. The ATL keyboard management code does not call important keyboard-related methods on the control, namely IOleControl::
GetControlInfo and IOleControl::OnMnemonics. Also, the implementation of CAxHostWindow::OnControlInfoChanged simply returns S_OK without doing anything.
Another thing you can't do is give special treatment to controls based on their OLEMISC bits-based keyboard preferences, including checking whether the control is marked with OLEMISC_ACTSLIKEBUTTON and OLEMISC_ ACTSLIKELABEL.
Many standard containers merge their property pages with those of controls. In fact, the controls call IOleControlSite::ShowPropertyFrame in their implementation of IOleObject::DoVerb(OLEIVERB_PROPERTIES), allowing the container to merge its pages with those of the control. However, the CAxHostWindow::ShowPropertyFrame method simply returns E_NOTIMPL.
Most of all, unlike the rest of ATL, the containment architecture is not well-factored. It is not easy to change the behavior of any one of the base classes without changing other pieces of the ATL code. The most important class of the containment hierarchy (CAxHostWindow) is not easy to override. Many of its interface methods simply return E_NOTIMPL, S_FALSE, or S_OK without doing anything.
To right these control containment wrongs, the containment support provided by ATL needs to be extended. We started extending ATL containment by creating derivatives of CAxHostWindow and CAxWindow2, which we called CAxHostWindow2T and CAxWindowImplT. These are reusable classes that you can plug into your code while you're developing an ATL-based container.
CAxHostWindow2T
|
Figure 6 CAxHostWindow2T
We started extending ATL 3.0 control containment by deriving a class called CAxHostWindow2T from CAxHostWindow (see Figure 6 and Figure 7). As you can see, CAxHostWindow2T also inherits from IPersistPropertyBagImpl and IPersistStreamInitImpl. This provides the text and binary persistence of all the ambient properties implemented by CAxHostWindow and exposed via IAxWinAmbientDispatch. This is done in conjunction with the corresponding PROP_MAP in CAxHostWindow2T.
// All the ambient properties exposed by
// CAxHostWindow via IAxWinAmbientDispatch
BEGIN_PROP_MAP(CAxHostWindow2T)
...
PROP_ENTRY("BackColor",DISPID_AMBIENT_BACKCOLOR,
CLSID_NULL)
PROP_ENTRY("ForeColor",DISPID_AMBIENT_FORECOLOR,
CLSID_NULL)
PROP_ENTRY("LocaleID",DISPID_AMBIENT_LOCALEID,
CLSID_NULL)
...
END_PROP_MAP()
It's a good idea to disable the IOleContainer member. IOleContainer is a top-level interface implemented on the Form or Document object. It provides the services of enumerating all the controls on the Form or Document object. Using this interface, a control can navigate all its sibling controls on the Form. You get the interface from the control by calling IOleClientSite::GetContainer. The designers of this interface provided this method because they probably did not want to have it on every site object (and expose it via QueryInterface). ATL, on the other hand, implements this interface on every CAxHostWindow object (and exposes it via QueryInterface), which defeats the purpose of the interface and changes the standard container object model.
class ATL_NO_VTABLE CAxHostWindow :...
public IOleControlSite,
public IOleContainer,...
{...
BEGIN_COM_MAP(CAxHostWindow)
...
COM_INTERFACE_ENTRY(IOleControlSite)
COM_INTERFACE_ENTRY(IOleContainer)
...
END_COM_MAP()
...
};
The ATL COM_INTERFACE_ENTRY_NOINTERFACE macro comes in handy for disabling the QueryInterface for IOleContainer in the CAxHostWindow2T class, even when we chain to the interface map of the base class (CAxHostWindow):
typedef CAxHostWindow2T<TFrameWindow> thisClass;
BEGIN_COM_MAP(thisClass)
...
COM_INTERFACE_ENTRY_NOINTERFACE(IOleContainer)
...
COM_INTERFACE_ENTRY_CHAIN(CAxHostWindow)
END_COM_MAP()
CAxHostWindow also derives from IObjectWithSiteImpl, which stores the current site pointer in its m_spUnkSite member. The m_spUnkSite gets set from the container that calls IObjectWithSite::SetSite. The ATL implementation of IOleClientSite::GetContainer first checks whether the site object also implements IOleContainer by doing a QueryInterface on its m_spUnkSite member and, if that fails, returns its own implementation of IOleContainer.
STDMETHOD(GetContainer)(IOleContainer** ppContainer)
{
ATLTRACE2(atlTraceHosting, 0,
_T("IOleClientSite::GetContainer\n"));
HRESULT hr = E_POINTER;
if (ppContainer)
{
hr = E_NOTIMPL;
(*ppContainer) = NULL;
if (m_spUnkSite)
hr = m_spUnkSite->QueryInterface(
IID_IOleContainer, (void**)ppContainer);
if (FAILED(hr))
hr = QueryInterface(IID_IOleContainer,
(void**)ppContainer);
}
return hr;
}
CAxHostWindow2T maintains a CComPtr<IOleContainer> smart pointer member, m_spOleContainer, and expects the user of the class to set it with the Form or Document object's IOleContainer interface during its creation. The CAxHostWindow2T implementation of IOleClientSite::
GetContainer corrects CAxHostWindow's behavior by returning the m_spOleContainer:
//CAxHostWindow2T::GetContainer
STDMETHODIMP GetContainer(IOleContainer** ppContainer)
{
if(!ppContainer) return E_POINTER;
if(m_spOleContainer)
return (*ppContainer =
m_spOleContainer)->AddRef(),S_OK;
*ppContainer = 0;
return E_NOINTERFACE;
}
Support for SimpleFrame Site Controls
There is a category of controls called SimpleFrame that acts as simple containers of other controls. Examples include the Frame or PictureBox intrinsic controls of Visual Basic or the Microsoft Tabbed Dialog control. SimpleFrame controls rely on the container's support for the ISimpleFrameSite interface on its site object. We decided to add support for ISimpleFrameSite as a tear-off interface on CAxHostWindow2T:
BEGIN_COM_MAP(CAxHostWindow2T<TFrameWindow>)
...
COM_INTERFACE_ENTRY_CACHED_TEAR_OFF(IID_ISimpleFrameSite,
CSimpleFrameSite<TFrameWindow>,
m_spUnkSimpleFrameSite.p)
...
END_COM_MAP()
The corresponding class, which implements ISimpleFrameSite as a tear-off, is CSimpleFrameSite:
template<typename TFrameWindow>
class ATL_NO_VTABLE CSimpleFrameSite:
public ISimpleFrameSite,
public CComTearOffObjectBase<CAxHostWindow2T<TFrameWindow> >
{
public:
...
typedef CSimpleFrameSite<TFrameWindow> thisClass;
BEGIN_COM_MAP(thisClass)
COM_INTERFACE_ENTRY(ISimpleFrameSite)
END_COM_MAP()
...
//ISimpleFrameSite
STDMETHODIMP PreMessageFilter(HWND hWnd,UINT msg,
WPARAM wp,LPARAM lp, LRESULT *plResult,
DWORD *pdwCookie){...}
STDMETHODIMP PostMessageFilter(HWND hWnd, UINT msg,
WPARAM wp, LPARAM lp, LRESULT *plResult,
DWORD dwCookie){...}
};
SimpleFrame controls announce their framing ability
by adding OLEMISC_SIMPLEFRAMESITE to their OLEMISC status bits. The site object of a container that supports these controls implements the ISimpleFrameSite interface, which has two methods: PreMessageFilter and PostMessageFilter. The SimpleFrame control calls these methods before and after processing the messages in its WndProc. A SimpleFrame control obtains the pointer to ISimpleFrameSite in its IOleObject::SetClientSite method as follows:
STDMETHODIMP ASimpleFrameControl::SetClientSite(
IOleClientSite *pClientSite)
{
...
// Imagine m_spSimpleFrameSite is a
// CComPtr<ISimpleFramesite>
HRESULT hr = pClientSite->
QueryInterface(&m_spSimpleFrameSite);
if(SUCCEEDED(hr)){
// Hmmm..This container supports SimpleFrame
// controls!
}
...
}
This interface is optional, so the control should work gracefully in its absence. Containers enabled with this interface should parent the child control (or the site window, in the case of ATL) onto the SimpleFrame control instead of its site (as is the case with ATL) or the Form window. You can figure out the OLEMISC status bits by using the OleRegGetMiscStatus function. The following snippet checks if the control is a SimpleFrame:
DWORD dwStatus;
::OleRegGetMiscStatus(clsidOfControl, DVASPECT_CONTENT,
&dwStatus);
if(dwStatus & OLEMISC_SIMPLEFRAME)
{
//aha! A SimpleFrame control after all!
}
While a generic implementation of ISimpleFrameSite can be pretty complex (SimpleFrame controls can have arbitrarily nested child controls), our implementation of it in CSimpleFrameSite is pretty basicit simply routes certain messages to the wndproc of the child control of a SimpleFrame control via SendMessage:
//CSimpleFrameSite::PreMessageFilter…
STDMETHODIMP PreMessageFilter(HWND hWnd, UINT msg,
WPARAM wp,LPARAM lp, LRESULT *plResult,
DWORD *pdwCookie)
{
if(!plResult || !pdwCookie) { return E_POINTER; }
HWND hWndChild = GetAxWindow(hWnd);
if(hWndChild && ((msg == WM_PAINT) ||
/*other messages*/) )
{
*plResult = 0;
::SendMessage(hWndChild,msg,wp,lp);
...
}...
return S_OK;
}
Support for Licensed Controls
CAxHostWindow uses a static helper routine called CreateNormalizedObject that performs the magic of creating an instance of the COM control based on a string argument; that is, the szWindowName argument of the CAxWindow[2]::Create method. For creation, it calls CoCreateInstance on the control's CLSID. CoCreateInstance does the moral equivalent of a call to CoGetClassObject to retrieve the IClassFactory interface, and then calls IClassFactory::CreateInstance.
Controls that support licensing hand out their control object via IClassFactory2 instead of IClassFactory. To support licensed controls, the container must call CoGetClassObject for IClassFactory2, and then call IClassFactory2::
CreateInstanceLic.
CAxHostWindow2T supports licensed control creation through a virtual method called CreateLicensedControl (see Figure 8). The overridden version of CAxHostWindow2T::CreateControlEx then calls its CreateNormalizedObjectEx method.
STDMETHODIMP CreateControlEx(LPCOLESTR lpszTricsData,
HWND hWnd, IStream* pStream,
IUnknown** ppUnk, REFIID iidAdvise,
IUnknown* punkSink){
...
if (::IsWindow(hWnd)){
...
bool bWasHTML;
hr = CreateNormalizedObjectEx(lpszTricsData,
IID_IUnknown,(void**)ppUnk, bWasHTML);
...
}
}
Returning Shared Objects to the Control
The Form object in a container's object model implements IOleInPlaceUIWindow (see Figure 9), which is used to negotiate border space on the container's Form window. On the other hand, the container's Frame object, which is commonly implemented by its top-level frame window, implements the IOleInPlaceFrame interface. The Frame object allows menu negotiations, sets and displays status text relevant to the in-place object, enabling the frame's modeless dialog boxes, and translates accelerator keystrokes intended for the container's frame.
|
Figure 9 The Form Object
The CAxUIWindow and CAxFrameWindow classes of ATL provide a basic implementation of IOleInPlaceUIWindow and IOleInPlaceFrame, respectively. Ideally, the container's instances of the UIWindow and Frame objects are shared across all the site objects. In the ATL implementation, each instance of CAxHostWindow internally creates instances of CAxUIWindow and CAxFrameWindow via CAxHostWindow::GetWindowContext (see Figure 10).
You may be tempted to override the CAxUIWindow and CAxFrameWindow classes in ATL (the ATL implementations don't do anything interesting). But since CAxHostWindow::GetWindowContext is hardcoded to use CAxUIWindow and CAxFrameWindow, you also have to override GetWindowContext to hand out your implementations. CAxHostWindow2T does exactly that. In fact, CAxHostWindow2T doesn't even create an instance of CAxUIWindow or CAxFrameWindow; it uses the m_spInPlaceFrame(CComPtr<IOleInPlaceFrame>) and m_spInPlaceUIWindow(CComPtr<IOleInPlaceUIWindow>) members, which now can be assigned by the user of CAxHostWindow2T during its creation. CAxHostWindows2T simply returns copies of those members in its implementation of GetWindowContext (see Figure 11).
Merging Property Pages
The IOleObject::DoVerb(OLEIVERB_PROPERTIES) implementation of controls generally calls IOleControlSite::ShowPropertyFrame, giving the container a chance to merge its property pages along with those of the control. The default ATL implementation of the method for CAxHostWindow just returns E_NOTIMPL.
STDMETHOD(ShowPropertyFrame)()
{
return E_NOTIMPL;
}
The CAxHostWindow2T class overrides this implementation, which now merges the property pages of the control with the containers (for our extended control properties, which are discussed next). For this, we obtain the CLSID array of the control's property pages via a call to ISpecifyPropertyPages::
GetPages and add our pages to the array before calling the OleCreatePropertyFrame API to create the Property Frame (see Figure 12 and Figure 13).
|
Figure 13 A Form's Merged Property Pages
CAxHostWindow::GetExtendedControl
For every hosted control, most standard containers maintain an Extended Control object, which contains the implementation of automation attributes common to all the controls. You access the Extended Control object by calling IOleControlSite::GetExtendedControl. The documented behavior of this method allows containers that don't support Extended Controls to simply return E_NOTIMPL.Strangely, the CAxHostWindow implementation of GetExtendedControl returns the IDispatch of the embedded control.
STDMETHOD(GetExtendedControl)(
IDispatch** ppDisp)
{
if (ppDisp == NULL)
return E_POINTER;
return m_spOleObject.QueryInterface(ppDisp);
}
CAxHostWindow2T, on the other hand, does the right thing. Each CAxHostWindow2T instance contains an IDispatch pointer member, m_spExtendedDispatch, which the user of the class can set during its creation. CAxHostWindow2T now returns the IDispatch of a given Extended Control object in its implementation of IOleControlSite::
GetExtendedControl:
//CAxHostWindow2T<>::GetExtendedControl...
STDMETHODIMP GetExtendedControl(IDispatch** ppDisp)
{
if(!ppDisp) return E_POINTER;
if(m_spExtendedDispatch)
return (*ppDisp = m_spExtendedDispatch)->
AddRef(), S_OK;
*ppDisp = 0;
return E_NOTIMPL;
}
CAxWindowImplT
Remember that CAxHostWindow objects are created indirectly by calling the CAxWindow::Create method. CAxWindow::Create creates an instance of the AtlAxWin window class; it's the WM_CREATE handler of CAxWindow that creates an instance of CAxHostWindow. To bootstrap our CAxHostWindow2T-based objects, we instead created a class called CAxWindowImplT, which derives from CAxWindow2 and CMessageMap and acts like the envelope or handle class for the enhanced site class described earlier (CAxHostWindow2T). CAxWindowImplT also superclasses the AtlAxWin window class, registering a new window class called AtlAxWinEx, which eliminates the need to replace the AtlAxWin WndProc (AtlAxWindowProc). This means we get to process all AtlAxWin Windows® messages using the MESSAGE_HANDLER macros within the comfort of our own CAxWindowImplT class. In other words, we further enhanced CAxWindow2 with message processing capabilities. The result is CAxWindowImplT:
template <typename TDeriving,
typename TWindow = CAxWindow2>
class CAxWindowImplT : public CWindowImplBaseT<
TWindow >{...};
The ATL DECLARE_WND_SUPERCLASS macro helps create the superclassed version of the AtlAxWin class:
class CAxWindowImplT : ...{
DECLARE_WND_SUPERCLASS(_T("AtlAxWinEx"),
CAxWindow::GetWndClassName())
...
};
The CAxWindowImplT class just handles WM_CREATE and WM_NCDESTROY messages. In fact, CAxWindowImplT's WM_CREATE handler calls AxCreateControl2, which in its default implementation calls AtlAxCreateControlEx. The derived class of CAxWindowImplT can override this method to implement any specialized control-creation technique without changing the rest of the code:
LRESULT OnCreate(UINT uMsg, WPARAM wParam,
LPARAM lParam, BOOL& bHandled)
{
::OleInitialize(NULL); ...
//Derived class gets a chance to change the
//control-creation behavior
TDeriving* pT = static_cast<TDeriving*>(this);
HRESULT hr = pT>AxCreateControl2(
T2COLE(lpstrName),
m_hWnd, spStream, &spUnk);
...
bHandled = TRUE;
return 0L;
}
So far we have developed some basic plumbing with CAxHostWindow2T and its handle class, CAxWindowImplT (and CAxWindow2). Now we'll show you how to implement a form-based container constructed on top of the reusable pieces that we just built. We'll start off by creating the Extended Control class and a specialized implementation of CAxWindowImplT, and then later add other containers like forms, property browsers, objects that implement persistence, and a container frame windowall of which, along with some common glue code, will help to create a form-based container with all the goodness of ATL.
Extended Control Objects
A typical control container implements an Extended Control Dispatch alongside the control's primary IDispatch. This object implements the automation properties, methods, and events that are specific to the container itself, rather than the control. Common examples include Name, Left, Top, Visible, and so on. Usually the containers have a one-to-one relationship between the site and the Extended Dispatch. We created a custom interface called IExtendedDispatch to implement the Extended Control object for our container:
[
object,
uuid(6BF5B59F-C506-
1D2-B382-00C04F72D6C1),
dual, helpstring(
"IExtendedDispatch
Interface"),
pointer_default(unique)
]
interface IExtendedDispatch : IDispatch
{
[propget, id(DISPID_Name),
helpstring("extended
Name property")]
HRESULT Name([out, retval]
BSTR *pVal);
[propput, id(DISPID_Name),
helpstring("extended Name property")]
HRESULT Name([in] BSTR newVal);
//other extended properties and methods
...
}
Figure 14 shows the implementation of the Extended Control object realized by the CExtendedDispatch class, which implements a dual interface called IExtendedDispatch.
For writing the sample container, we derived another class called CAxWindowHandle from CAxWindowImplT. The CAxWindowHandle class can interact effectively with the container's Frame window and the site object. It also contains an instance of the CExtendedDispatch class, and thus creates a one-to-one relationship between the Extended Dispatch and the site.
In the CAxWindowHandle implementation of AxCreateControl2 (which is called from the WM_CREATE handler of CAxWindowImplT), it creates an instance of CAxHostWindow2T, passing the container's Frame window as its template argument. It sets the smart pointer members of CAxHostWindowT, including the IDispatch of the Extended Control object and IOleInPlaceFrame, IOleInPlaceUIWindow, and IOleContainer, with the values obtained from the right set of objects from the container's object model in CAxWindowHandle::InitializeSite (see Figure 15). Here's how it initializes the CAxHostWindow2T members:
HRESULT CAxWindowHandle::InitializeSite()
{
HRESULT hr = E_FAIL;
CContainerFrame *pFrame = GetContainerFrame();
if(!pFrame) return hr;
m_pSiteObject->m_pContainerFrame = pFrame;
if(pFrame && pFrame->m_pFormObj)
{
hr = pFrame->m_pFormObj->QueryInterface
(&m_pSiteObject->m_spOleContainer);
// and similarly initializes other
// CAxHostWindow2T CComPtr<>members
...
}
return hr;
}
The main purpose of CAxWindowHandle is message handling for both the site and the control window. For better control over the windowing aspects of the contained control during design mode, CAxWindowHandle actually keeps a member of CContainedWindow, which dynamically subclasses the control window in case the control is windowed. As shown in Figure 15, it dynamically subclasses the control window by calling its SubClassControl method, which in turn calls CWindowImplBaseT::SubClassWindow.
BOOL
CAxWindowHandle::SubclassControl()
{ ...
HWND hWndChildControl =
::GetWindow(m_hWnd,GW_CHILD);
if(::IsWindow(hWndChildControl) )//windowed control
{
...
return m_wndControl.SubclassWindow(
hWndChildControl);
}
return FALSE;
}
The CAxWindowHandle message map contains an ALT_
MSG_MAP entry to handle the windowed control's messages:
BEGIN_MSG_MAP(CAxWindowHandle)
...
MESSAGE_HANDLER(WM_SETFOCUS,OnSetFocus)
MESSAGE_HANDLER(WM_NCDESTROY,OnNCDestroy)
MESSAGE_HANDLER(WM_NCHITTEST,OnNCHitTest)
...
COMMAND_RANGE_HANDLER(ID_MIN_VERB,ID_MAX_VERB,OnVerb)
MESSAGE_HANDLER(WM_COMMAND,OnDelete)
...
CHAIN_MSG_MAP(CAxWindowImplT<CAxWindowHandle,
CAxWindow2>)
ALT_MSG_MAP(1)
MESSAGE_HANDLER(WM_CONTEXTMENU,
OnContextMenu)
...
//like static controls
//this bounces back
//WM_NCHITEST in design
//mode
MESSAGE_HANDLER(WM_NCHITTEST,
OnNCHitTestCtl)
...
END_MSG_MAP()
One thing that's particularly interesting is the way we move and resize the control window (and the corresponding site window) at design time and runtime based on the UserMode property of the container. In design mode, the WM_NCHITTEST handler for the subclassed control returns HTTRANSPARENT, thus bouncing the message to the site window (CAxWindowHandle). In run mode, it simply delegates the message to the control's window procedure. Returning HTTRANSPARENT in design mode makes the control window behave exactly like the Windows static control (label control), which cannot handle most of the messages by itself, but rather routes them to the parent of the static control. In our case this is the site's handle window:
LRESULT
CAxWindowHandle::OnNCHitTestCtl(
UINT, WPARAM,
LPARAM, BOOL& bHandled)
{
bHandled = FALSE;
// RunTime
if(m_pSiteObject->m_bUserMode)
return 0L;
bHandled = TRUE;
return HTTRANSPARENT;
}
Once WM_NCHITEST is bounced to CAxWindowHandle, it then handles the messages of the child control. This helps to move and resize the control, keeping the site and the control window in sync at design time, as expected.
LRESULT CAxWindowHandle::OnNCHitTest(UINT, WPARAM,
LPARAM lParam, BOOL& bHandled)
{
if(m_pSiteObject->m_bUserMode)//RunTime
return HTCLIENT;
RECT rc; GetWindowRect(&rc);...
POINT pt = {LOWORD(lParam), HIWORD(lParam)};
if( ::PtInRect(&rc,pt) )
return HTCAPTION;
if( IsInvisAtRunTime() )
return HTCLIENT;
bHandled = FALSE;
return 0L;
}
We use a similar technique for a few nonclient messages.
|
Figure 17 A Verb Menu
Most containers display the control's verb menu when the user right-clicks on the control on the Form. Because we're handing the control's messages (in case it's windowed) in design mode, we can handle WM_CONTEXTMENU and show the control's verb menu. The verb menu itself is populated via the OleUIAddVerbMenu API (see Figure 16). Figure 17 shows a sample verb menu.
Containment is a Form of Art
The Form object is just an extension of the classic document object of an OLE container. The Form class generally inherits from the IOleInPlaceFrame
interface, which derives from IOleInPlaceUIWindow. It also derives from IOleContainer and allows control enumeration, among other things.
Besides inheriting the container frame interfaces just mentioned, we decided to implement the form-based container as an ATL composite control so that it can be dealt with in pretty much the same way as the rest of the controls it embeds, using the modified CAxWindowHandle and CAxHostWindow2T. An ATL composite control is a container control that derives from CComCompositeControl and provides all of the necessary support for dialog-based containment. Our container con-trol provides all the necessary container services:
- Keeping track of the current UI Active object
- Text and binary persistence of the hosted controls
- Setting the right parent of the control dropped onto its client area by checking if the control is being dropped directly on the Form or a SimpleFrame control
- Disabling or enabling the owned popup and child windows in its implementation of IOleInPlaceFrame::
EnableModeless
- Allowing the control to enumerate all of its sibling controls by implementing IOleContainer::EnumObjects
- Checking if the dropped control is marked with OLEMISC_INVISIBLEATRUNTIME and not allowing such controls to resize in design mode
- Maintaining a consistent container-wide design time and runtime UI behavior, and creating, destroying, and persisting the controls during the UserMode transitions
- Showing the fired control events as status bar text by implementing IOleInPlaceFrame::SetStatusText
- Initializing the scripting engine and adding the map of the control's native IDispatch and the value of its extended name property
- Maintaining a vector of the hosted controls and updating it based on the deletion or addition of a new control on the form
- Keeping the container-wide accelerator table and handing it out in the OLEINPLACEFRAMEINFO structure the site object asks for in its implementation of GetWindowContext
- Implementing PreTranslateAccelerator to override the default composite control tabbing behavior
- Having its own properties that allow us to seamlessly show the control's and form's properties in the container's Property Browser
A hosted control can gain access to the interfaces of the Form object via the site object associated with every control (CAxHostWindow2T). Typically the control calls IOleInPlaceSite::GetWindowContext to access IOleInPlaceFrame, IOleInPlaceUIWindow, and IOleClientSite::GetContainer to obtain IOleContainer (see Figure 18 and Figure 19).
|
Figure 18 An Artistic Form Object
Enumerating Controls on the Form
Some controls work in pairs and need to access each other via a private interface. Such controls rely on the generic service provided by the container to enumerate the IUnknown pointers of all the controls on the form. The Form object implements its IOleContainer::EnumObjects using the CComEnumOnSTL ATL class, providing it a _Copy implementation in the form of a custom copy policy class called _CopyIUnknownFromCAxWindowHandle (see Figure 20). _CopyIUnknownFromCAxWindowHandle returns an IUnknown interface of the control hosted by CAxWindowHandle given its pointer by calling CAxWindowHandle::QueryControl.
struct _CopyIUnknownFromCAxWindowHandle
{
static HRESULT copy(IUnknown** ppUnk,
CAxWindowHandle** ppAxWindow)
{
if(!ppAxWindow || !(*ppAxWindow) || !ppUnk)
return E_POINTER;
*ppUnk = 0;
return (*ppAxWindow)->QueryControl(ppUnk);
}
static void init(IUnknown** ppUnk) { }
static void destroy(IUnknown** ppUnk)
{
if(*ppUnk)
(*ppUnk)->Release();
}
};
The Form class has a helper routine that creates and hosts the site and its control at the point where it's dropped by the user from the control toolbox. m_bstrDroppedCtrlCLSID is the CLSID of the control being dropped on the form, and it's set by the toolbox during the drag. If a control is being dropped on a SimpleFrame control, the Form will use the SimpleFrame control as the parent instead of the Form itself (see Figure 21).
The routine in Figure 21 also checks whether the dropped control is marked with OLEMISC_INVISIBLEATRUNTIME status bits. If so, the Form disables resizing of the control and makes it acquire a fixed size. The container can obtain the initial size of such a control by calling the following function:
IOleObject::GetExtent:
...
::OleRegGetMiscStatus(clsid,
DVASPECT_CONTENT, &dwStatus);
if(dwStatus & OLEMISC_INVISIBLEATRUNTIME)
{
pSiteNode->SetInvisAtRunTime(true);
}
...
if(pSiteNode->IsInvisAtRunTime())
{
CComPtr<IOleObject> spObj;
if(SUCCEEDED( pSiteNode->
QueryControl(&spObj) ))
{
SIZEL sz;
spObj->
GetExtent(DVASPECT_CONTENT,&sz);
AtlHiMetricToPixel(&sz,&sz);
pSiteNode->
SetWindowPos(0,pt.x,pt.y,
sz.cx,sz.cy,SWP_NOZORDER);
}
}
Ideally, a control container should never create the window for a control marked OLEMISC_INVISIBLEATRUNTIME, but that would require substantial changes in the ATL containment architecture. To keep it simple, CAxWindowHandle implements OnUserModeChanged and hides the control if it's marked OLEMISC_INVISIBLEATRUNTIME. This routine gets called from the form during UserMode changes.
HRESULT
CAxWindowHandle::OnUserModeChanged(
VARIANT_BOOL bUserMode)
{
...
if( IsInvisAtRunTime() )
{
if(!m_pSiteObject->m_bUserMode)
ShowWindow(SW_SHOW);
else
ShowWindow(SW_HIDE);
}
...
return S_OK;
}
A control calls IOleInPlaceFrame::EnableModeless just before and after popping up a dialog box. This tells the container to enable or disable its own popup windows and dialogs to obtain the correct modal or modeless behavior. The Form object implementation of the method calls the EnableWindow API on each of the owned popup windows of the container frame.
STDMETHODIMP CForm::EnableModeless(BOOL fEnable)
{
...
if(m_pData)
m_pData->EnableOrDisableOwnedPopups(fEnable);
return S_OK;
}
Event Sinking
To demonstrate how to sink the control's events, we wrote a very simple IDispatch-derived CEventMap class. Every CAxWindowHandle contains an instance of the CEventMap. During its creation, CEventMap automatically calls AtlAdvise, establishing the advisory connection with the control for the events. It also calls IOleControl::FreezeEvents on the control as appropriate. CEventMap keeps an array of EventInfo structures, which in turn hold a DISPID-CComBSTR pair for the DISPID of the event and the name of the event combination.
struct EventInfo
{
DISPID m_dispID;
CComBSTR m_strEventName;
};
After its creation, CEventMap initializes its array of EventInfo structures by calling Init, which extracts the DISPID of the event methods and the names of the methods from the control's type information (see Figure 22).
As you can see in Figure 22, Init delegates extracting the Event source's TypeInfo to another helper called GetEventTypeInfo, which calls AtlGetObjectSourceInterface and later extracts the ITypeInfo pointer from the Typelib.
HRESULT CEventMap::GetEventTypeInfo(
LPTYPEINFO *ppTypeInfo)
{
if(!m_spObjUnk || !ppTypeInfo){ return E_POINTER; }
//This gets the LIBID, IIDSrc,and version
HRESULT hr =
AtlGetObjectSourceInterface(m_spObjUnk.p,
&m_libID, &m_iidSrc, &m_dwMajor, &m_dwMinor);
if(FAILED(hr)) return hr;
CComQIPtr<IDispatch> spDispatch(spObjUnk);
if(!spDispatch) return E_NOINTERFACE;
CComPtr<ITypeInfo> spTypeInfo;
hr = spDispatch->GetTypeInfo(0, 0, &spTypeInfo);
if(FAILED(hr)) return hr;
CComPtr<ITypeLib> spTypeLib;
hr = spTypeInfo->GetContainingTypeLib(&spTypeLib,
0);
if(FAILED(hr)) return hr;
return spTypeLib->GetTypeInfoOfGuid(m_iidSrc,
ppTypeInfo);
}
Finally, CEventMap's implementation of IDispatch::Invoke is called each time the control fires an eventour implementation just shows the name of the fired event in the status bar of the container's frame window using IOleInPlaceFrame::SetStatusText (see Figure 23 and Figure 24).
|
Figure 23 VBLite's Fired Events
CEventMap shows just how easy it is to sink a control's events. If you're interested, you can extend this basic event sinking mechanism by adding the support for event handlers via scripting. We have already provided the basic scripting mechanism in the sample container, which allows scripting for a control's native properties by providing an implementation of a Script Site object.
Control Property Browser
ATL 3.0 ships with a sample control container called ATLCON. The sample container has the code for a listview-based COM control, which shows all the properties of the control by reading the type library. It also allows the user to edit the properties. We extended this code and added the listview control in a tabbed view, along with some more tabs for showing the container-maintained ambient and extended properties. We also added a dropdown showing all the controls on the form. It gets filled by using the control enumeration code described earlier (see Figure 25). Figure 26 shows the property browser.
|
Figure 26 Our Property Browser
You can extend the sample container by presenting a categorized view of the control's properties (hint: such controls support ICategorizeProperties) and showing the control's property pages associated with a given property (hint: a control like this
supports IPerPropertyBrowsing).
|
Figure 27 Adding Controls to the Container
So that users can add controls to the container, we added a toolbar showing the icons of the COM controls the container knows about. To add to the list of known controls, VBLite pops up the standard control insert dialog using the call to the OleUIInsertObject API when the user chooses the Add New Component menu item (see Figure 27). Upon selecting and closing the dialog we retrieve the bitmap registered under the ToolboxBitmap32/ ToolboxBitmap key from the obtained control CLSID and create a tool button (with a tooltip giving the control's progID, of course) in our toolbar. We then allow the user to drag the tool button and drop it on the format which point we create an instance of the CAxWindowHandle, host the control, and add the site pointer to the form-managed vector of sites.
Persistence Support
During the UserMode transitions, the container interacts with the IPersistStreamInit interface of the control to persist the control's native properties along with the container-maintained extended control properties. The Form object provides an IStream object created to host the control's IPersistStreamInit::Load and IPersistStreamInit::Save.
HRESULT CForm::CreateStream()
{
FreeStream();//Frees the previous Stream
return ::CreateStreamOnHGlobal(0,TRUE,&m_spStream);
}
When the container leaves design mode, it saves all of the properties of the hosted controls and destroys the control objects. In run mode, it recreates the control objects and initializes them with the persisted properties. This cycle
of destruction, creation, and initialization is repeated
when the container shifts from run mode to design mode. Figure 28 shows the implementation of the binary save of the control's native and extended properties.
Keyboard Handling
An instance of the frame window gets created in WinMain. The frame window also implements a keyboard-handling routine called PreTranslateAccelerator that after some filtering, routes the keystrokes to the Form object's PreTranslateAccelerator. The Form's implementation of PreTranslateAccelerator (which you'll see in a moment) implements tabbing across the hosted controls. Finally, the application's main message loop calls the Frame's PreTranslateAccelerator after checking the state of the EnableModeless flag.
MSG msg;
while (::GetMessage(&msg, 0, 0, 0))
{
if( !wndFrame.IsModeless() &&
wndFrame.PreTranslateAccelerator(&msg,hr) ){ continue; }
::TranslateMessage(&msg);
::DispatchMessage(&msg);
}
Keyboard handling for all the controls on an ATL-based composite control is accomplished via CComCompositeControl::PreTranslateAccelerator, which gets called from IOleInPlaceActiveObject::TranslateAccelerator. Our Form, being a composite control, allows us to reuse the same implementation. The container's main message loop routes the messages to the composite control via the frame window. While the default ATL implementation of CComCompositeControl::PreTranslateAccelerator doesn't let the user loop back through the controls on the dialog using the Tab key (this is by design), our version does (see Figure 29).
Top-level Application Frame Window
Our container's main frame window is an overlapped window that creates and owns the form, the property browser, and the control toolbox. Among other things, it has a status bar (which shows the fired events of the control), a toolbar, and a menu that you can use to save and load the form. The frame also manages the PropertyBag object for text persistence.
class CContainerFrame :
public CWindowImpl<CContainerFrame,CWindow,
CFrameWinTraits>
{
public:
...
DECLARE_WND_CLASS_EX(NULL, 0, 0)
...
BEGIN_MSG_MAP(CContainerFrame)
MESSAGE_HANDLER(WM_CREATE, OnCreate)
MESSAGE_HANDLER(WM_DESTROY, OnDestroy)
MESSAGE_HANDLER(WM_SIZE, OnSize)
COMMAND_ID_HANDLER(ID_FILE_OPEN, OnFileOpen)
COMMAND_ID_HANDLER(ID_FILE_CLOSE, OnFileClose)
COMMAND_ID_HANDLER(ID_FILE_SAVE, OnFileSave)
COMMAND_ID_HANDLER(ID_FILE_EXIT, OnFileExit)
COMMAND_ID_HANDLER(ID_VIEW_SCRIPTER,
OnScripterShow)
COMMAND_ID_HANDLER(ID_OLE_INSERT_NEW,
OnAddControl2ToolBox)
...
};
The instance of the frame gets created from WinMain, which is where the container comes to life (see Figure 30).
Because our control container (like any other COM control container) is a single-threaded apartment container, it calls OleInitialize in WinMain. This is because controls and containers are primarily windowed objects and need to be serviced by the same thread that created the window.
Other Features in VBLite
The sample container also implements text persistence with a basic version of a PropertyBag object, which implements IPropertyBag. When saving and loading the container's form, this object interacts with the hosted control's IPersistPropertyBag interface and reads or
writes the properties to a text file that looks like the one
in Figure 31.
Figure 32 Scripting in VBLite
Also enabled in the sample container is a basic implementation of scripting using a Script Site COM object. This object implements IActiveScriptSite and IActiveScriptSiteWindow, which interact with the scripting host's IActiveScript and IActiveScriptSiteParse interfaces. The site object that implements these interfaces is also a dialog where the user can type in the script (we only support VBScript). This is shown in Figure 32.
Summary
The crux of ATL containment is CAxHostWindow. We showed you how to extend the ATL containment implementation to write real-world containers. We also modified the CAxHostWindow class into CAxHostWindow2T and extended its access using CAxWindowImplT. You then saw how extending the handle class, along with writing a set of container-specific classes, can enable you to realize all of the features of a standard form-based container. In addition, this implementation corrects some of the design limitations of the ATL site object.
Further enhancing the VBLite sample container means adding support to create the controls added via the dialog editor; adding MDI/Multi SDI support to the container application; exposing ambient properties to the controls via read-only access; implementing clipboard-based editing of controls on the form; supporting additional OLEMISC status bits; complex scripting, including persistence of scripts and hooking up events from the script; more intricate control
keyboard handling, including calling IOleControl::
GetControlInfo; checking the type-information for attributes like OLE_TRISTATE and OLE_OPTEXCLUSIVE; and complex message routing for SimpleFrame controls. Now get cracking!
From the December 1999 issue of Microsoft Systems Journal.
| | | | |