适用于 Visual Studio 2005 IDE 的 HJAddin,包含代码模板
HJAddin 为 VS2005 用户带来了代码模板功能,并提供了一个使用 C++ 和 ATL 编写插件的框架。
引言
HJAddin 是 Visual Studio 2005 IDE 的一个插件。它为 VS2005 用户带来了流行的代码模板功能,添加了一组有用的自定义按钮和菜单,最重要的是,为希望编写自己插件的其他开发者提供了一个使用 C++ 和 ATL 开发插件的框架。
成功安装后,该插件的外观如下
是不是很漂亮?
安装和卸载
如果我们自己构建了该插件,VS 2005 IDE 将会注册该类并在注册表中写入信息,使用AddIn.rgs。这使得彻底卸载变得困难,而不是更容易。为了确保能够彻底卸载以便部署干净的版本,请按照以下步骤操作:
- 卸载过程:如果我们至少安装过一次,或者在我们想要部署产品的机器上构建过它,那么卸载过程就会变得复杂。
- 首先,关闭所有 Visual Studio 2005 IDE 实例;
- 其次,运行程序setup.exe。如果这是该程序第一次执行,请转到步骤 2;否则,选择卸载插件;
- 最后,运行包含的Uninstall.vbs* 三次;听起来很奇怪,是吧?
- 安装过程:很简单,只需按照屏幕上的说明进行操作。
*Uninstall.vbs 是 Ivo Beltchev 的 VB 脚本,包含在他的“VSHelper - Visual Studio IDE enhancements”作品中。非常感谢他。
成功安装后,将在注册表中创建条目
HKEY_CLASSES_ROOT\CLSID\{306AB7FD-9FF3-4491-81D6-BB5687D81F70}
如下面的图片所示
支持的命令
- 代码模板按钮
,支持 关键字 替换和非常基础的格式化,允许我们插入从预定义代码模板文件中提取的代码片段。目前我们仅支持 C/C++ 代码模板。原因是,在我们人类发明的所有编程语言中,我们只会一种,而恰好是 C++。也就是说,我们仍然计划在不久的将来支持所有其他流行语言。
- 资源管理器按钮
,允许我们打开解决方案文件夹。它是一个双状态按钮,如果按住 Ctrl 单击它,它将打开活动文档所在的文件夹,即使该文档不属于解决方案。
- 命令提示符按钮
,允许我们打开命令提示符。当处理简单的控制台程序时,这可能很有用。
- H2Cpp 按钮
,允许我们在源文件(.cpp 或 .c)和头文件(.h)之间切换。Microsoft 在光标位于源文件时提供了一个右键单击命令“转到头文件”,但反之则不行。此按钮提供了双向跳转。
- 切换行号按钮,允许我们切换行号显示。这也是一个双状态按钮:如果按住 Ctrl 单击它,它将切换自动换行。
- 全屏按钮,允许我们在全屏模式下查看文件。
- 生成解决方案按钮,允许我们生成解决方案。其功能与 F7 类似。
- 无调试启动按钮,允许我们编译、链接然后运行程序而不进行调试。这与 Ctrl + F5 相同。
请注意,我们只编写了前四个按钮的代码。对于其余按钮,我们基本上只是执行 DTE 本身的命令。例如,要切换自动换行状态,我们执行命令“Edit.ToggleWordWrap
”,如下所示:
BOOL bCtrl = ((UINT)GetAsyncKeyState(VK_CONTROL) > 1); // control if( bCtrl) // if Ctrl-click { m_pDTE->ExecuteCommand(CComBSTR("Edit.ToggleWordWrap"), CComBSTR("")); return S_OK; }
代码模板功能
我们改编了 Michael Taylor 的作品来支持代码模板的关键字。功劳归于他,尽管代码已经发生了巨大变化。
自从 Darren Richard 在 1998 年或更早的时候推出代码模板功能以来,其功能一直没有改变。为了本文档的完整性,或者为了方便新用户,我们仍然希望在此进行描述。假设模板文件没有语法错误,CodeTmpl 按钮的工作方式如下:
- 当点击按钮时,它会读取模板文件并将其转换为一个菜单。
- 当点击菜单项时,它会翻译该菜单项对应的代码片段,并将代码片段插入到合适的位置。
- 当按住 Ctrl 点击按钮时,它会在底部添加一个名为“Open HJCodeTmpl.txt”的菜单项。
- 当点击“Open HJCodeTmpl.txt”菜单项时,它会在 VS2005 IDE 中打开位于安装目录下的模板文件,并将其视为 C++ 源文件。此时,用户可以进行一些就地或即时编辑。在完成文件修改并将其保存到磁盘后,用户再次按住 Ctrl 点击按钮即可使修改立即生效。
支持的关键字和格式化器
当前,我们支持以下关键字。它们已经过全面测试,我们认为它们是稳健的。
关键词 | 示例用法 | 结果 |
SOLUTION |
解决方案是 #%SOLUTION%#。 | 解决方案是 HJAddin.sln。 |
PROJECT |
项目是 #%PROJECT%#。 | 项目是 HJAddin |
文件 |
文件是 #%FILE%#。 | 文件是 source1.cpp。 |
FILEFULLNAME |
完整文件名是 #%FILEFULLNAME%#。 | 完整文件名是 d:\Cpp\HJAddin_VS2005\0.8c\source1.cpp。 |
PATH |
路径是 #%PATH%#。 | 路径是 d:\Cpp\HJAddin_VS2005\0.8c\。 |
FILENAME_ONLY |
文件名(不含扩展名)是 #%FILENAME_ONLY%#。 | 文件名(不含扩展名)是 source1。 |
EXT_ONLY |
文件名扩展名是 #%EXT_ONLY%#。 | 文件名扩展名是 cpp。 |
NOW |
现在是 #%NOW%#。 | 现在是 2006/10/23 00:41:45。 |
DATE |
日期是 #%DATE%# | 日期是 2006/10/23 |
TIME |
时间是 #%TIME%# | 时间是 00:41:45 |
我们计划在有时间时添加更多关键字,例如 AUTHOR
。
本版本的 HJAddin 支持以下格式化器,我们认为它们是稳健的。
格式化器 | 示例用法 | 结果或解释 |
W n | #<W 35 Hai Jin># | 将 #< 和 ># 之间的字符串展开或截断到正好长度 35——如果字符串不够长则用空格填充右侧,如果太长则截断。 |
U | #<U Hai Jin># | 将“Hai Jin”转换为大写,变成“HAI JIN”。 |
L | #<L Hai Jin># | 将“Hai Jin”转换为小写,变成“hai jin”。 |
%$% | 描述:%(美元符号)% | 描述:I --- 这表示光标或插入点将位于“I”处。 |
以下两个格式化器处于测试模式,并且被认为不稳健:
格式化器 | 示例用法 | 结果或解释 |
%BEGINDOC% | %BEGINDOC% | 将插入或编辑点移到当前活动文档的开头。 |
%NSF% | %NSF% | 暂时禁用智能格式化功能。 |
模板文件
设置一个好的代码模板文件并非易事,因此我想在这里花些时间。用户可能想通读包含的模板文件“HJCodeTmpl.txt”,但这可能会花费他们一两个小时的宝贵时间。为简单起见,我们使用以下图示和模板文件中的菜单项来展示如何操作:
代码模板按钮的外观如下:
而模板文件中的相应文本是:
#[Message Box
#{AfxMessageBox
AfxMessageBox(_T("%$%"));
}#
#########
#[MessageBox
#{MsgBox Sub#1
MessageBox(NULL, "%$%", "", MB_OK);
}#
#{MsgBox Sub#2
MessageBox(NULL, "%$%", "", MB_ICONINFORMATION);
}#
]#
#{MsgBox #3
MessageBox(NULL, "%$%", "", MB_OK | MB_ICONINFORMATION);
}#
]#
用于添加命名命令、按钮和菜单/菜单项的框架
添加命令的过程是:添加一个命名命令并在连接时更新 UI (CConnect::OnConnection
),更新查询状态使其可见 (CConnect::QueryStatus
),然后根据用户请求执行它 (CConnect::Exec
)。更新 QueryStatus
的工作非常简单,而 Exec
是我们真正的工作所在。然而,我们不打算讨论后两者,而是专注于 OnConnection
。
修改 OnConnection
的任务包括:添加命名命令,创建我们的工具栏并向其添加按钮,以及创建我们的菜单并向其添加菜单项。为了使任务易于遵循,我们实现了一些辅助函数。让我们从 AddCommand2
开始。
AddCommand2
的声明如下:
HRESULT AddCommand2(const char* cmdFullName, const char* cmdShortName, const char* cmdButtonText, const char* cmdToolTip, long nBitmapID, VARIANT_BOOL bMSOButton = VARIANT_FALSE);
因此,我们只需要做以下任一操作,选项 #1 --- 如果我们想添加一个带有自定义位图的命名命令:
hr = AddCommand2(m_szExplorer_FN, m_szExplorer_SN, m_szExplorer_BT, m_szExplorer_TT, IDB_EXPLORER);
或者选项 #2 --- 如果我们想添加一个带有内置位图的命名命令:
hr = AddCommand2(m_szToggleLineNumbers_FN, m_szToggleLineNumbers_SN,
m_szToggleLineNumbers_BT, m_szToggleLineNumbers_TT,
11, VARIANT_TRUE);
对于选项 #1,需要一个卫星 DLL 来存放位图。有关添加自定义位图的更多信息,请参见下面的 部署项目 部分。
我想对包含的 AddToMyMenu(...)
、AddToMyToolbar(...)
、GetMenuPosition(...)
、CreateMyMenu(...)
和 CreateMyToolbar(...)
辅助函数做一些说明。
GetMenuPosition
函数返回现有菜单(如“工具”和“帮助”)的位置;AddToMyMenu
和AddToMyToolbar
并不意味着我们只能将项添加到自己的菜单和工具栏中,它们实际上可以添加项到现有菜单(如“生成”和“窗口”)和现有工具栏(如“标准”和“文本编辑器”)中;- 我们可以使用
CreateMyMenu
和CreateMyToolbar
来创建任意数量的新菜单和工具栏。但是,我们不想过多地干扰 IDE,对吧?事实上,我并没有为我的插件版本创建自定义工具栏;相反,我将所有按钮添加到了“文本编辑器”工具栏中。
最后,让我们用伪代码描述用于使 OnConnection
正确工作的框架:
STDMETHODIMP CConnect::OnConnection(IDispatch *pApplication, ext_ConnectMode ConnectMode, IDispatch *pAddInInst, SAFEARRAY ** /*custom*/ ) { HRESULT hr = S_OK; hr = pApplication->QueryInterface(__uuidof(DTE2), (LPVOID*)&m_pDTE); hr = pAddInInst->QueryInterface(__uuidof(AddIn), (LPVOID*)&m_pAddInInstance); CComPtr<COMMANDS> pCommands; hr = m_pDTE->get_Commands(&pCommands); CComPtr<COMMAND> aCommand; hr = pCommands->Item(CComVariant(m_szCodeTmpl_FN), 0, &aCommand); // Check if one of the named commands exists. // If it exists, then all other named commands exist as well. // If not, we add them. if( hr != S_OK && !aCommand) { hr = AddCommand2(m_szExplorer_FN, m_szExplorer_SN, m_szExplorer_BT, m_szExplorer_TT, IDB_EXPLORER); // add more commands, either those with a custom // bitmap resource or those with built-in face // create the toolbar once and for all hr = CreateMyToolbar(); // create more toolbars if you wish // add buttons to the toolbar hr = AddToMyToolbar(m_szCodeTmpl_FN, m_szCodeTmpl_TT); // add more buttons to the corresponding toolbar // you can add built-in command to a toolbar just as below hr = AddToMyToolbar("Build.BuildSolution", "Build solution"); } // The menu we created is temporary so that we are forced to // create it every time the add-in is connected int n = GetMenuPosition(); hr = CreateMyMenu(n+1); // create more menus // add items to the menu hr = AddToMyMenu(m_szExplorer_FN); // add more menuitems to the corresponding menu return S_OK; }
如果我们遵循上述框架,事情就不会出错。如果出现问题,请检查 故障排除 部分。
部署项目
部署项目(在我们的例子中是HJAddinSetup项目)负责我们产品的安装和卸载。它将所需文件复制到相应位置,使用唯一的 CLSID(类 ID)注册类,并写入注册表。有几点我想强调:
- 我们需要为插件提供信息以查找位图资源。这就是部署项目的注册表部分发挥作用的地方。
- 我们需要正确设置部署项目中“Primary Output”的属性:主要是,我们希望“Register”字段为“vsdrpCOM”,以便插件在安装过程中能够自行注册。
在这个小型项目中,我们选择仅支持美式英语,以便插件 DLL 可以作为自己的卫星 DLL。具体来说,我们被迫添加两个字符串注册表项 SatelliteDLLName
和 SatelliteDLLPath
。
SateliteDLLName = ..\HJAddin.dll
SateliteDLLPath = [TARGETDIR]
其中,[TARGETDIR] 将被扩展为安装路径,例如“C:\Program Files\Hai Jin\HJAddin\”。
这种方法的好处在于我们可以使用资源 ID,如 IDB_CODETMPL
,而缺点是它使我们的产品更难获得国际支持。如果我们必须在全球分发产品,最好为我们支持的每种语言编写一个资源 DLL。然而,资源 DLL 方法通常不使用资源 ID。
编写插件的故障排除
喝完一杯咖啡坐下来敲键盘,为了打扮插件,我们遇到了很多麻烦。本节专门用于故障排除。
- 命令消失的问题
- 同名工具栏过多的问题
- 自定义菜单消失的问题
Microsoft 提出了三种解决此问题的方法。详细信息可以在 这里找到。我们的方法是检查我们的一个命令是否存在。如果存在,那么我们的所有命令都存在,因此无需再次添加;否则,我们一次性添加所有命令。该方法确保我们只添加一次命令。
// Check if one of the named commands exists. // If it exists, then all other named commands exist as well. // If not, we add them. if( hr != S_OK && !aCommand) { hr = AddCommand2(m_szExplorer_FN, m_szExplorer_SN, m_szExplorer_BT, m_szExplorer_TT, IDB_EXPLORER); // add more commands }
这很可能是因为每次插件连接时都添加工具栏,或者我们没有正确清理。以下代码执行高质量的清理工作:
// delete all previously installed toolbars with name toolbarName // note that we delete it until we cannot delete it anymore // using a do...while loop CComQIPtr<_CommandBars> pCommandBars; pCommandBars = pDisp; do { hr = pCommandBars->get_Item(CComVariant(toolbarName), (CommandBar**)&pDisp); if(pDisp) pCommands->RemoveCommandBar(pDisp); } while(hr == S_OK);
您可以在插件每次启动时添加工具栏,但这将在每次启动 IDE 时使工具栏浮动在 VS2005 IDE 的左上角。这是一个糟糕的解决方案,是吧?我提出了一种方法,可以让我们一劳永逸地添加工具栏:
// Check if one of the named commands exists. // If it exists, then all other named commands exist as well. // If not, we add them. if( hr != S_OK && !aCommand) { // add the commands // create the toolbar once and for all hr = CreateMyToolbar(); // add buttons to the toolbar hr = AddToMyToolbar(m_szCodeTmpl_FN, m_szCodeTmpl_TT); // add more buttons if you like }
默认情况下,我们添加的任何菜单都是临时的,因此每次连接插件时我们都必须重新添加它。
// The menu we created is temporary so that we are forced to // create it every time the add-in is connected int n = GetMenuPosition(); hr = CreateMyMenu(n+1); // add items to the menu hr = AddToMyMenu(m_szExplorer_FN); // add more menuitems
致谢
在为 Microsoft Visual Studio 2005 IDE 开发此插件的过程中,以下人员的代码、文章和书籍提供了帮助。我谨向他们致以最深的谢意。
他们的姓名和相关工作是:
- Darren Richard 为 Visual C++ 5.0 编写的 Code Template 插件的原始代码;
- Michael Taylor 为 Visual C++(适用于 Visual Studio 6)更新的 Code Template 插件,增加了关键字增强功能;
- Eddie Velasquez 的 Code Template Add-In for Visual Studio 7.0 (2002) 和 7.1 (2003)。该插件是用 C# 实现的,支持 VB、C/C++ 和 C# 等多种语言;
- Ivo Beltchev 关于“VSHelper - Visual Studio IDE enhancements”的文章和代码。他的代码适用于 VS 7.1 和 VS 8.0。部署项目对 HJAddin 的安装帮助很大;
- 感谢 Joseph M. Newcomer 关于“CString Management”的精彩文章。我复制了他的一项转换函数,用于将
BSTR
字符串转换为CString
字符串。