Introduction
Every MFC VC++ programmer knows there is no easier task than to make a simple application with docking toolbar. Basically it is one-click job.
MFC application wizard does all the work and the coding itself is limited to adding images and filling in WM_COMMAND handlers.
Things are getting complicated when you would like to have a fancy MS Dev Studio-MS Word-Outlook-etc like look for your docking
controls. The good examples would be the source code of such a libraries as Kirk Stowel's Xtreme Toolkit
or Stas Levin's BCGSoft library.
However, the problem I was trying to solve is to create an ActiveX control with docking capabilities.
This would give Visual Basic applications the same look as MFC applications have.
In the article I am going to show how docking ActiveX control can be implemented using ATL and MFC.
There is also MS VC++ 6.0 project and Visual Basic (VB) samples attached to the article, which illustrate the usage of control.
Window Docking Control vs. ActiveX Docking Control
The specific feature of docking control is to maintain the docking state from user mouse dragging operations,
for instance to be able to align itself to the edges of application window or float.
MFC docking capabilities are supported in CControlBar
class,
which plays the role of docking control itself, and CFrameWnd
class,
which implements window to dock to. Usually the main window of MFC application is derived from CFrameWnd
class.
Docking process is performed by negotiation dialog between those two classes. It starts with enabling docking capabilities by specifying docking style for both classes.
Then ControlBar
register itself in CFrameWnd
class (DockControlBar
function) providing pointer.
So when docking is about to occur CFrameWnd
knows which control to dock and at what place.
The negotiation happens on every mouse movement of a docking control and consists of checking control size for a possible docking place.
On other hand, ActiveX control is not an application.
It is a COM object in form of DLL with implemented COM interfaces for visual presentation,
persistence and automation. The process of placing ActiveX control on container's form is called
'In-place activation' or 'embedding'. Another major difference is the way containing application
window communicates with controls. Container, through COM interfaces negotiation with control,
provides all necessary information to place control on a form, including a parent window handle
for control's child window creation. Initially Microsoft did not provide any docking capabilities
in Container-ActiveX control negotiation dialog. Later they introduced IDockingWindow
interface in
Internet Explorer, but not as a part of OLE framework (on which the whole ActiveX technology is based).
Container takes full control over visual representation of in-placed ActiveX control in terms of size and position.
The frame to place the ActiveX control on is supplied by container and there is no way to say in
advance is it MDI or SDI application.
Besides the OLE part of control implementation, the main problem is to find the way of implementing
the docking window and docking control. I divided the whole process in to the following issues:
� Necessary COM interfaces implementation. Differences from regular control.
|
� Docking approach implementation; docking frame and docking window; ATL and MFC libraries integration.
|
� Implementation of Automated collections.
|
� Control persistence.
|
Basic COM implementation
Lets start with a new ATL COM project in MSVC 6.0 IDE.
Using wizard add 'Full Control' from Add ATL Object Wizard Dialog
with following attributes: threading model - apartment; Interfaces - dual;
support for Connection point interface; miscellaneous status- 'invisible at runtime' and 'act as label'.
'Act as label' is needed to be able to place control on MDI frame. 'Connection point interface'
and 'Dual interfaces' are necessary for event handling and automation support respectively.
And 'invisible at runtime' makes ActiveX control be windowless. The last one deserves more detailed explanation.
Container takes full responsibility of size and position of in-placed ActiveX control.
In contrary, docking controls tend to maintain their size and position themselves and are able to
align to an edge of docking window or float, depending on relative position against docking window.
So making control invisible at runtime will allow control to maintain state internally without container's supervision.
As we can see from wizard-generated code, ATL implements the most of required ActiveX control interfaces.
There are few interfaces implementation and overwritten functions need to be added. Among them IpersistPropertyBag
interface with overwritten following functions:
IpersistPropertyBag::Load(), IpersistPropertyBag::Save(), IpersistStreamInit::Load(), IpersistStreamInit::Save()
for properties persistence.
Functions FinalConstruct()
, FinalRelease()
are used for creating/releasing aggregated
collection objects.
IOleObject::SetClientSite(IOleClientSite *pClientSite)
implementation is important for setting
up keyboard short-cut processing. This is the place to set active object that would get containers keyboard
processing messages before dispatching them.
Finally, since control is visible only in design time, the OnDraw()
function is being called only in design mode.
For instance, here in-place control icon can be drawn.
ATL and MFC
Using MFC for GUI and ATL 3.0 for COM and automation support would be fair trade off for control development.
In my project I have tried to combine functionality of both of those libraries.
The first problem I faced with was multiple inheritance from ATL CComObjectRootEx
class and
any of MFC CWnd
classes. It is necessary for objects like Bar which represent window control
"toolbar" and COM object at the same time. ATL uses the same naming convention for COM support
as MFC does. MFC CCmdTarget
class, the parent of CWnd
, has built-in COM support and contains all
conflicting names. I used redefinition for conflicted names to solve the problem:
#define InternalAddRef IntATLAddRef
#define InternalRelease IntATLRelease
#define InternalQueryInterface IntATLQueryInterface
#define m_dwRef m_ATLdwRef
#define m_pOuterUnknown m_pATLOuterUnknown
As ATL is a template library and nearly all it's code is in .h files, the solution can be
simple redefinition of conflicting names in stdafx.h file, placing them after
MFC includes and before ATL. By the way, this might not work in .NET environment.
Another problem appears at runtime. In case of dynamic linking MFC library any calls to control's
exposed interfaces, properties or methods may crash an application. This happens when
interface exposed function code makes a call to MFC library. MFC maintains internal state by
setting pointer to current thread state controlling data(see MSDN Tech Note 58). For any call across boundaries of application thread,
like calls to DLL or COM interfaces, MFC synchronizes the state with AFX_MANAGE_STATE(AfxGetStaticModuleState())
macro call. But wiht ATL support AfxGetStaticModuleState()
global function returns indefinite information.
It is known problem of managing of MFC internal state. And it is easily can be avoided with static linking of control libraries.
Which is exactly what we need in case of ActiveX control.
For detailed information on how to solve this problem with dynamic linking MFC library
see 'Using ATL to Automate a MFC Application' by Nick Hodapp.
Overall MFC and ATL integration subject well covered in wonderful article
'Com Toys' by Paul DiLascia.
Another way to make MFC & ATL work together is to use separate objects for UI presentation and automated
interface (proxy) objects instead of multiple inheritance. I used such architecture for the first version
of my docking control. It had two types of internal object; one was responsible for UI representation,
another for automation. But then things quickly got messed up with persistence and object's lifetime
synchronization. Eventually I gave up that idea.
Docking approach implementation
Docking controls have specific ability to align itself to the edges of a window or tear off (float).
Usually control can resize itself for the best view. MFC implementation also prompts the docking place.
As I already mention that MFC implementation of docking process involves at least two parties - control
itself and docking window. Usually the role of docking window plays the main window of application.
For ActiveX docking control the docking window would be the container's window, on which control is placed.
It is important for MFC docking process, that docking window would be a MFC CFrameWnd
derived object.
The only information about container's parent window ActiveX control can get, is window handle.
This handle provided through IOleInPlaceFrame::GetWindow
call as a part of OLE in-place activation process.
Further by subclassing window handle and attaching it to MFC CFrameWnd
class, the required docking window could
be obtained. Docking control should derive from MFC CControlBar
class to successfully complete docking
operations. So at creation time of docking control it makes a new instance of a CFrameWnd
class attaches
is to container's window handle and further makes necessary for docking steps as a regular MFC application does.
Everything seems to be working fine, but there are still some minor problems. Sometimes you can see
drawing artifacts after changing the docking state of control. The problem is that MFC has resource cleaning and UI updating mechanism, so called
ON_UPDATE_COMMAND_UI
mechanism (MFC technical note 31). Being called from application event loop in
CWinApp::OnIdle
, it handles UI cleanup and update, including docking state of controls. But there is no event
loop (Dispatch/TranslateMessage
) in ActiveX control (because it is a DLL). The solution is to emulate OnIdle
calls from timer.
Another problem appears during placing control on MDI (Multi Document Interface) frame. The position and size
of controls on MDI form have to be negotiated with container. In order to place control on MDI frame it is
necessary to negotiate for toolbar space. Otherwise control will be always obscured with a frame background.
Toolbar space negotiation code may look like this:
BOOL CICuteBar::OnResizeBorder(CFrameWnd* pFarme)
{
CRect rectBorder; rectBorder.SetRectEmpty();
VERIFY(m_spInPlaceFrame->GetBorder(&rectBorder) == S_OK);
CRect rectNeeded ( rectBorder);
pFrame->RepositionBars(0, 0xFFFF, 0, CWnd::reposQuery, &rectNeeded,&rectBorder);
CRect rectRequest( rectNeeded.left - rectBorder.left,rectNeeded.top - rectBorder.top,
rectBorder.right - rectNeeded.right,rectBorder.bottom - rectNeeded.bottom);
if ((!rectRequest.IsRectNull() || spInPlaceFrame->RequestBorderSpace(&rectRequest) == S_OK)
{
VERIFY(m_spInPlaceFrame->SetBorderSpace(&rectRequest) == S_OK);
pFrame->RepositionBars(0, 0xFFFF, 0, CWnd::reposDefault, NULL,&rectBorder);
}
else
return FALSE;
return TRUE;
}
Objects hierarchy and persistence
Usually docking window represents another controls, like buttons, combo boxes, labels, etc.
To be able to manage them from user application code, those controls should be seen as a
separate automated object. That means that ActiveX control should be able to hold and manage
inner objects. For that purposes object collections are used.
A collection is an automated object that manages the other objects in an ordered
(indexed) manner. Possible organization chart of such a control is shown on Pic1. The main object
represents ActiveX control and holds collection object of another (Bar) objects, which are the actually
docking UI components. In turn the Bar object holds ItemCollection object,
the collection of Item objects.
The ItemCollection manages Item tools objects, which in that case associated with buttons on toolbar.
Pic1.
The access to the contents of collection can be obtained by means of collection properties and methods
like Item()
, Add()
, Remove()
, Count()
programmatically or via UI,
using property page. Code of container's scripting language is able to access any leaf object,
cycling through the objects of collections.
Object Collections
A collection is an object that exposes at least two properties:
NewEnum()(DISPID = DISPID_NEWENUM)
returns enumerator object and Count()
returns number
of items in collection.
ATL provides support for collection management based on STL (Standard Template Library) container classes,
like vector
, list
, map
etc. Collection template classes cover implementation of
NewEnum
, Item
and Count
properties. Optional methods Add
, Remove
,
Clear
may be added and implemented in derived class. And what is most important, ATL implementation supports
IEnumXXX
enumeration interface, which is used for 'For Each' constructions in scripting languages
like Visual Basic, etc.
STL based classes ICollectionOnSTLImpl
and CComEnumOnSTL
implement collection and enumerator.
Those template classes are based on STL containers used for managing the instances of collection objects.
Also there are "copy policy" classes, which ATL uses for providing conversion of STL container data type to
exposed type in Item
and EnumInit
functions.
Some examples of simple collections using ATL, like BSTR strings or VARIANT collection objects are well covered
in "ATL internals" by Brent Rector, Chris Sells, Jim Springfield.
Below is example of collection objects definition. Unfortunately code is so overloaded with namespaces
that it becomes difficult to read. (For full implementation see attached project)
namespace BarColl
{
typedef CComObject<CIBar>* ContObj;
typedef std::vector< ContObj > ContainerType;
typedef VARIANT EnumeratorExposedType;
typedef IEnumVARIANT EnumeratorInterface;
typedef IBar* CollectionExposedType;
typedef IBarCollection CollectionInterface;
typedef VCUE::GenericCopy<EnumeratorExposedType, ContainerType::value_type> EnumeratorCopyType;
typedef VCUE::GenericCopy<CollectionExposedType, ContainerType::value_type> CollectionCopyType;
typedef CComEnumOnSTL< EnumeratorInterface, &__uuidof(EnumeratorInterface), EnumeratorExposedType,
EnumeratorCopyType, ContainerType > EnumeratorType;
typedef ICollectionOnSTLImpl< CollectionInterface, ContainerType, CollectionExposedType,
CollectionCopyType, EnumeratorType > CollectionType;
};
There are additional files for collection support that have not been included in original ATL package.
They come with Microsoft MSDN ATL collection samples. One of them is VCU_copy.h file that contains
definitions for copy template policy classes of the types: VARIANT to BSTR, BSTR to VARIANT
.
If contained data type is different from those types, it is necessary to define your own generic copy policy class.
In case of conversion form CComObject<CIBar>
to exposed IBar
automated interface it may look like these:
template <>HRESULT VCUE::GenericCopy::copy(destination_type* pTo, const source_type* pFrom)
{
HRESULT hr = E_INVALIDARG;
if (pFrom == NULL && *pFrom == NULL)
return hr;
return(*pFrom)->QueryInterface(IID_IDispatch,(void**)pTo);
};
To complete collection we need to add following functions: Add
, Remove
and Item
.
Add
and Remove
functions control the content of collection.
Item(Index)
returns collection data of exposed type. The VARIANT type Index
parameter
usually has an integer value. In many cases byte string (BSTR) indexes is the only or most effective way to manage
collection. For example, if we need to get access programmatically to an object that implements 'save'
button functionality we would be able to get it by text name "Save". Possible implementation example
of BSTR indexes overwrites ATL Item
property code with following:
STDMETHODIMP CIBarCollection::get_Item(VARIANT *Index, IBar **pVal)
{
if (Index->vt == VT_EMPTY ) return E_POINTER;
HRESULT hr = E_FAIL;
CComVariant var;
var = *Index;
if(var.vt == VT_BSTR)
{
CString Str(var.bstrVal);
BarColl::ContainerType::iterator iter = m_coll.begin();
while (iter != m_coll.end())
{
if(var.vt == VT_BSTR)
{
BarColl::ContObj pObj = *iter;
if (pObj->m_strName == Str) break;
}
iter++;
}
if (iter != m_coll.end())
hr = BarColl::CollectionCopyType::copy(pVal, &*iter);
return hr;
}
In general, container manages lifetime of AxtiveX control. Lifetime of inner objects and collections is maintained
within lifetime of control itself. ATL provides special mechanism for maintaining lifetime of aggregated
and inner objects. Typically collections as aggregated objects are created within FinalConstruct
of each
holding object and released in FinalRelease
. To avoid memory leaks all internal objects should be released
before control quits. Collection functions Add
, Remove
also manage the lifetime of objects.
Persistence scheme
Persistence is an essential part of COM technology. It is an ability of control to save and restore
internal state (data) to and from the persistent media. ActiveX control persistence is provided
by implementation of the following interfaces: IPersistStorage, IpesistStream(Init) and IPersistproperybag
.
ATL supports persistence through implementation of those interfaces in corresponding
IpersistStorageImpl, IpersistStreamImpl and IPersistProperyBagImpl
classes. ATL prsistence works for
control properties. Properties should be added to ATL "Property Map", which is basically a table
of property names, DISPIDs, and values. Once you have added properties to the property map, ATL knows how to persist them.
ATL persistence implementation provides support only for main control. Other collections data have to
be serialized by control itself. Objects organization plays key role in persistence. As an example,
I will take object's scheme described on Pic2.
Pic2.
As control persistence is supported with ATL implementation there is no need for additional
coding for IPersistStorage
and IPersistProperyBag
interfaces. Every object, including collection objects,
has to implement persistence interface IPesistStreamInit
. All objects, including main control, have to
implement Save
and Load
methods. For main control ATL based class should be called in order
to persist properties values from "Property Map".
For instance, saving process unfolds in following sequence: Process starts from main control IpersistStreamInit_Save
call initialized by ActiveX container. First in this function we initialize aggregated collection object persistence
by querying IPersistStreamInit
pointer. Once pointer is obtained it calls its Save
function
with the stream pointer passed. In a turn, collection object Save
function cascades the saving process by
walking through all inner objects, and further to the very last object. The last thing the based ATL class is being called for
control property persistence. Following code illustrates persistence:
HRESULT CICuteBar::IPersistStreamInit_Save(LPSTREAM pStm, BOOL fClearDirty, ATL_PROPMAP_ENTRY* pMap)
{
CComPtr spPersistStm;
HRESULT hr = m_spBarCollection->QueryInterface(&spPersistStm);
if(FAILED(hr))
return hr;
hr = MarkVersion(pStm);
if(FAILED(hr))
return hr;
hr = spPersistStm->Save(pStm,fClearDirty);
if(FAILED(hr))
return hr;
hr = IPersistStreamInitImpl<CICuteBar>::IPersistStreamInit_Save( pStm, fClearDirty, pMap);
return hr;
}
HRESULT CIBarCollection::IPersistStreamInit_Save(LPSTREAM pStm, BOOL fClearDirty, ATL_PROPMAP_ENTRY* pMap)
{
HRESULT hr = S_OK;
try
{
BarColl::ContainerType::iterator iter = m_coll.begin();
int size = m_coll.size();
pStm->Write(&size, sizeof(size),NULL);
while (iter != m_coll.end())
{
BarColl::ContObj pObj = *iter;
CComPtr spPersistStm;
if(FAILED(pObj->QueryInterface(&spPersistStm)))
{
bRet = FALSE;
break;
}
if(FAILED(spPersistStm->Save(pStm, fClearDirty)))
{
bRet = FALSE;
break;
}
iter++;
}
}
catch(CFileException* e)
{
e->Delete();
hr = E_FAIL;
}
return hr;
}
HRESULT CIBar::IPersistStreamInit_Save(LPSTREAM pStm, BOOL fClearDirty, ATL_PROPMAP_ENTRY* pMap)
{
HRESULT hr = S_OK;
hr = IPersistStreamInitImpl<CIBar>::IPersistStreamInit_Save( pStm, fClearDirty, pMap);
if(FAILED(hr))
return hr;
try
{
COleStreamFile File;
File.Attach(pStm);
CArchive ar(&File,CArchive::store);
Serialize(ar);
ar.Close();
File.Detach();
}
catch(CFileException* e)
{
e->Delete();
hr = E_FAIL;
}
if(FAILED(hr))
return hr;
CComPtr spPersistStm;
hr = m_pItemCol->QueryInterface(&spPersistStm);
if(FAILED(hr))
return hr;
return spPersistStm->Save(pStm,fClearDirty);
}
The loading process slightly differs from saving. Besides reading the data of the objects,
collection should recreate instances of the objects (with COM class factory) and then serialize
them particularly in the same order they have been saved.
MFC has it own powerful and simple mechanism for objects serialization. To make my work
easy I've decided to utilize this approach in my control. Everything to be serialized is put in one
function for both saving and loading processes. In MFC for serialization CArchive
class is being used.
So for each object I've added Serialize
function with CAchive
attached to a given persistence stream.
Of course, persistence could be done with more obvious Stream.Write()/Read()
functions.
Especially if you decide not to use MFC support you will not have a choice.
The code bellow illustrates using MFC for control serialization:
HRESULT CIBarCollection::IPersistStreamInit_Load(LPSTREAM pStm, ATL_PROPMAP_ENTRY* pMap)
{
HRESULT hr = S_OK;
try
{
if (!RestoreObjects(pStm))
hr = E_FAIL;
}
catch(CFileException* e)
{
e->Delete();
hr = E_FAIL;
}
return hr;
}
BOOL CIBarCollection::RestoreObjects(LPSTREAM pStm)
{
BOOL bRet = TRUE;
int size = 0;
pStm->Read(&size, sizeof(size),NULL);
for ( int i =0 ; i < size ; i++)
{
BarColl::ContObj pObj;
CComPtr pIBar;
HRESULT hr = CComObject<CIBar>::CreateInstance(pObj);
if (FAILED(hr))
return hr;
pObj->QueryInterface(&pIBar);
pObj->AddRef();
m_coll.push_back(pObj);
CComPtr spPersistStm;
if(FAILED( pIBar->QueryInterface(&spPersistStm)))
{
bRet = FALSE;
break;
}
if(FAILED(spPersistStm->Load(pStm)))
{
bRet = FALSE;
break;
}
}
return bRet;
}
Described above control persistence process may be used not only for serialization into container storage,
but also into a file as exchange or backup media. For example, this is how "Save" button handling code on
control property page initializes serialization control state to a file.
LRESULT CBarPropPage::OnClickedButton_save(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
USES_CONVERSION;
static char BASED_CODE lpszccFilter[] = "Cute Controls File (*.ccb)|*.ccb||";
TCHAR lpszExt[] = _T("ccb");
CFileDialog FileDlg(FALSE, lpszExt,NULL,OFN_HIDEREADONLY |OFN_OVERWRITEPROMPT,lpszccFilter,NULL );
int ret = FileDlg.DoModal();
if ( ret != IDOK)
return 0;
LPSTORAGE lpStorage;
SCODE sc = ::StgCreateDocfile(T2COLE(FileDlg.m_ofn.lpstrFile),
STGM_READWRITE|STGM_TRANSACTED|STGM_SHARE_DENY_WRITE|STGM_CREATE,
0, &lpStorage);
if (sc != S_OK)
{
AfxMessageBox(IDS_FILE_ERORR, MB_OK|MB_ICONSTOP);
return 0;
}
CComPtr spStream;
static LPCOLESTR vszContents = OLESTR("Contents");
HRESULT hr = lpStorage->CreateStream(vszContents,
STGM_READWRITE | STGM_SHARE_EXCLUSIVE | STGM_CREATE,
0, 0, &spStream);
if (FAILED(hr))
{
AfxMessageBox(IDS_FILE_ERORR, MB_OK|MB_ICONSTOP);
return 0;
}
CComPtr pObject;
hr = _GetMainControl()->ControlQueryInterface(IID_IPersistStreamInit, (void**)&pObject);
if (FAILED(hr))
{
AfxMessageBox(IDP_INTERLAL_ERROR, MB_OK|MB_ICONSTOP);
return 0;
}
pObject->Save(spStream, TRUE);
lpStorage->Commit(STGC_OVERWRITE);
return 0;
}
Known problems
One problem that I run into using that control is the consistent short-cut processing. Control's keyboard short-cut
processing mechanism is based on IOleInPlaceActiveObject
as an active object on a container's form. After setting
active IOleInPlaceActiveObject
object a direct channel of communication between an in-place object and
the associated application's most-outer frame window and the document window established. The problem is that VB
application has original VB menu, once user hits a menu VB resets active object and no message translation is possible
any more. After that control starts missing short-cuts. The solution could be further enhancement of control with adding docking
menu bar, which would replace original VB menus. Common message processing mechanism would work for all toolbars and
menu bar of control. Status bar realization would complete UI representation for control.
Conclusion
With new coming Microsoft .NET platform the practical usage of such a control may seem obsolete.
Docking capabilities of controls in .NET environment are built into framework. For example, VB.NET
already has standard control properties "dock" and "anchor". Nevertheless in some cases one would still prefer
using stand-alone control rather than .NET runtime, or use project as a starting point for own control. Solutions
being used in this project, like object collections and in-place window subclassing and many others, may be helpful
for other controls implementation.
There are number of opportunities to enhance and develop or even give a nice contemporary look to the control.
Another field of activity could be the docking approach. It can be modified for MS Word like docking or anything
we have not seen yet.
It is virtually impossible to cover all you can face while building docking ActiveX control in one small publication.
There are tons of small things left beyond the boundaries of this article, which can be found in the attached
MSVC project code of docking toolbar ActiveX control. There are also VB6.0 sample files that demonstrate how
control can be used. Comments and participations are appreciated. The latest source code is available
on www.activexstore.com site.