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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (29投票s)

2004年1月27日

10分钟阅读

viewsIcon

92142

downloadIcon

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...] 行。这是一个属性。它定义了库块。这意味着我们拥有 DllMainDllRegisterServerDllUnregisterServer 函数,而无需编写任何代码。

添加 coclasses

让我们将 coclasses 添加到项目中。

解决方案资源管理器 中右键单击 ServicesManager 项目,然后选择 添加/添加类。然后在 添加类 - ServicesManager 窗口中选择 ATL/ATL 简单对象。在 ATL 简单对象向导 中输入 ServicesMgr 作为名称。将下一页的所有选项保留原样。注意 双重接口 选项已选中。这将帮助我们为 C++ 等使用 VTBL 绑定方法的语言以及使用 IDispatch 接口与对象通信的脚本语言提供 ServicesMgr 的功能。

在向导窗口中点击 完成,即可获得我们 ServicesMgr coclass 所需的所有代码。

现在,找到 ServicesMgr.h 文件中的 IServicesMgr 接口声明,并向此接口添加以下属性:oleautomationhiddennonextensible,使其看起来像这样

[
    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。向 smServicessmService 这两个 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 接口的默认实现,并将 IDispatchIsmServices 接口都暴露给客户端。

现在我们可以使用以下构造实例化 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;
}

并实现 ServicesMgrget_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 的 NameDisplayName 属性。

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。

© . All rights reserved.