使用属性化 ATL 构建丰富的 COM 组件






4.95/5 (29投票s)
2004年1月27日
10分钟阅读

92142

918
这是一篇关于如何使用属性化 ATL 构建功能丰富的组件的教程文章。
引言
有很多关于如何使用 ATL 做事的文章和示例代码。通常它们只教你如何为组件添加功能,你需要查阅很多教程才能构建一个功能丰富的组件。
在这篇文章中,我将介绍如何创建 COM 服务器,将其暴露给脚本语言,使其成为事件源,为对象添加一个 VB 风格的集合,以及为你的对象添加报告错误的能力。
我并未将本文的目标定为涵盖 COM 或属性化 ATL 的所有问题,所以不要期望在这里找到每个属性或 COM 基本知识的解释。有关更详细的信息,请参阅 MSDN。本文只是一个快速的指南,介绍如何使你的 COM 对象对其他程序员更友好。
准备就绪
我们将通过示例来学习。示例非常简单——Windows® 服务管理器。服务管理器本身将是我们用属性化 ATL 编写的 COM 对象,同时还会有一组 VBScript 脚本,允许我们批量管理服务。
Start
项目中的 Coclasses
在创建程序时,我们应该考虑将拥有哪些类。在这里,我们将拥有管理器本身、服务集合和服务。我们讨论的是 COM,所以它们将是我们的 coclasses。
ServicesMgr
coclass 将为用户提供对 Services
集合以及按名称标识的服务的一系列操作。Services
集合将允许用户使用 foreach
语句迭代服务。Service
coclass 将代表单个服务。
创建一个项目
要开始一个 ATL 项目,请运行 Visual Studio .NET IDE 并选择 文件/新建/项目... 命令。选择 Visual C++ 项目/ATL/ATL 项目 并输入名称。在本教程中,我将使用名称 "ServicesManager"。
不要更改 ATL 项目向导 中的任何选项。保持为 属性化 和 动态链接库。点击 完成 — 好了!
现在我们有了一个虚拟的 COM 对象。它可以编译,但目前还什么都不做。
打开 ServicesManager.cpp 文件。注意其中的 [module...]
行。这是一个属性。它定义了库块。这意味着我们拥有 DllMain
、DllRegisterServer
和 DllUnregisterServer
函数,而无需编写任何代码。
添加 coclasses
让我们将 coclasses 添加到项目中。
在 解决方案资源管理器 中右键单击 ServicesManager 项目,然后选择 添加/添加类。然后在 添加类 - ServicesManager 窗口中选择 ATL/ATL 简单对象。在 ATL 简单对象向导 中输入 ServicesMgr 作为名称。将下一页的所有选项保留原样。注意 双重接口 选项已选中。这将帮助我们为 C++ 等使用 VTBL 绑定方法的语言以及使用 IDispatch
接口与对象通信的脚本语言提供 ServicesMgr
的功能。
在向导窗口中点击 完成,即可获得我们 ServicesMgr
coclass 所需的所有代码。
现在,找到 ServicesMgr.h 文件中的 IServicesMgr
接口声明,并向此接口添加以下属性:oleautomation
、hidden
和 nonextensible
,使其看起来像这样
[
object,
uuid("2543548B-EFFB-4CB4-B2ED-9D3931A2527D"),
dual,
oleautomation,
nonextensible,
hidden,
helpstring("IServicesMgr Interface"),
pointer_default(unique)
]
__interface IServicesMgr : IDispatch
{
};
向接口添加这些属性将使其与 OLE 自动化兼容,在面向用户的对象浏览器中隐藏(仅为节省用户时间),并禁止用户在运行时向此接口添加属性或方法。
再注意一点:我们所有操作都在 C++ 代码中进行。我们不用操心 IDL 等事宜。
重复上述步骤以添加 coclasses smServices
(我们不能使用 Services
这个名字,因为它是一个系统命名空间的名字)和 smService
。向 smServices
和 smService
这两个 coclasses 添加 noncreatable
属性。这将阻止用户创建这些对象。
添加功能
让我们向 ServicesMgr
coclass 添加 Start()
和 Stop()
方法。右键单击 类视图 中的 IServiceMrg
节点,然后选择 添加/添加方法。将方法名设置为 Start
,并添加一个 BSTR [in]
参数,名为 ServiceName
。对 Stop()
方法也执行相同的操作。你应该得到以下代码
__interface IServicesMgr : IDispatch
{
[id(1), helpstring("method Start")] HRESULT Start([in] BSTR ServiceName);
[id(2), helpstring("method Stop")] HRESULT Stop([in] BSTR ServiceName);
};
向导还会为 coclass 添加相应的声明,并为你提供该方法的默认实现。编辑 helpstring
属性以提供更有用的用户提示。
注意每个方法旁边的 id
属性。它设置了方法的 dispatch ID。通过使用此属性,我们无需手动编写任何 dispatching 代码 — 一切都将由编译器完成。
为了简化测试,就这样“实现”这些方法
STDMETHODIMP CServicesMgr::Start(BSTR ServiceName)
{
Beep(400, 100);
return S_OK;
}
STDMETHODIMP CServicesMgr::Stop(BSTR ServiceName)
{
Beep(1000, 100);
return S_OK;
}
构建项目并运行以下脚本进行测试
Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
Mgr.Start("SomeSvc")
MsgBox "Started!"
Mgr.Stop("SomeSvc")
如果你一切顺利,你将先听到一声蜂鸣,然后看到消息框,最后再次听到一声蜂鸣。
注意,我们使用脚本测试我们的对象,因此它暴露给了脚本语言。同时也要注意,我们只用了很少的精力就完成了这一切,通过使用 dual
属性,让我们的接口继承自 IDispatch
,并为方法使用 id
属性。
使用名称启动和停止服务是个好主意,但用户怎么知道这些名称呢?我们应该提供迭代服务名称的能力,以便获取所有可用的名称。
Services Coclass
根据文章 "Building COM Components That Take Full Advantage of Visual Basic and Scripting",我们应该实现一个具有 2 个方法和 1 个属性的接口 — _NewEnum()
方法、Item
属性和 Count()
方法。这些方法具有特殊的 dispatch ID 代码,因此调用者将知道它们的用途。注意下划线开头的 _NewEnum()
方法。这意味着该方法对用户是不可见的。
因此,我们的 IsmServices
应该具有这些方法和属性
[
object,
uuid("5BB63796-959D-412D-B94C-30B3EB8D97F1"),
dual,
oleautomation,
hidden,
nonextensible,
helpstring("IsmServices Interface"),
pointer_default(unique)
]
__interface IsmServices : IDispatch
{
[propget, id(DISPID_VALUE),
helpstring("Returns a service referred by name or index")]
HRESULT Item([in] VARIANT Index, [out, retval] IsmService** ppVal);
[id(1), helpstring("Returns number of services")]
HRESULT Count([out,retval] LONG* plCount);
[id(DISPID_NEWENUM), helpstring("method _NewEnum")]
HRESULT _NewEnum([out,retval] IUnknown** ppUnk);
};
注意属性 Item
和方法 _NewEnum()
使用特殊的 DISPID
标识符。这很重要。
我们决定由 coclass smServices
执行服务枚举,但另一方面,提供 Services
属性的 coclass ServicesMgr
具有启动和停止服务的方法。那么,将 Start()
和 Stop()
方法委托给 smServices
会是个好主意。但这会导致 smServices
coclass 的声明有点棘手。
现在 smServices
实现 IsmServices
接口。删除此声明并替换为以下内容
class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
BEGIN_COM_MAP(CsmServices)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IsmServices)
END_COM_MAP()
...
};
这将为我们提供 IDispatch
接口的默认实现,并将 IDispatch
和 IsmServices
接口都暴露给客户端。
现在我们可以使用以下构造实例化 smServices
coclass(这将为 smServices
实现 IUnknown
接口)
CComObject<CsmServices> Services;
对 smService
coclass 重复上述步骤。
然后创建一个 typedef
并向 ServicesMgr
coclass 添加一个声明
typedef CComObject<CsmServices> CServices;
class ATL_NO_VTABLE CServicesMgr :
public IServicesMgr
{
private:
CServices *m_pServices;
public:
CServicesMgr()
{
if (SUCCEEDED(CServices::CreateInstance(&m_pServices)))
m_pServices->AddRef();
}
void FinalRelease()
{
if (m_pServices)
m_pServices->Release();
}
...
};
最后,向 ServicesMgr
coclass 添加 Services
属性
__interface IServicesMgr : IDispatch
{
[id(1), helpstring("method Start")] HRESULT Start([in] BSTR ServiceName);
[id(2), helpstring("method Stop")] HRESULT Stop([in] BSTR ServiceName);
[propget, id(3), helpstring("Collection of available services")]
HRESULT Services([out, retval] IsmServices** ppVal);
};
现在向我们的 smServices
coclass 添加 EnumServices()
方法(不在接口中!)
typedef std::vector<_Service> _Services;
class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
...
private:
_Services m_Services;
public:
STDMETHOD(EnumServices)();
...
};
STDMETHODIMP CsmServices::EnumServices()
{
// Populate m_Services here
return S_OK;
}
并实现 ServicesMgr
的 get_Services()
方法
STDMETHODIMP CServicesMgr::get_Services(IsmServices** ppVal)
{
if (m_pServices)
{
// Make sure we enumerated services
HRESULT hr = m_pServices->EnumServices();
if (SUCCEEDED(hr))
return m_pServices->QueryInterface(ppVal);
else
return hr;
}
return E_FAIL;
}
我们已经填充了 CsmServices
coclass 的方法,而没有触及客户端用于枚举的接口。客户端无需直接调用 EnumServices()
。
现在以同样的方式向 smServices
coclass 添加 Start()
和 Stop()
方法,并将它们的实现从 ServicesMrg
coclass 移动过来。
启用集合迭代
为了支持集合迭代行为(For Each ... Next
),我们应该实现 CsmServices
的 _NewEnum()
方法。该方法应返回一个新的枚举集合的对象。该对象应实现 IEnumVARIANT
接口。
让我们创建 CsmServicesEnum
类。此类将从 CsmServices
复制服务列表,并使用户能够迭代它。服务列表应该被复制,因为如果用户同时运行两个枚举,我们将需要独立处理它们。
向项目添加一个新的 ATL 简单对象。将其命名为 smServicesEnum
。它不需要自定义接口,所以删除 IsmServicesEnum
接口声明,并更改 CsmServicesEnum
类的声明,然后填充 IEnumVARIANT
接口方法。
class ATL_NO_VTABLE CsmServicesEnum
: public CComObjectRoot
, IEnumVARIANT
{
BEGIN_COM_MAP(CsmServicesEnum)
COM_INTERFACE_ENTRY(IEnumVARIANT)
END_COM_MAP()
...
public:
STDMETHOD(Next)(unsigned long celt,
VARIANT *rgvar, unsigned long *pceltFetched);
STDMETHOD(Skip)(unsigned long celt);
STDMETHOD(Reset)();
STDMETHOD(Clone)(IEnumVARIANT **ppenum);
};
别忘了添加 typedef
以便实例化对象。
typedef CComObject<CsmServicesEnum> CServicesEnum;
Next()
方法将获取集合中的 celt
个元素,Skip()
将跳过一定数量的项目,Reset()
方法将枚举状态重置到初始状态,而 Clone()
方法应该创建当前枚举状态的副本。
我们的枚举器必须保存服务的副本和枚举的当前状态。
class ATL_NO_VTABLE CsmServicesEnum
: public CComObjectRoot
, IEnumVARIANT
{
...
private:
_Services m_Services;
int m_Idx;
public:
CsmServicesEnum()
: m_Idx(0)
{
}
void CloneServices(const _Services *pServices)
{
m_Services.assign(pServices->begin(), pServices->end());
m_Idx = 0;
}
...
};
那么,smServices
的 _NewEnum()
方法将如下所示
STDMETHODIMP CsmServices::_NewEnum(IUnknown** ppUnk)
{
CServicesEnum *pEnum;
CServicesEnum::CreateInstance(&pEnum);
pEnum->AddRef();
pEnum->CloneServices(&m_Services);
HRESULT hr = pEnum->QueryInterface(ppUnk);
pEnum->Release();
return hr;
}
现在我们可以实现我们枚举器的方法了。
STDMETHODIMP CsmServicesEnum::Next(unsigned long celt,
VARIANT *rgvar, unsigned long *pceltFetched)
{
if (pceltFetched)
*pceltFetched = 0;
if (!rgvar)
return E_INVALIDARG;
for (int i = 0; i < celt; i++)
VariantInit(&rgvar[i]);
unsigned long fetched = 0;
while (m_Idx < m_Services.size() && fetched < celt)
{
rgvar[fetched].vt = VT_DISPATCH;
// Create and initialize service objects
CService *pService;
CService::CreateInstance(&pService);
pService->AddRef();
pService->Init(m_Services[m_Idx]);
HRESULT hr = pService->QueryInterface(&rgvar[fetched].pdispVal);
pService->Release();
if (FAILED(hr))
break;
m_Idx++;
fetched++;
}
if (pceltFetched)
*pceltFetched = fetched;
return (celt == fetched) ? S_OK : S_FALSE;
}
STDMETHODIMP CsmServicesEnum::Skip(unsigned long celt)
{
unsigned long i = 0;
while (m_Idx < m_Services.size() && i < celt)
{
m_Idx++;
i++;
}
return (celt == i) ? S_OK : S_FALSE;
}
STDMETHODIMP CsmServicesEnum::Reset()
{
m_Idx = 0;
return S_OK;
}
STDMETHODIMP CsmServicesEnum::Clone(IEnumVARIANT **ppenum)
{
CServicesEnum *pEnum;
CServicesEnum::CreateInstance(&pEnum);
pEnum->AddRef();
pEnum->CloneServices(&m_Services);
HRESULT hr = pEnum->QueryInterface(ppenum);
pEnum->Release();
return hr;
}
为了测试我们的枚举器,请实现 smService
coclass 的 Name
和 DisplayName
属性。
STDMETHODIMP CsmService::get_Name(BSTR* pVal)
{
*pVal = m_Service.Name.AllocSysString();
return S_OK;
}
STDMETHODIMP CsmService::get_DisplayName(BSTR* pVal)
{
*pVal = m_Service.DisplayName.AllocSysString();
return S_OK;
}
现在我们可以编写一个简单的测试脚本
Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
WScript.Echo Mgr.Services.Count
Dim Service
For Each Service In Mgr.Services
WScript.Echo Service.DisplayName
Next
要完成集合支持,只剩下一件事。那就是 Item
属性。
STDMETHODIMP CsmServices::get_Item(VARIANT Index, IsmService** ppVal)
{
_Service svc;
*ppVal = 0;
if (VT_BSTR == Index.vt)
{
// Reference by string handle
CString SvcHandle(Index);
if (!GetService(SvcHandle, &svc))
return E_FAIL;
}
else
if (Index.vt & (VT_BYREF | VT_VARIANT))
{
// Reference by VARIANT (Dim i; For i = 0 to x Next; in VBScript)
LONG i = Index.pvarVal->lVal;
if (!GetService(i, &svc))
return E_FAIL;
}
else
{
// Reference by integer index
LONG i = V_I4(&Index);
if (!GetService(i, &svc))
return E_FAIL;
}
// Create service
CService *pService;
CService::CreateInstance(&pService);
pService->AddRef();
pService->Init(svc);
HRESULT hr = pService->QueryInterface(ppVal);
pService->Release();
return hr;
}
上面的代码使用了重载函数 GetService()
。该函数使用整数索引或服务句柄来查找服务记录。详情请参阅 smServices.cpp。
现在我们可以编写以下代码来处理我们的集合
Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
For i = 0 To Mgr.Services.Count - 1
WScript.Echo Mgr.Services.Item(i).Name
Next
恭喜,我们为 COM 对象添加了集合支持。你可以使用类似的技术添加另一个集合。
报告错误
如果用户指定了无效的服务句柄或索引值怎么办?如果我们的机器上的服务管理器出现问题怎么办?正确的解决方案是为我们的对象添加报告错误的能力。
要报告错误,我们的对象应该实现 ISupportErrorInfo
接口并使用 SetErrorInfo
函数向调用者提供错误信息。
首先,我们将编写一个错误报告函数,它将处理与 SetErrorInfo
函数相关的所有事务,并返回一个特殊的结果代码。
template<class ErrorSource>
HRESULT ReportError(ErrorSource* pes, ULONG ErrCode, UINT ResourceId = -1)
{
ICreateErrorInfo *pCrErrInfo;
IErrorInfo *pErrInfo;
if (SUCCEEDED(CreateErrorInfo(&pCrErrInfo)))
{
// Set all needed information for Err object in VB or active scripting
CString Descr;
if (-1 != ResourceId)
Descr.LoadString(ResourceId);
pCrErrInfo->SetDescription(Descr.AllocSysString());
pCrErrInfo->SetGUID(__uuidof(ErrorSource));
CString Source = typeid(ErrorSource).name();
pCrErrInfo->SetSource(Source.AllocSysString());
if (SUCCEEDED(pCrErrInfo->QueryInterface(IID_IErrorInfo,
reinterpret_cast<void**>(&pErrInfo))))
{
// Set error information for current thread
SetErrorInfo(0, pErrInfo);
pErrInfo->Release();
}
pCrErrInfo->Release();
}
// Report error via result code
return MAKE_HRESULT(1, FACILITY_ITF, ErrCode);
}
这是一个模板函数。它将使用类型信息来推断源的接口 GUID 和源类型名称(这可以通过 Err.Source
获取)。它还可以从资源加载错误描述。
为了实现 ISupportErrorInfo
接口,我们将使用 support_error_info
属性。实际上,这就是我们需要做的全部。
[
...
support_error_info("IServicesMgr"),
...
]
class ATL_NO_VTABLE CServicesMgr;
// ...
[
...
support_error_info("IsmService"),
...
]
class ATL_NO_VTABLE CsmService;
// ...
[
...
support_error_info("IsmServices"),
...
]
class ATL_NO_VTABLE CsmServices;
现在,让我们定义错误代码以及我们将如何返回它们。
对于 ServicesMgr
,错误情况是当 smServices
无法实例化时。将以下内容添加到代码中
class ATL_NO_VTABLE CServicesMgr :
public IServicesMgr
{
...
private:
enum
{
errNoServices = 0x100
};
...
};
STDMETHODIMP CServicesMgr::Start(BSTR ServiceName)
{
if (m_pServices)
{
CString SvcName(ServiceName);
return m_pServices->Start(SvcName);
}
else
return ReportError(this, errNoServices);
}
STDMETHODIMP CServicesMgr::Stop(BSTR ServiceName)
{
if (m_pServices)
{
CString SvcName(ServiceName);
return m_pServices->Stop(SvcName);
}
else
return ReportError(this, errNoServices);
}
对于 smServices
,错误情况是当无法枚举服务、用户指定了无效的服务句柄或索引,或者服务无法停止或启动时。
class ATL_NO_VTABLE CsmServices
: public IDispatchImpl<IsmServices>
{
...
private:
enum
{
errCannotEnumServices = 0x200,
errCannotStart,
errCannotStop,
errInvalidIndex,
errInvalidHandle,
errCannotOpenServiceManager,
errCannotEnumerateServices,
errOutOfMemory,
errCannotOpenService,
errCannotQueryStatus,
errOperationFailed
};
...
};
那么 CsmServices::get_Item()
将如下所示
STDMETHODIMP CsmServices::get_Item(VARIANT Index, IsmService** ppVal)
{
_Service svc;
*ppVal = 0;
if (VT_BSTR == Index.vt)
{
// Reference by string handle
CString SvcHandle(Index);
if (!GetService(SvcHandle, &svc))
return ReportError(this, errInvalidHandle);
}
else
if (Index.vt & (VT_BYREF | VT_VARIANT))
{
// Reference by VARIANT (Dim i; For i = 0 to x Next; in VBScript)
LONG i = Index.pvarVal->lVal;
if (!GetService(i, &svc))
return ReportError(this, errInvalidIndex);
}
else
{
// Reference by integer index
LONG i = V_I4(&Index);
if (!GetService(i, &svc))
return ReportError(this, errInvalidIndex);
}
// Create service
CService *pService;
CService::CreateInstance(&pService);
pService->AddRef();
pService->Init(svc);
HRESULT hr = pService->QueryInterface(ppVal);
pService->Release();
return hr;
}
我们可以使用此脚本测试错误报告
Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr")
Err.Clear
On Error Resume Next
WScript.Echo Mgr.Services.Item("qwe").Name ' "qwe" doesn't exist
MsgBox Err.Source
MsgBox Err.Number
MsgBox Err.Description
触发事件
我们将添加到服务管理器中的最后一项是通知客户端事件的能力。我们将添加 ServiceOperationProgress()
事件来通知客户端服务正在进行的启动或停止。
首先,我们创建一个全新的事件接口
// Service operation progress codes
[
export,
helpstring("Operation progress codes")
]
enum ServiceProgress
{
spContinuePending = SERVICE_CONTINUE_PENDING,
spPausePending = SERVICE_PAUSE_PENDING,
spPaused = SERVICE_PAUSED,
spRunning = SERVICE_RUNNING,
spStartPending = SERVICE_START_PENDING,
spStopPending = SERVICE_STOP_PENDING,
spStopped = SERVICE_STOPPED
};
// IServicesMgrEvents
[
dispinterface,
nonextensible,
hidden,
uuid("A51F19F7-9AF5-4753-9B6F-52FC89D69B18"),
helpstring("ServicesMgr events")
]
__interface IServicesMgrEvents
{
[id(1), helpstring("Notifies about lenghtly operation on service")]
HRESULT ServiceOperationProgress(ServiceProgress ProgressCode);
};
注意,我们还添加了一个枚举,它将在 VB.NET 中对用户可见,以便他们可以使用特殊的命名值而不是数字。
现在,使用 __event __interface
关键字在 ServicesMrg
coclass 中将 IServicesMgrEvents
接口指定为事件接口。ServicesMrg
coclass 还必须用 event_source("com")
属性标记。要触发 ServiceOperationProgress()
事件,我们应该使用 __raise
关键字。
[
...
event_source("com"),
...
]
class ATL_NO_VTABLE CServicesMgr :
public IServicesMgr
{
...
__event __interface IServicesMgrEvents;
void Fire_ServiceOperationProgress(ServiceProgress Code)
{
__raise ServiceOperationProgress(Code);
}
...
};
在完成所有这些工作之后,我们可以通过调用 Fire_ServiceOperationProgress()
方法轻松地通知客户端服务状态。
HRESULT CsmServices::WaitPendingService(SC_HANDLE hService,
DWORD dwPendingState, DWORD dwAwaitingState)
{
// ...
while (dwPendingState == ServiceStatus.dwCurrentState)
{
// ...
if (m_pMgr)
m_pMgr->Fire_ServiceOperationProgress
(static_cast<ServiceProgress>(ServiceStatus.dwCurrentState));
// ...
}
// ...
}
为了测试事件处理,我们将使用以下脚本
Set Mgr = WScript.CreateObject("ServicesManager.ServicesMgr", "Mgr_")
Mgr.Start("Alerter")
Sub Mgr_ServiceOperationProgress(ProgressCode)
WScript.Echo ProgressCode
End Sub
最好使用 cscript.exe 而不是 wscript.exe 运行此脚本,以便输出到 stdout
。
关注点
你可以在 MSDN 的 "Scripting Events" 文章(Andrew Clinick,2001)中找到更多关于处理脚本事件的信息。这对我来说确实是一件有趣的事情。
还有一篇很棒的文章 "Building COM Components That Take Full Advantage of Visual Basic and Scripting"(Ivo Salmre,1998,MSDN)。在这篇文章中,你将找到关于你的 COM 服务器需要具备哪些功能才能无缝地在 C++、VB 和 VBScript 语言中使用等基本信息。
如果你想调试类似的 对象,只需用 VBScript 编写一个脚本,将 cscript.exe 设置为调试命令,并将脚本路径设置为命令行参数。然后放置断点,然后运行项目。这是调试此类 COM 对象的最简单方法。
历史
目前版本 1.0。