通用加载项
为 DevStudio、Visual Studio 和 Office 编写插件
引言
本文介绍了一种编写插件的方法,使得一个二进制文件可以在多个版本的 DevStudio、Visual Studio 和 Office 中运行。它使用了 C++ 和 ATL,但其原理也适用于其他语言和其他框架。
背景
多年来,微软 IDE 版本之间插件编程模型的更改让我感到沮丧。我投入了大量精力为 DevStudio 6 编写了一些插件,却发现升级到 Visual Studio 2003 时不得不完全重写它们。然后,当我迁移到 Visual Studio 2005 时,对象模型再次发生了变化(尽管很小),又需要一次更新。我在不同的项目上仍然同时使用这三个 IDE,所以不能放弃旧版本;我必须维护我所写的任何给定插件的这三个版本。
这让我非常恼火,以至于我设计了一种编写插件的方案,使得同一个 DLL 可以加载到 DevStudio 6、Visual Studio 2003、2005 和 2008,甚至 Office 2003 中,并作为每个插件的主机。由于我怀疑不止我一个人经历过这种情况,所以我决定写下我所做的工作。
插件的通用布局
核心思想非常简单;大部分工作在于解决大量的实现细节。在本文的其余部分,我将引用我为说明我的方法而编写的示例插件 SampleCAI
(请参阅下载)。但在深入细节之前,让我先介绍我的通用方案。
我将构建一个进程内 COM 服务器(即一个 *.dll),它导出一个实现我们目标主机所需的所有接口的单个组件。该组件将以目标主机能够识别的方式向每个主机展示自己,作为一个插件。例如,当 DevStudio 实例化我们的组件时,它会请求 IDSAddIn
接口。只要我们实现了 IDSAddIn
,DevStudio 就会将其视为符合 DevStudio 标准的插件。我们可以实现一百个其他接口,呈现一个 UI,处理 HTTP 请求,等等——DevStudio 不会知道也不会在意。
同样,当 Visual Studio 2003、2005、2008 或 Office 实例化我们的组件时,它们会向我们的组件请求 IDTEExtensibility2
和 IDTECommandTarget
接口。同样,只要我们实现了这两个接口,宿主应用程序就会将其视为插件,而不管我们还能做什么。
简而言之,如果您创建了一个实现这三个接口的 COM 组件,我们所有的宿主都将愉快地将该组件加载为符合其各自模型的插件。

顺便说一下,DevStudio 6 和 Visual Studio 接口之间确实存在名称冲突(例如 ITextDocument
)。我通过 #include
'objmodel' 中的 DevStudio 6 头文件来处理这个问题,并将这些标识符保留在全局 namespace
中。然后,我 #import
了描述 Visual Studio 可扩展性模型的 typelib
,利用了 import 默认创建新 namespace
的行为来包含导入的 typelib
中定义的所有实体。
该示例是一个标准的 Win32 DLL 项目,基于 ATL 使用 C++ 实现。我将一步步讲解实现过程,并尝试解释我做了什么以及为什么。
CAdd-In 类
SampleCAI.cpp 是样板 ATL 代码;它实现了 DLLMain
和所有进程内 COM 服务器所需的四个导出。真正开始的部分是 SampleCAI.dll 的唯一 COM 组件 CoAddIn
。这是 CoAddIn
的 IDL 定义:
[
uuid(5f4e04a1-1a92-11db-89d7-00508d75f9f1),
helpstring("Common Add-In Sample Add-In Object")
]
coclass CoAddIn
{
[default] interface IUnknown /*IDSAddIn*/;
}
由于 DevStudio 发现新插件的方式,DLL 只能导出一个组件。当您将 DevStudio IDE 指向一个 DLL 并要求将其加载为插件时,DevStudio 似乎会调用 LoadLibrary
,然后在该 DLL 上调用 RegisterClassObjects
。它似乎会监视注册表,以找出该调用注册了哪些 COM 组件。我说“似乎”和“看上去”是因为这个机制没有文档记录;各种插件作者是通过经验推断出来的(例如,请参阅 [1])。
无论如何,DevStudio 会确定您的 DLL 导出了哪些 COM 组件,并对每个组件调用 CoCreateInstance
。在创建组件后,它会调用 QueryInterface
,查找 IDSAddIn
。如果 QI 失败,DevStudio 会通知用户并拒绝加载该 DLL 作为插件。因此,如果我们实现了两个 COM 对象,一个用于 DevStudio,一个用于 Visual Studio,DevStudio 会发现其对第二个组件的 QI 失败,认为该 DLL 不是有效的插件,并拒绝加载 **整个** DLL。
因此,SampleCAI.dll 导出且仅导出 CoAddIn
这一个 COM 组件,该组件实现了 IDSAddIn
。请注意,我实际上并没有在 IDL 中声明 IDSAddIn
:那只是因为 IDSAddIn
只在 C/C++ 头文件(ADDAUTO.H)中定义;我们没有它的 IDL。
现在,让我们看一下 C++ 类 CAddIn
,它是 COM 组件 CoAddIn
的实现。
class ATL_NO_VTABLE CAddIn :
// Standard ATL parent classes...
{
public:
/// ATL requires a default ctor
CAddIn();
/// ATL-defined initialization routine
HRESULT FinalConstruct();
/// ATL-defined cleanup routine
void FinalRelease();
# ifndef DOXYGEN_INVOKED // Shield the macros from doxygen...
// Stock ATL macros...
// Tell ATL which interfaces we support
BEGIN_COM_MAP(CAddIn)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IDSAddIn, m_pDSAddIn)
COM_INTERFACE_ENTRY_AGGREGATE(EnvDTE::IID_IDTCommandTarget,
m_pDTEAddIn)
COM_INTERFACE_ENTRY_AGGREGATE(AddInDO::IID__IDTExtensibility2,
m_pDTEAddIn)
COM_INTERFACE_ENTRY_AGGREGATE(IID_IDispatch, m_pDTEAddIn)
END_COM_MAP()
# endif // not DOXYGEN_INVOKED
// ...
public:
/// Display our configuration dialog
void Configure();
/// Carry out our command
void SayHello();
private:
/// Reference on our aggregated instance of CoDSAddIn
CComPtr m_pDSAddIn;
/// Reference on our aggregated instance of CDTEAddIn
CComPtr m_pDTEAddIn;
};
正如您所见,CAddIn
是一个普通的 ATL 类,实现了一个 COM 组件。第一个值得关注的点是接口映射。如上所述,CAddIn
类导出了 IDSAddIn
。但是,我们通过一个聚合体 m_pDSAddIn
来支持它。这是一个从属 COM 对象。
[
uuid(5f4e04a2-1a92-11db-89d7-00508d75f9f1),
helpstring("Common AddIn Sample DevStudio 6 AddIn Object"),
noncreatable
]
coclass CoDSAddIn
{
[default] interface IUnknown /*IDSAddIn*/;
}
请注意 noncreatable
标签;我们也不会注册它。严格来说,这样做并非必需;我创建这个类纯粹是为了代码的可读性。将所有对象模型(即 DevStudio、Visual Studio 和 Office)的代码添加到 CAddIn
会使类过于庞大。这纯粹是我个人的偏好;从外部来看,调用者不会知道任何区别。
结果是,当 DevStudio 调用 QueryInterface
查找 IDSAddIn
时,我们将将其委托给 m_pDSAddIn
。
m_pDTEAddIn
将持有 CoDTEAddIn
的引用。这是另一个类似于 CoDSAddin
的聚合体,但支持 Visual Studio 和 Office 2003 所需的接口。这是该组件的 IDL 定义:
[
uuid(5f4e04a3-1a92-11db-89d7-00508d75f9f1),
helpstring("Common AddIn Sample DTE-Compatible AddIn"),
noncreatable
]
coclass CoDTEAddIn
{
[default] interface IDispatch;
}
下一个棘手的问题是 IDTEExtensibility2
和 IDTECommandTarget
都是双重接口;如果收到 QI 请求 IDispatch
,我们应该怎么做?这是我不喜欢双重接口的众多原因之一,但那是另一篇文章的内容!在这种情况下,事实证明 IDE 希望我们返回 IDTEExtensibility2
。幸运的是,IDSAddIn
是自定义的,所以那里没有冲突。
下一个值得关注的点是 public
方法 Configure
和 SayHello
。该示例只能做两件事:配置自身和问好。此功能位于 CAddIn
中。想法是将插件的“核心功能”集中在 CAddIn
中,并将任务委托给聚合的辅助类来处理每个宿主应用程序。当它们检测到宿主应用程序已调用某个命令时,它们将调用 CAddIn
来执行工作。毕竟,这次尝试的全部意义在于消除多个插件中代码重复的需要。
DevStudio 6 宿主
在 DevStudio 中宿主方面,我做了一些非标准的处理。严格来说,这些对于将 AddIn
加载到多个宿主中不是必需的,它们只是让插件更好用一些。
当您将 DevStudio IDE 指向一个 DLL 并要求将其加载为 AddIn
时,DevStudio 会在新 AddIn
下创建注册表项:
HKEY_CURRENT_USER\Software\Microsoft\DevStudio\6.0\AddIns
但是,组件可以在安装过程中作为其一部分在 tersebut 键下“自我注册”,从而省去用户的麻烦。然而,这意味着新 AddIn
首次加载时,OnConnect
的 vfFirstTime
参数不会设置为 VARIANT_TRUE
。这是一个问题,因为这通常是我们判断我们的插件是首次加载的方式,因此也是执行一次性设置任务(例如创建工具栏)的时候。
创建工具栏很复杂,因为:
- 我们需要自己跟踪它是否已被创建
- 当
vfFirstTime
为false
时调用AddCommandBarButton
会失败
我通过在注册表中写入一个 bool
ean 来解决了问题 #1。我通过向一个隐藏的消息窗口发送消息来解决了问题 #2(事实证明,在 OnConnection
上下文之外调用 AddCommandBarButton
会成功)。
我从 Nick Hodapp 的文章“Undocumented Visual C++” [1] 中学到了这一点。这是示例使用的注册表脚本:
HKCU
{
NoRemove Software
{
NoRemove Microsoft
{
NoRemove DevStudio
{
NoRemove '6.0'
{
NoRemove AddIns
{
ForceRemove 'SampleCAI.CoAddin.1' = s '1'
{
val Description = s 'Sample Common AddIn Developer Studio Add-in'
val DisplayName = s 'SampleCAI'
val Filename = s '%MODULE%'
}
}
}
}
}
}
}
同一篇文章中的另一个技巧是命名工具栏的方法。使用标准的 AddIn
API,您的新工具栏将被命名为 Toolbar<n>
<n>
是未命名工具栏的数量(这很烦人且丑陋)。相反,示例 AddIn
会挂钩工具栏创建并将其窗口名称更改为更友好的名称。注意:新名称不能超过目标名称(通常为八个字符)。
这样,我们来看一下 CDSAddIn
,它是实现 DevStudio AddIn
的 C++ 类:
class ATL_NO_VTABLE CDSAddIn :
public CComObjectRootEx,
public CComCoClass,
public ISupportErrorInfo,
public IDSAddIn
{
public:
...
/// Private initialization routine
void SetParam(CAddIn *pParent);
...
/// Tell the ATL Registrar *not* to register us
DECLARE_NO_REGISTRY();
/// This component may only be created as an aggregate
DECLARE_ONLY_AGGREGATABLE(CDSAddIn)
...
/// Tell ATL which interfaces we support
BEGIN_COM_MAP(CDSAddIn)
COM_INTERFACE_ENTRY(IDSAddIn)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
END_COM_MAP()
...
private:
...
/// Non-owning reference to our parent CAddIn instance
CAddIn *m_pParent;
...
};
总的来说,这是一个实现 IDSAddIn
的标准的 ATL COM 类。事实上,对于 DevStudio AddIn
开发者来说,它的实现应该很熟悉。它对 IDSAddIn::OnConnection
的实现向 DevStudio 提供了一个暴露我们命令的 dispinterface,并接收它感兴趣的任何 DevStudio 事件。在 IDSAddIn::OnDisconnection
中,它会断开该连接。
注释
- COM coclass
CoDSAddIn
**不能**直接创建;它甚至没有注册,并且无论如何,没有聚合体创建都会失败。 CDSAddIn
维护一个(非拥有)回指指针指向其父CAddIn
。这使得它可以委托给其父级的命令实现。我猜这并非最通用的设计:一个 COM 对象如此密切地了解另一个对象的实现。然而,考虑到此组件 **绝不会** 在其他地方使用,便利性似乎是值得的。
Visual Studio 和 Office 宿主
如上所述,我们的插件将通过另一个 COM 聚合体 CoDTEAddIn
提供 IDTEExtensibility2
和 IDTECommandTarget
的实现。这个 coclass 由 CDTEAddIn
实现:
class ATL_NO_VTABLE CDTEAddIn :
// Stock ATL parent classes...
{
public:
/// Application host flavors
enum Host
{
/// Sentinel value
Host_Unknown,
/// Visual Studio 2003
Host_VS2003,
/// Visual Studio 2005
Host_VS2005,
/// Excel 2003
Host_Excel2003,
// Add new hosts here...
};
public:
...
/// Private initialization routine
void SetParam(CAddIn *pParent);
...
/// Tell the ATL Registrar *not* to register us
DECLARE_NO_REGISTRY();
/// This component may only be created as an aggregate
DECLARE_ONLY_AGGREGATABLE(CDTEAddIn)
/// Tell ATL which interfaces we support
BEGIN_COM_MAP(CDTEAddIn)
COM_INTERFACE_ENTRY(ISupportErrorInfo)
COM_INTERFACE_ENTRY(EnvDTE::IDTCommandTarget)
COM_INTERFACE_ENTRY(AddInDO::_IDTExtensibility2)
COM_INTERFACE_ENTRY2(IDispatch, AddInDO::IDTExtensibility2)
END_COM_MAP()
...
private:
...
/// Reference to our host's Application object
CComPtr<:_dte> m_pApp;
/// Reference to our host's Application object
CComPtr<:_application> m_pExcel;
/// Which host are we loaded into
Host m_nHost;
/// Non-owning reference to our parent CAddIn instance
CAddIn *m_pParent;
...
}; // End CDTEAddIn.<:_dte><:_application>
首先应该引起您注意的是,该类知道它已被加载到哪个宿主中。虽然 Visual Studio **和** Office 2003 都使用此插件编程模型,但宿主应用程序本身向 **我们** 提供不同的接口。我们在 IDTExtensibility2
接口的 OnConnection
方法中需要考虑这一点。我们在 OnConnection
方法中猜测宿主类型:
HRESULT hr = S_OK; // Eventual return value
try
{
// Validate our parameters...
if (NULL == pApplication) throw _com_error(E_INVALIDARG);
if (NULL == pAddInInst) throw _com_error(E_INVALIDARG);
// take a reference on the AddIn object representing us,
m_pAddIn = com_cast<:addin>(pAddInInst);
// & try to figure out what DTE-compatible host we're currently
// loaded into:
m_nHost = GuessHostType(pApplication);
ATLTRACE2(atlTraceHosting, 2, "CoDTEAddIn has been loaded with a c"
"onnect mode of %d (our host is %d).\n", nMode, m_nHost);
...<:addin>
在验证了我们的参数之后,我们做的第一项实际工作包装在对 GuessHostType
的调用中;在这里,我们弄清楚我们被加载到什么样的环境中。现在,我见过一些插件通过调用 GetModuleFileName(NULL,...)
来获取它们被加载到的可执行文件的名称,但我采取了不同的方法。我的想法是,只要宿主实现了我期望的接口,我就可以与它通信。例如,像 Open Office 这样的应用程序套件可以通过实现适当的 COM 接口来宿主 Microsoft Office 插件。
GuessHostType
通过对 OnConnection
中提供的应用程序指针进行 QI 来获取各种接口:
CDTEAddIn::Host CDTEAddIn::GuessHostType(IDispatch *pApp)
{
HRESULT hr = S_OK;
// Are we being hosted by Visual Studio 2005? I suspect this will be
// the most common case. Check by asking for an ENVDTE80::DTE2
// interface...
EnvDTE80::DTE2 *pDTE2Raw;
hr = pApp->QueryInterface(EnvDTE80::IID_DTE2, (void**)&pDTE2Raw);
if (SUCCEEDED(hr))
{
m_pApp = com_cast<:_dte>(pApp);
pDTE2Raw->Release();
return Host_VS2005;
}
// Ok-- maybe it's Visual Studio 2003...
...<:_dte>
请注意,我们不区分 Visual Studio 2005 和 2008。事实证明,Visual Studio 2008 实现接口 EnvDTE80::IID_DTE2
,并且其实现方式与 Visual Studio 2005 非常接近,因此,对我们而言,无需区分它们。
目标是将 m_nHost
填充为 Host
枚举的一个成员,以便其余逻辑“知道”如何行为。例如,我们在 OnConnection
中做的下一件事是调用 AddCommands
:
void CDTEAddIn::AddCommands(AddInDO::ext_ConnectMode nMode)
{
switch (m_nHost)
{
case Host_VS2003:
AddCommandsVS2003(nMode);
break;
case Host_VS2005:
AddCommandsVS2005(nMode);
break;
...
注释
- COM coclass
CoDTEAddIn
**不能**直接创建;它甚至没有注册,并且无论如何,没有聚合体创建都会失败。 - 与
CDSAddIn
一样,CDTEAddIn
维护一个(非拥有)回指指针指向其父CAddIn
,原因相同。
结论
这些是大致的要点;正如我在开头提到的,大部分工作都在细节中。我附带了一个功能齐全的示例插件,它可以加载到 DevStudio、Visual Studio 2003、Visual Studio 2005、Visual Studio 2008 和 Excel 2003 中。它是一个 Visual Studio 2005 解决方案,包含插件本身及其相关的卫星 DLL。要安装它,只需构建 Debug 或 Release 配置;有一个生成后步骤会自动正确注册 DLL。
当然还有更多工作可以做;请参阅附录 A。
享受--欢迎提问、反馈和建议。
附录 A - 未来工作
缓存 COM 组件创建
主要 COM 组件 CoAddIn
以聚合组件的形式实现了不同的宿主模型。今天,这两个组件都在 FinalConstruct
中实例化;最好迁移到某种缓存方案,以避免在加载到 DevStudio 时实例化(例如)CoDTEAddIn
的实例……
属性页
Visual Studio 2003 和 2005 允许其插件向它们在响应 Tools | Options 时显示的对话框添加页面。您可以通过添加一些额外的注册表项来告知 Visual Studio 您的页面或页面(请参阅示例中的 vs2003.rgs 或 vs2005.rgs,或在此处 [查看])。
我曾认为为 DevStudio 6 和 Excel 2003 添加一个新的属性页会很好,但我没能弄清楚如何做到。我的方案是安装一个 CBT 钩子,并捕获 Tools | Options 属性表的创建。在那里,我会将消息发送回一个私有的、仅用于消息的窗口,该窗口会创建**我的**属性页,并将一个 PSM_ADDPAGE
消息发送到属性表,并附带我的新页面。
无论出于何种原因,我在一个小型测试应用程序中成功实现了这一点,但在 Dev Studio 6 或 Excel 2003 中均未成功。在这两种情况下,Tools | Options 属性表都没有“Dialog”这个“Windows”类(就像这个一样),所以这些应用程序可能有一些非标准的实现。
如果任何人对此有任何想法,或者比我更成功,我很想听听。
同样,最好让 Visual Studio 显示的页面在调用 Configure 时选择我们的页面。目前,它们会打开到上次查看的页面。我有一些想法,例如再次安装一个 CBT 钩子来捕获属性表的创建,并向其子树形控件发送消息,但我还没有做任何事情。 Jeff Paquette 告诉我,他在他的 VisEmacs 插件中成功使用了这一点。
其他应用程序
我没有 Visual Studio 2002 的副本,所以无法测试。我实现了对 Excel 2003 的支持,但仅此而已。为整个套件构建支持将会很棒。
项目模板
一个用于生成通用插件代码的项目模板将会很好。
附录 B - Visual Studio 命令和命令栏
添加命令和设置命令栏是我编写这个示例中最烦人的部分。在解决这个麻烦的过程中,我大量依赖 Carlos J. Quintero 的文章“HOWTO: Adding buttons, commandbars and toolbars to Visual Studio .NET from an add-in”[2]。
Carlos 描述了两种不同类型的 Visual Studio 命令栏:永久和临时。
永久命令栏
- 即使插件通过插件管理器被卸载,仍然显示在 IDE 中
- 仅创建一次(当
OnConnection
方法收到ext_ConnectMode.ext_cm_UISetup
的值时……这在插件安装在机器上后仅发生一次) - 通过
DTE.Commands.AddCommandBar()
添加 - 仅在插件卸载时(而不是在加载时)通过卸载程序中的自定义操作使用
DTE.Commands.RemoveCommandBar()
函数移除
临时命令栏
- 每次加载插件时使用
DTE.CommandBars.Add()
或CommandBar.Controls.Add()
函数(取决于命令栏类型:Toolbar
或CommandBarPopup
)创建 - 每次卸载插件时,使用
CommandBar.Delete()
方法由插件移除
根据 Carlos 的说法,永久命令栏即使在用户卸载插件后仍然存在,“会让许多用户感到困惑”,因此,“大多数插件不使用这种方法”。他提出了以下方法:
If ext_cm_AfterStartup or ext_cm_Startup
Check for the command's existence through Commands::Item
If not there, create it via Commands::AddNamedCommand for
both 2003 & 2005
Create a new (temporary) command bar by calling
pTempCmdBar = ICommandBars::Add() (both 2003 & 2005!)
Add a button:
pTempCmdBar->AddControl
pTempCmdBar->Visible = true;
Then call pTempCmdBar->Delete() in OnDisconnect
请注意,他完全忽略了 ext_cm_UIStartup
。
对我来说,临时工具栏看起来还可以,除了两个问题:
- 每次启动 Visual Studio 时,工具栏都会重新出现,即使用户将其关闭了。
- 即使您注销了
AddIn
,命令仍然存在;这倒不是什么大问题…… IDE 会检测到这一点并在下次调用命令时移除它们。
永久工具栏会尊重用户关闭它们的决定,但如果您注销了 AddIn
并删除了命令,该该死的东西**仍然**在那里,您无法删除它!
我还没有确定“正确”的解决方案。示例 AddIn
可以根据 #define SAMPLECAI_COMMAND_BAR_STYLE
的设置使用几种不同的方法。它可以取以下三个值之一:
SAMPLECAI_COMMAND_BAR_TEMPORARY
使用临时命令栏SAMPLECAI_COMMAND_BAR_PERMANENT
使用永久命令栏SAMPLECAI_COMMAND_BAR_SEMIPERMANENT
使用永久命令栏,但在卸载时以编程方式删除命令栏
附录 C - 重置 Visual Studio
在开发 AddIn
的过程中,您可能会遇到需要重置 AddIn
对 Visual Studio 所做的所有 UI 更改的情况。
对于 Visual Studio 2005,您可以运行 devenv /resetaddin <AddInNamespace.Connect>
。不幸的是,Visual Studio 2003 只提供 devenv.exe /setup
,这会重置**所有** UI 自定义(包括您的按键绑定!)。由于这有点过激,我编写了这个小 VBS 脚本:
Dim objDTE
Dim objCommand
Dim objTb
On Error Resume Next
Set objDTE = CreateObject("VisualStudio.DTE.7.1")
If objDTE Is Nothing Then
MsgBox "Couldn't find VS 2003"
Else
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.Configure")
If objCommand Is Nothing Then
MsgBox "The Configure command has already been deleted."
Else
objCommand.AddControl(objDTE.CommandBars.Item("Tools"))
objCommand.Delete
End If
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.SayHello")
If objCommand Is Nothing Then
MsgBox "The SayHello command has already been deleted."
Else
objCommand.AddControl(objDTE.CommandBars.Item("Tools"))
objCommand.Delete
End If
Set objTb = objDTE.CommandBars.Item("SampleCAI")
If objTb Is Nothing Then
MsgBox "No (permanent) command bar named SampleCAI."
Else
objDTE.Commands.RemoveCommandBar(objTb)
objTb.Delete
set objTb = Nothing
End If
objDTE.Quit
set objDTE = Nothing
End If
Set objDTE = CreateObject("VisualStudio.DTE.8.0")
If objDTE Is Nothing Then
MsgBox "Couldn't find VS 2005"
Else
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.Configure")
If objCommand Is Nothing Then
MsgBox "The Configure command has already been deleted."
Else
'objCommand.AddControl(objDTE.CommandBars.Item("Tools"))
objCommand.Delete
End If
Set objCommand = objDTE.Commands.Item("SampleCAI.CoAddIn.SayHello")
If objCommand Is Nothing Then
MsgBox "The SayHello command has already been deleted."
Else
'objCommand.AddControl(objDTE.CommandBars.Item("Tools"))
objCommand.Delete
End If
Set objTb = objDTE.CommandBars.Item("SampleCAI")
If objTb Is Nothing Then
MsgBox "No (permanent) command bar named SampleCAI."
Else
objDTE.Commands.RemoveCommandBar(objTb)
objTb.Delete
set objTb = Nothing
End If
objDTE.Quit
set objDTE = Nothing
End If
参考文献
- Undocumented Visual C++ 作者:Nick Hodapp
- 为 Visual Studio .NET 的插件添加按钮、命令栏和工具栏
- 使用 VC++/ATL 构建 Office2K COM 插件
历史
- 2006年10月1日:首次发布
- 2008年4月26日:文章更新