VS 2003 IDE插件开发指南
作者 sema
2005-04-15 10:03
Visual Studio.NET插件能做很多事情,例如:
1、 编写如CodeRush一样的开发环境代码辅助工具
2、 编写如CodeSmith这样的代码模板工具
3、 编写代码生成器,根据自定义的一些条件自动生成代码。如现在比较流行的一些代码生成工具,如果和开发环境集成,使用起来应该会更加方便。
4、 编写如DataSetPryer这样的调试工具,可以在调试时查看DataSet的内容。
5、 甚至还可以在VS.NET里集成Google搜索引擎,或将MSN集成到VS.NET。
这里不再一一列举,总而言之,凡是可以和Visual Studio.NET开发环境相关的,都能以插件的形式进行。
开发VS.NET插件,目前有两种形式:一是利用VS向导生成的VS外接程序;二是利用微软的VSIP开发包(Visual Studio Industry Partner:微软合作伙伴计划)。本文讨论的是第一种方法。
二、 程序框架概述
在Visual Studio.NET中选择”新建项目à其他项目à扩展性项目àVisual Studio.NET外接程序”,按照向导生成代码,最后会生成两个工程文件,一个是外接程序项目,一个是外接程序安装项目。可以在外接程序项目里看到生成的项目文件中有个connect.cs文件,该文件有以下几个部分:
1、 类的继承接口及其常量定义
[GuidAttribute("952A6CFF-8516-4DA0-B0BA-519CB9614525"), ProgId("STDTools.Connect")]
public class Connect : Object, Extensibility.IDTExtensibility2, IDTCommandTarget
{…}
Connect类主要从两个接口继承,一个是Extensibility.IDTExtensibility2接口,该接口主要定义了下面几个方法:
OnAddInsUpdate 方法:在环境中加载或卸载外接程序时发生。
OnBeginShutdown 方法:正在关闭环境时发生。
OnConnection 方法:将外接程序加载到环境中时发生。
OnDisconnection 方法:当从环境中卸载外接程序时发生。
OnStartupComplete 方法:环境启动完毕时发生。
IDTCommandTarget接口则定义了以下两个方法
Exec 方法:在VS开发环境中选择了某个外接菜单命令时被VS环境所调用。
QueryStatus方法:当VS环境要显示外接菜单时调用该方法查询菜单的状态。
该方法返回指定的已命名命令的当前状态,无论此命令是启用、禁用还是隐藏
2、 OnConnection()函数:
本事件处理函数是在插件被加载时发生,一般用于做一些初始化工作,如创建菜单等。该函数的传入参数如下:
object application:定义了IDE自动化对象
Extensibility.ext_ConnectMode connectMode:连接模式,指明了插件当前的连接模式
ext_cm_AfterStartup 外接程序是在应用程序启动后加载的,或是通过将相应 AddIn 对象的 Connect 属性设置为 True 加载的。
ext_cm_Startup 外接程序是在启动时加载的。
ext_cm_UISetup 外接程序自安装后首次被启动。
3、 OnDisconnection()函数:系统卸载插件时被调用
本事件处理函数是在插件被卸载时发生,其传入参数如下
Extensibility.ext_DisconnectMode disconnectMode:
ext_dm_HostShutdown:外接程序是在开发环境关闭时卸载的。
ext_dm_UserClosed:外接程序是在用户清除“外接程序管理器”对话框中该外接程序的复选框时卸载的
ext_dm_UISetupComplete:外接程序是在环境安装完成后和在 OnConnection 方法返回后卸载的。
4、 QueryStatus()函数:
系统查询菜单状态
5、 Exec()函数:
在VS开发环境中选择了某个外接菜单命令时被VS环境所调用,在这里可以编写自己的响应代码,例如运行自己的程序或弹出某个窗口。
三、 处理菜单
在OnConnect方法中可以进行一系列初始化工作,其中之一就是生成菜单
1、 添加菜单条菜单和工具条菜单
applicationObject = (_DTE)application;
addInInstance = (AddIn)addInInst;
if(connectMode == Extensibility.ext_ConnectMode.ext_cm_UISetup
|| connectMode == Extensibility.ext_ConnectMode.ext_cm_Startup)
{// 如果是安装状态或是插件刚被启动的状态,则创建菜单
object []contextGUIDS = new object[] { };
//获取IDE环境的Command集合和CommandBar集合 Commands commands = applicationObject.Commands;
_CommandBars commandBars = applicationObject.CommandBars;
try
{
//菜单条对象和工具条对象都是CommandBar类型
CommandBar menuObj,toolbarObj;
//生成新的子菜单对象,将会被插入到菜单条和工具条对象上
Command commandObj = commands.AddNamedCommand(addInInstance,
"PublishUserManage",
"添加用户管理代码",
"添加用户管理的代码",
true,
127,
ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported+(int)vsCommandStatus.vsCommandStatusEnabled
);
#region
这里有几点要说明:
1、菜单图片:
以上的参数中的true,bool MSOButton参数为true,意味着该菜单命令的图片是来源于Office资源库,则后面的int Bitmap(位图ID号)则指明了是哪个图片。如何知道哪个ID号对应哪个图片?可以从微软的页面上下载一个用VBA写的工具,该工具列举出了所有的图片资源(附录工具中已经包含FaceID.xls)。
以上的参数中的true(bool MSOButton),意味着该菜单命令的图片是来源于Office资源库,则后面的127(int Bitmap--位图ID号)则指明了是哪个图片。如何知道哪个ID号对应哪个图片?可以从微软的页面上下载一个用VBA写的工具,该工具列举出了所有的图片资源(附录工具中已经包含FaceID.xls)。
如果不想用Microsoft Office自带的图片资源,则需要先在自己的解决方案中添加一个MFC DLL的工程,然后在其中加入图片资源;然后将MSOButton设为False,将Bitmap位图ID设为该资源文件中图片资源的ID号,然后还要在安装程序里添加一个文件夹,同时添加注册表项。具体过程就不再多说了,可以看参考资料
2、菜单项的名称
函数的第二项参数string Name(本例中是”PublishUserManage”),系统在生成菜单对象时会自动在Name前面添加本插件的ProgID代表的字符串作为菜单对象的全名。后面在根据名称检索该菜单对象时,需要用全名,本例中应该是”STDTools.Connect.PublishUserManage”;
*/
#endregion
//添加主菜单
CommandBarButton buttonObj;
//创建主菜单项和工具条
menuObj = (CommandBar) applicationObject.Commands.AddCommandBar("代码生成(&C)" ,
vsCommandBarType.vsCommandBarTypeMenu,
applicationObject.CommandBars["MenuBar"],10);
toolbarObj = (CommandBar) applicationObject.Commands.AddCommandBar(
"CodeTools(&C)",
vsCommandBarType.vsCommandBarTypeToolbar,
null,
-1);
toolbarObj.Position = Microsoft.Office.Core.MsoBarPosition.msoBarTop;
//增加子菜单
//将子菜单加入主菜单和工具条
buttonObj = (CommandBarButton) commandObj.AddControl(menuObj, menuObj.Controls.Count + 1);
buttonObj = (CommandBarButton) commandObj.AddControl(toolbarObj, toolbarObj.Controls.Count + 1);
buttonObj.Style = MsoButtonStyle.msoButtonIcon;
//将子菜单加入Project的右键菜单
CommandBar projBar = this.applicationObject.CommandBars["Project"];
commandObj.AddControl(projBar,1);
}
catch(System.Exception ex)
{
string error = ex.Message;
}
2、 添加右键弹出快捷菜单
除了上面在VS开发环境中添加常规菜单外,还允许用户为开发环境添加一些右键弹出菜单项。下面代码是为代码编辑窗口添加右键弹出菜单。
//检索代码编辑窗口右键弹出菜单的工具条
CommandBar projBar = this.applicationObject.CommandBars["Code Window"];
//将自己的菜单项加入工具条
commandObj.AddControl(projBar,1);
如果想在鼠标右键点击“解决方案资源管理器”中的某项目结点时弹出的菜单条中添加自己的菜单项,则只需把上面的"Code Window"改成"Project"即可。如何知道是"Code Window"或"Project"这个没有什么资料说明,但是也很简单,只要编写一个插件程序,列举出所有的菜单条对象就行了。幸好笔者已经做了这件事情,把所有的菜单条对象的名称都列在附录的CommandBar_Names.txt文件里了,你所要做的就是根据名称去猜哪个是你所需要的菜单条了。
3、 卸载菜单
为什么需要卸载菜单?
因为如果在你的插件中没有处理卸载菜单,则用户通过“工具à外接程序管理器”暂时关闭了你的插件,或者是通过添加/删除程序卸载了你的插件时,菜单项依然存在,但是却已经不能执行命令,这是不合理的。
卸载菜单是在OnDisconnection方法中实现的,具体可参见下载资源里的STDTool里的Connect.cs文件中的OnDisconnection方法。在该方法中的代码比较简单,先判断是不是VS环境正在关闭或用户通过外接程序暂停了插件,然后查找到子菜单项并删除,再查找到主菜单项和工具条项,从系统菜单集合里移除掉这些命令条对象。
4、 确定菜单状态
菜单显示时有一项比较重要的工作,就是根据应用环境的不同,菜单对象的显示状态也在不断变化。也即可用,禁用,不显示。每当VS集成环境要显示一个插件的菜单时,它会调用QueryStatus()方法查询该菜单应该显示的状态。具体代码可参见下载资源里的STDTool里的Connect.cs文件中的QueryStatus方法,该方法先预设菜单项状态为受支持状态(vsCommandStatusSupported,如果不受支持,则菜单不会显示),然后判断条件满足的情况下添加菜单为可用状态(vsCommandStatusEnabled),如果不满足条件,则菜单为禁用状态。
5、 执行菜单命令
当用户选择插件的菜单项时,集成环境会调用Exec()方法,具体代码可参见下载资源里的STDTool里的Connect.cs文件中的Exec方法,该方法查询被执行的菜单的名称是本插件所期望的名称,则执行一段代码,如显示窗口等。执行成功,则设置handled为true,告诉集成环境已经成功执行命令。
四、 使用窗口
1、 使用WinForm窗口
在VS.NET中显示WinForm窗口非常容易,和平常编写WinForm程序没什么两样,步骤如下:
l 在项目里添加一个WinForm窗体
l 修改WinForm窗体的构造函数
将VS环境的根对象DTE对象传入,以便在WinForm中可以操纵开发环境
public class MyForm : System.Windows.Forms.Form
{
private EnvDTE.DTE applicationObject;
…
public MyForm(EnvDTE.DTE applicationObject)
{
InitializeComponent();
this.applicationObject = applicationObject;
…
}
l 在Connect文件的Exec函数里生成并显示WinForm窗体。
MyForm form = new MyForm(this.applicationObject.DTE);
form.ShowDialog();
注意:这里的this.applicationObject是_DTE接口指针,它的一个成员DTE成员才是DTE对象,不能直接用类型转换如(DTE) this.applicationObject
2、 使用工具窗口
VS.NET的工具窗口指的是类似“解决方案资源管理器”,“类视图”,“工具箱”这样停靠在上下左右侧边的窗口。而如代码编辑窗口这样的窗口,需要用VSIP进行开发,外接程序插件是没有办法开发这类窗口的。
根据MSDN里的资料,如果要开发侧边工具窗口,是件非常繁琐的事情。因为VS.NET是基于COM开发的,所以开发出来的侧边工具窗口也需要实现一系列接口,也就是说,不能直接使用.NET程序集。幸而已经有人做了这件事情
(http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dv_vstechart/html/vstchFAQAboutVSNETAutomation.asp),
开发了一个将.NET用户控件包装成COM组件的容器VSUserControlHostLib,使得我们只需要开发出.NET用户控件,就可以将其嵌入到VS集成环境里了
开发步骤:
1、 生成Addin项目
2、 在项目中添加VSUserControlHost.dll引用
3、 在Connect.cs文件的OnConnection方法中添加显示窗口的代码
下面是代码示例:(添加在OnConnect方法中)
//GUID串可以用VS开发环境里菜单命令“工具à创建GUID”弹出的对话框创建
string guidstr = "{65FADCBF-63D2-448b-8A4B-393D7E751345}";
object objTemp = null;
VSUserControlHostLib.IVSUserControlHostCtl objControl = null;
//这里先生成容器窗口,返回的控件对象在objTemp里
windowExplorer = applicationObject.Windows.CreateToolWindow(
addInInstance,
"VSUserControlHost.VSUserControlHostCtl",
"菜单浏览器",
guidstr,
ref objTemp);
//使用容器窗口时,必须在调用该容器控件前设置其为可见,否者将会不能正常显示
windowExplorer.Visible = true;
windowExplorer.IsFloating = false;
//将返回的工具窗口转换为容器对象
objControl = (VSUserControlHostLib.IVSUserControlHostCtl)objTemp;
System.Reflection.Assembly asm = System.Reflection.Assembly.GetExecutingAssembly();
//用容器对象包含用户控件,
menuExplorer = (MenuExplore)objControl.HostUserControl(asm.Location,
"MenuBrowser.MenuExplore");
//设置窗口的图标
//windowExplorer.SetTabPicture(…)
//将DTE对象赋给窗口
menuExplorer.Application = applicationObject;
说明:
l 生成工具窗口:
以上代码先用CreateToolWindow函数生成工具窗口,工具窗口里承载的VSUserControlHost.VSUserControlHostCtl类型控件由objTemp对象返回
l 设置可见性:
将工具窗口设置为可见,非浮动
l 获取IVSUserControlHostCtl接口:
将返回的工具窗口中承载的控件对象objTemp转换为IVSUserControlHostCtl接口类型,并赋给objControl对象
l 加载用户控件:
用objControl的HostUserControl方法加载指定程序集的指定用户控件对象,上面代码中因为MenuBrowser.MenuExplore类是在插件项目的程序集里,所以用System.Reflection.Assembly.GetExecutingAssembly()方法获取当前程序集位置。如果用户控件是在其他程序集里,需要根据实际情况指定程序集位置
4、 最后还应该在OnDisconnection方法里关闭窗口
if(windowExplorer!= null)
{
windowExplorer.Visible = false;
}
五、 操纵VS开发环境
VS.NET自动化模型涉及的面太广,本文只针对一些专题加以说明。
1、 利用代码模型浏览代码
l 获取代码模型对象
//获取当前正活动的文档
Document activeDocument = applicationObject.ActiveDocument;
//获取代码模型对象,
//请自行判断activeDocument,ProjectItem,FileCodeModel以及CodeElements是否不为空
CodeElements codeElements = activeDocument.ProjectItem.FileCodeModel.CodeElements;
//检索代码元素
foreach(CodeElement ce in codeElements)
{
result.AddRange(getCodeElements(ce));
}
说明:
上面的代码先从DTE对象(也即VS自动模型的根对象)获得当前正活动的文档对象
然后再根据文档对象获取和它关联的项目元素,这里的ProjectItem其实就是在解决方案资源管理器下面的项目文件夹对应的子项。可以是各种类型,如文件,文件夹等
再从项目元素获得关联的文件代码模型
根据文件代码模型获取其代码元素的集合CodeElementCollection,代码元素的父类为CodeElement,下面派生出许多的子类,代表具体的代码元素,分别有:
CodeNamespace、CodeClass、CodeInterface、CodeFunction
CodeProperty、CodeVariable、CodeDelegate、CodeStruct、CodeEnum
代表着命名空间、类、接口、函数、属性、变量、委托、结构、枚举等类型的代码元素,某个CodeElement具体是何类型,可以根据CodeElement. vsCMElement枚举类型来判断,然后再显式转换成具体的子类即可
但是更进一步的代码元素,如函数体内的语句,好像还没有看到有方法检索。不过也可以利用一些其它的方法,比如CodeRush的程序集里面提供了一个StructuralParser程序集,用于C#的代码分析。如果谁有兴趣可以研究研究
l 根据具体类型检索代码元素
具体可参见下载资源包里的MenuBrowser的getCodeElements方法,这里不多作解释
2、 操纵解决方案及其项目文档
l 获取当前被选中的项目
下面代码演示了鼠标点击一个项目,或项目中的子项时,如何得到该项目对象
public Project GetSelectedProject()
{
Project project = null;
//从被选中对象中获取工程对象
EnvDTE.SelectedItem item = Application.SelectedItems.Item(1);
if(item.Project != null)
{//被选中的就是项目本生
project = item.Project;
}
else
{//被选中的是项目下的子项
project = item.ProjectItem.ProjectItems.ContainingProject;
}
return project;
}
l 获取当前项目的所在目录
private string GetSelectedProjectPath()
{
string path = "";
//获取被选中的工程
Project project = GetSelectedProject();
if(project != null)
{
//全名包括*.csproj这样的文件命
path = project.FullName;
}
//去掉工程的文件名
path = Path.GetDirectoryName(path);
return path;
}
l 将文件加入工程中
//获取被选中的工程
Project project = this.GetSelectedProject();
//将文件夹下的文件加入工程
project.ProjectItems.AddFromDirectory(sdir);
//将单个文件加入工程
project.ProjectItems.AddFromFile(nfile);
l 向项目中加入程序集引用
using VSLangProj;
private void AddReference(string assembly)
{
Project project = GetSelectedProject();
VSProject vsproject = null;
if (project.Kind == PrjKind.prjKindCSharpProject)
{
//工程类型为C#工程,将project的Object成员转换为VSProject对象
vsproject = project.Object as VSProject;
}
if(vsproject != null)
{
//获取C#工程的引用集
VSLangProj.References refers = vsproject.References;
if(refers != null)
{
//将程序集引用添加到工程中
refers.Add(assembly);
}
}
}
3、 在代码编辑窗口操纵代码
这里不再详述,只是列出网上的资源,大家可以自行参考
六、 参考资料
http://www.knowdotnet.com/add-insmacros.html
msdn帮助目录:Visual Studio .NET-->使用 Visual Studio .NET 进行开发-->参考-->自动化与扩展性参考-->公共环境对象模型