65.9K
CodeProject 正在变化。 阅读更多。
Home

停靠 ActiveX 控件: 原理和实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (7投票s)

2001年6月10日

15分钟阅读

viewsIcon

186600

downloadIcon

2931

本文介绍了如何使用 MFC 和 ATL 实现停靠 ActiveX 控件

Sample Image

引言

几乎所有 MFC VC++ 程序员都知道,制作一个带有可停靠工具栏的简单应用程序是一项非常容易的任务。基本上,只需一次点击即可完成。MFC 应用程序向导会完成所有工作,编码本身仅限于添加图像和填充 WM_COMMAND 处理程序。当您希望为可停靠控件实现类似 MS Dev Studio - MS Word - Outlook 等炫酷的外观时,事情就会变得复杂起来。很好的例子是此类库的源代码,例如 Kirk Stowel 的 Xtreme ToolkitStas Levin 的 BCGSoft 库。然而,我试图解决的问题是创建一个具有可停靠功能的 ActiveX 控件。这将使 Visual Basic 应用程序具有与 MFC 应用程序相同的外观。在本文中,我将展示如何使用 ATL 和 MFC 实现可停靠 ActiveX 控件。文章还附带 MS VC++ 6.0 项目和 Visual Basic (VB) 示例,说明了控件的用法。

窗口停靠控件与 ActiveX 停靠控件

停靠控件的特定功能是维护用户鼠标拖动操作的停靠状态,例如能够将其对齐到应用程序窗口的边缘或浮动。MFC 的停靠功能在 CControlBar 类中得到支持,该类充当停靠控件本身的角色,而 CFrameWnd 类则实现要停靠到的窗口。通常,MFC 应用程序的主窗口派生自 CFrameWnd 类。停靠过程通过这两个类之间的协商对话框执行。它以通过为两个类指定停靠样式来启用停靠功能开始。然后 ControlBarCFrameWnd 类(DockControlBar 函数)中注册自身,提供一个指针。因此,当发生停靠时,CFrameWnd 会知道要停靠哪个控件以及在哪个位置。协商发生在每次停靠控件的鼠标移动时,包括检查控件的大小以确定可能的停靠位置。

另一方面,ActiveX 控件不是应用程序。它是 DLL 形式的 COM 对象,实现了用于可视化表示、持久化和自动化的 COM 接口。将 ActiveX 控件放置在容器窗体上的过程称为“就地激活”或“嵌入”。另一个主要区别是包含应用程序窗口与控件通信的方式。容器通过与控件的 COM 接口协商,提供放置控件在窗体上的所有必要信息,包括用于控件创建子窗口的父窗口句柄。最初,Microsoft 没有在容器-ActiveX 控件协商对话框中提供任何停靠功能。后来,他们在 Internet Explorer 中引入了 IDockingWindow 接口,但不是作为 OLE 框架(ActiveX 技术整个技术都基于此)的一部分。容器完全控制就地激活的 ActiveX 控件的可视化表示,包括大小和位置。放置 ActiveX 控件的框架由容器提供,无法提前确定它是 MDI 应用程序还是 SDI 应用程序。

除了控件实现的 OLE 部分之外,主要问题是找到实现停靠窗口和停靠控件的方法。我将整个过程分为以下几个问题:

· 必要的 COM 接口实现。与常规控件的区别。
· 停靠方法实现;停靠框架和停靠窗口;ATL 和 MFC 库集成。
· 自动化集合的实现。
· 控件持久化。

基本 COM 实现

让我们在 MSVC 6.0 IDE 中开始一个新的 ATL COM 项目。使用向导,在“添加 ATL 对象”向导对话框中添加“完整控件”,并具有以下属性:线程模型 - 单元;接口 - 双重;支持 连接点接口;杂项状态 - “运行时不可见”“充当标签”“充当标签”用于能够将控件放置在 MDI 框架上。“连接点接口”“双重接口”分别对事件处理和自动化支持是必需的。而“运行时不可见”使 ActiveX 控件无窗口。最后一个值得更详细的解释。容器对就地激活的 ActiveX 控件的大小和位置负责。相反,停靠控件倾向于自己维护其大小和位置,并能够根据相对于停靠窗口的相对位置对齐到停靠窗口的边缘或浮动。因此,使控件在运行时不可见将允许控件在内部维护状态,而无需容器的监督。

从向导生成的代码可以看出,ATL 实现了很多必需的 ActiveX 控件接口。还需要添加一些接口实现和重写函数。其中包括用于属性持久化的 IpersistPropertyBag 接口及其重写的以下函数:IpersistPropertyBag::Load(), IpersistPropertyBag::Save(), IpersistStreamInit::Load(), IpersistStreamInit::Save()。函数 FinalConstruct(), FinalRelease() 用于创建/释放聚合的集合对象。

IOleObject::SetClientSite(IOleClientSite *pClientSite) 实现对于设置键盘快捷键处理很重要。这是设置活动对象的地方,该对象将在分派它们之前接收容器的键盘处理消息。

最后,由于控件仅在设计时可见,因此 OnDraw() 函数仅在设计模式下调用。例如,可以在此处绘制就地控件图标。

ATL 和 MFC

对于控件开发来说,使用 MFC 进行 GUI,使用 ATL 3.0 进行 COM 和自动化支持是明智的选择。在我的项目中,我尝试结合这两个库的功能。

我遇到的第一个问题是多重继承自 ATL CComObjectRootEx 类和任何 MFC CWnd 类。这对于像 Bar 这样的对象是必需的,它们同时代表窗口控件“工具栏”和 COM 对象。ATL 使用与 MFC 相同的命名约定来实现 COM 支持。MFC CCmdTarget 类,CWnd 的父类,具有内置的 COM 支持并包含所有冲突的名称。我使用了重定义冲突名称来解决这个问题。

#define InternalAddRef          IntATLAddRef
#define InternalRelease         IntATLRelease
#define InternalQueryInterface  IntATLQueryInterface
#define m_dwRef                 m_ATLdwRef
#define m_pOuterUnknown         m_pATLOuterUnknown

由于 ATL 是一个模板库,几乎所有的代码都在 .h 文件中,解决方案可以在 stdafx.h 文件中简单地重定义冲突的名称,将它们放在 MFC include 之后,ATL 之前。顺便说一句,这在 .NET 环境中可能不起作用。

另一个问题出现在运行时。如果动态链接 MFC 库,任何对控件暴露的接口、属性或方法的调用都可能导致应用程序崩溃。这是因为接口暴露的函数代码调用了 MFC 库。MFC 通过设置指向当前线程状态控制数据的指针来维护内部状态(参见 MSDN Tech Note 58)。对于任何跨越应用程序线程边界的调用,例如对 DLL 或 COM 接口的调用,MFC 使用 AFX_MANAGE_STATE(AfxGetStaticModuleState()) 宏调用来同步状态。但是,使用 ATL 支持时,AfxGetStaticModuleState() 全局函数返回不确定的信息。这是管理 MFC 内部状态的一个已知问题。可以通过静态链接控件库轻松避免。这正是我们在 ActiveX 控件情况下所需要的。有关如何使用动态链接 MFC 库解决此问题的详细信息,请参阅 Nick Hodapp 的《使用 ATL 自动化 MFC 应用程序》'Using ATL to Automate a MFC Application' by Nick Hodapp。总体而言,MFC 和 ATL 集成主题在 Paul DiLascia 的精彩文章《Com Toys》 'Com Toys' by Paul DiLascia 中得到了很好的涵盖。

使 MFC 和 ATL 协同工作的另一种方法是使用单独的对象来进行 UI 表示和自动化接口(代理)对象,而不是使用多重继承。我在我的可停靠控件的第一个版本中使用了这种架构。它有两种内部对象:一种负责 UI 表示,另一种负责自动化。但随后持久化和对象生命周期同步很快就变得一团糟。最终我放弃了这个想法。

停靠方法实现

停靠控件具有将其对齐到窗口边缘或分离(浮动)的特定能力。通常控件可以调整自身大小以获得最佳视图。MFC 实现也提示了停靠位置。如前所述,MFC 停靠过程的实现至少涉及两个方面——控件本身和停靠窗口。通常,停靠窗口的角色由应用程序的主窗口扮演。

对于 ActiveX 停靠控件,停靠窗口将是控件放置在其上的容器的窗口。对于 MFC 停靠过程来说,停靠窗口是一个派生自 MFC CFrameWnd 的对象是很重要的。ActiveX 控件能获得的关于容器父窗口的唯一信息是窗口句柄。此句柄通过 IOleInPlaceFrame::GetWindow 调用提供,作为 OLE 就地激活过程的一部分。进一步通过子类化窗口句柄并将其附加到 MFC CFrameWnd 类,就可以获得所需的停靠窗口。停靠控件应派生自 MFC CControlBar 类,以成功完成停靠操作。因此,在创建停靠控件时,它会创建一个 CFrameWnd 类的新实例,将其附加到容器的窗口句柄,然后执行常规 MFC 应用程序所需的停靠步骤。

一切似乎都运行良好,但仍存在一些小问题。有时在更改控件的停靠状态后,您可能会看到绘图伪影。问题在于 MFC 具有资源清理和 UI 更新机制,称为 ON_UPDATE_COMMAND_UI 机制(MFC 技术注释 31)。它从 CWinApp::OnIdle 的应用程序事件循环中调用,处理 UI 清理和更新,包括控件的停靠状态。但 ActiveX 控件没有事件循环(Dispatch/TranslateMessage)(因为它是一个 DLL)。解决方案是从定时器模拟 OnIdle 调用。

在 MDI(多文档界面)框架上放置控件时会出现另一个问题。MDI 窗体上控件的位置和大小必须与容器协商。为了将控件放置在 MDI 框架上,有必要协商工具栏空间。否则,控件将始终被框架背景遮盖。工具栏空间协商代码可能如下所示:

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;
}

对象层次结构和持久化

通常,停靠窗口表示其他控件,例如按钮组合框标签等。为了能够从用户应用程序代码管理它们,这些控件应被视为单独的自动化对象。这意味着 ActiveX 控件应该能够持有和管理内部对象。为此目的使用了对象集合。

集合是一个自动化对象,它以有序(索引)的方式管理其他对象。这样的控件的可能组织图在图 1 中显示。主对象代表 ActiveX 控件,并持有另一个(Bar)对象的集合对象,这些对象实际上是停靠的 UI 组件。反过来,Bar 对象持有 ItemCollection 对象,这是 Item 对象的集合。ItemCollection 管理 Item 工具对象,在本例中与工具栏上的按钮相关联。

Control Object Scheme

图 1.

可以通过集合的属性和方法,如 Item()Add()Remove()Count(),以编程方式或通过 UI 使用属性页来访问集合的内容。容器脚本语言的代码能够访问任何叶对象,通过遍历集合的对象。

对象集合

集合是一个至少公开两个属性的对象:NewEnum() (DISPID = DISPID_NEWENUM) 返回枚举器对象,Count() 返回集合中的项目数。

ATL 支持基于 STL(标准模板库)容器类(如 vectorlistmap 等)的集合管理。集合模板类涵盖了 NewEnumItemCount 属性的实现。可选方法 AddRemoveClear 可以在派生类中添加和实现。最重要的是,ATL 实现支持 IEnumXXX 枚举接口,该接口用于 Visual Basic 等脚本语言中的“For Each”构造。

基于 STL 的类 ICollectionOnSTLImplCComEnumOnSTL 实现集合和枚举器。这些模板类基于 STL 容器,用于管理集合对象的实例。此外,还有“复制策略”类,ATL 使用它们在 ItemEnumInit 函数中提供 STL 容器数据类型到暴露类型的转换。

简单的集合示例,例如使用 ATL 的 BSTR 字符串或 VARIANT 集合对象,在 Brent Rector、Chris Sells、Jim Springfield 的《ATL internals》中得到了很好的介绍。
下面是集合对象定义的示例。不幸的是,代码被命名空间过度加载,难以阅读。(完整实现请参见附带的项目)

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;
};

还有用于集合支持的附加文件,这些文件未包含在原始 ATL 包中。它们随 Microsoft MSDN ATL 集合示例一起提供。其中之一是 VCU_copy.h 文件,其中包含以下类型的复制模板策略类的定义:VARIANT 到 BSTR,BSTR 到 VARIANT。如果包含的数据类型与这些类型不同,则需要定义自己的通用复制策略类。在从 CComObject<CIBar> 转换为暴露的 IBar 自动化接口的情况下,它可能看起来像这样:

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);
};

为了完成集合,我们需要添加以下函数:AddRemoveItemAddRemove 函数控制集合的内容。Item(Index) 以暴露的类型返回集合数据。 VARIANT 类型 Index 参数通常具有整数值。在许多情况下,字节字符串(BSTR)索引是管理集合的唯一或最有效的方式。例如,如果我们想以编程方式访问实现“保存”按钮功能的对象的权限,我们可以通过文本名称“Save”来获取它。BSTR 索引的可能实现示例通过以下方式重写了 ATL Item 属性代码:

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;
} 

总的来说,容器管理 ActiveX 控件的生命周期。内部对象和集合的生命周期维持在控件本身生命周期之内。ATL 为维护聚合对象和内部对象提供了特殊的机制。通常,集合作为聚合对象在每个持有对象的 FinalConstruct 中创建,并在 FinalRelease 中释放。为了避免内存泄漏,所有内部对象都应在控件退出之前释放。集合函数 AddRemove 也管理对象生命周期。

持久化方案

持久化是 COM 技术的重要组成部分。它是控件将内部状态(数据)保存到持久介质并从中恢复的能力。ActiveX 控件持久化通过实现以下接口提供:IPersistStorage, IpesistStream(Init) 和 IPersistproperybag。ATL 通过在其相应的 IpersistStorageImpl, IpersistStreamImpl 和 IPersistProperyBagImpl 类中实现这些接口来支持持久化。ATL 持久化适用于控件属性。属性应添加到 ATL 的“属性映射”中,它基本上是一个属性名称、DISPID 和值的表。一旦将属性添加到属性映射中,ATL 就会知道如何持久化它们。

ATL 持久化实现仅为主控件提供支持。其他集合数据必须由控件本身序列化。对象组织在持久化中起着关键作用。例如,我将采用图 2 中描述的对象方案。

Control Persistence Scheme

图 2.

由于 ATL 实现支持控件持久化,因此无需为 IPersistStorageIPersistProperyBag 接口进行额外编码。每个对象(包括集合对象)都必须实现持久化接口 IPesistStreamInit。所有对象(包括主控件)都必须实现 SaveLoad 方法。对于主控件,应调用基于 ATL 的类来持久化“属性映射”中的属性值。

例如,保存过程按以下顺序展开:进程从主控件 IpersistStreamInit_Save 调用开始,由 ActiveX 容器初始化。首先,在此函数中,我们通过查询 IPersistStreamInit 指针来初始化聚合集合对象的持久化。一旦获得指针,它就会调用其 Save 函数,并将流指针传递给它。反过来,集合对象的 Save 函数通过遍历所有内部对象来级联保存过程,直至最后一个对象。最后调用基于 ATL 的类来持久化控件属性。以下代码说明了持久化:

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);
}

加载过程与保存过程略有不同。除了读取对象数据外,集合还应使用 COM 类工厂重新创建对象实例,然后专门按照保存的顺序序列化它们。

MFC 拥有自己强大且简单的对象序列化机制。为了方便我的工作,我决定在我的控件中使用这种方法。所有需要序列化的内容都放在一个函数中,用于保存和加载过程。在 MFC 中,序列化使用 CArchive 类。因此,对于每个对象,我添加了一个 Serialize 函数,并将 CAchive 附加到给定的持久化流。当然,可以使用更直观的 Stream.Write()/Read() 函数来实现持久化。特别是如果您决定不使用 MFC 支持,您将别无选择。

下面的代码说明了使用 MFC 进行控件序列化:
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;
}

上面描述的控件持久化过程不仅可用于序列化到容器存储,还可用于序列化到文件作为交换或备份介质。例如,控件属性页上的“保存”按钮处理代码就是这样初始化控件状态的序列化到文件的。

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;
   }

已知问题

在使用该控件时,我遇到的一个问题是一致的快捷键处理。控件的键盘快捷键处理机制基于 IOleInPlaceActiveObject,它是容器窗体上的活动对象。设置活动 IOleInPlaceActiveObject 对象后,就在就地对象和关联应用程序的最外层框架窗口以及文档窗口之间建立了直接通信通道。问题是 VB 应用程序具有原始的 VB 菜单,一旦用户点击菜单,VB 就会重置活动对象,不再可能进行消息翻译。之后,控件开始丢失快捷键。解决方案可能是通过添加可停靠菜单栏来进一步增强控件,这将替换原始 VB 菜单。通用的消息处理机制将适用于控件的所有工具栏和菜单栏。状态栏的实现将完成控件的用户界面表示。

结论

随着新的 Microsoft .NET 平台的到来,这种控件的实际使用可能显得过时。在 .NET 环境中,控件的停靠功能已内置到框架中。例如,VB.NET 已经具有标准的控件属性“dock”“anchor”。然而,在某些情况下,人们仍然更倾向于使用独立控件而不是 .NET 运行时,或者将项目用作自己控件的起点。本项目中使用的方法,如对象集合和就地窗口子类化以及许多其他方法,可能有助于实现其他控件。

有许多机会可以增强和开发,甚至为控件提供一个不错的现代外观。另一个活动领域可能是停靠方法。它可以修改为类似 MS Word 的停靠,或者任何我们尚未见过的内容。
在一个小出版物中涵盖构建可停靠 ActiveX 控件时可能遇到的所有内容几乎是不可能的。本文的范围之外还有大量细节,这些细节可以在附带的停靠工具栏 ActiveX 控件的 MSVC 项目代码中找到。还有 VB6.0 示例文件,演示了控件的用法。欢迎评论和参与。最新源代码可在 www.activexstore.com 网站上获取。

© . All rights reserved.