ActiveX

Docking ActiveX Controls: Principles and Implementation

디버그정 2008. 8. 6. 20:26

http://www.codeproject.com/KB/COM/cutecontrol.aspx

Sample Image

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:

Collapse
BOOL CICuteBar::OnResizeBorder(CFrameWnd* pFarme)
{ 
   // use IOleInPlaceUIWindow::GetBorder if no border given
    CRect rectBorder;    rectBorder.SetRectEmpty();
   VERIFY(m_spInPlaceFrame->GetBorder(&rectBorder) == S_OK);

   // see how much space we need by calling reposition bars
   CRect rectNeeded ( rectBorder);
   
   // request the border space from the container
   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) 
   {

   	// set the border space -- now this object owns it	
        VERIFY(m_spInPlaceFrame->SetBorderSpace(&rectRequest) == S_OK);

   	// move the bars into position after committing the space	
        pFrame->RepositionBars(0, 0xFFFF, 0, CWnd::reposDefault, NULL,&rectBorder);
    }
    else              
        return FALSE;   // likely Frame is not MDI, just quit    

    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.

Control Object Scheme

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
{
    // Store our data in a vector of COM objects            
    typedef CComObject<CIBar>*          ContObj;            
    typedef std::vector< ContObj >      ContainerType;

    // Use IEnumVARIANT as the enumerator for VB compatibility  
    typedef VARIANT                     EnumeratorExposedType;
    typedef IEnumVARIANT                EnumeratorInterface;

    // Our collection interface exposes the data as IBar    
    typedef IBar*                       CollectionExposedType;
    typedef IBarCollection              CollectionInterface;

    // Typedef the copy classes using existing typedefs
    typedef VCUE::GenericCopy<EnumeratorExposedType, ContainerType::value_type>     EnumeratorCopyType;
    typedef VCUE::GenericCopy<CollectionExposedType, ContainerType::value_type>     CollectionCopyType; 
    
    // Now we have all the information we need to fill in the template arguments on the implementation classes  
    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)
    {
      // find item by Name        
      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.

Control Persistence Scheme

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:

Collapse
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;   
 
 //--- Save version information    
 hr = MarkVersion(pStm);     
 if(FAILED(hr))        
    return hr;
 
 //--- Do collection persistence    
 hr =  spPersistStm->Save(pStm,fClearDirty);    
 if(FAILED(hr))        
    return hr;
 
 //--- Save Control properties (ATL implementation)    
 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    
 {
 //--- Walk through all objects in collection for saving      
 BarColl::ContainerType::iterator iter = m_coll.begin();    
 // Save number of objects     
 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++;      
      }//= while    
 }    
 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();   // Flush Stream and set pointer back to written position        
     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:
Collapse
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;    
    
    // Get number of objects to read    
    pStm->Read(&size, sizeof(size),NULL);        

    for ( int i =0 ; i < size ; i++)   
    {        
        BarColl::ContObj  pObj;
        CComPtr pIBar;
        // Use COM class factory for object creation        
        HRESULT hr = CComObject<CIBar>::CreateInstance(pObj);        
        if (FAILED(hr))            
            return hr; 
                   
        pObj->QueryInterface(&pIBar);        
        pObj->AddRef(); // Prevent from deleting object after leaving function        

        m_coll.push_back(pObj);        
        CComPtr spPersistStm;        
        if(FAILED( pIBar->QueryInterface(&spPersistStm)))        
        {
            bRet = FALSE;            
            break;        
        }        
        if(FAILED(spPersistStm->Load(pStm)))        
        {            
            bRet = FALSE;            
            break;        
        }
      } //for    
      
      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.

Collapse
LRESULT CBarPropPage::OnClickedButton_save(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{     
    USES_CONVERSION;
    // File broswse dialog     
    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;

     // Create Storage from File    
     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;    
     }

     // Open stream for serialization
    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;    
    }

    // Get control's IPersistStreamInit    
    CComPtr pObject;        
    hr = _GetMainControl()->ControlQueryInterface(IID_IPersistStreamInit, (void**)&pObject);     
    if (FAILED(hr))    
    {       
        AfxMessageBox(IDP_INTERLAL_ERROR, MB_OK|MB_ICONSTOP);        
        return 0;    
    }
   
    // Serialize control    
    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Dmitri Sviridov



Location: United States United States

Other popular COM / COM+ articles: