(翻译)LearnVSXNow!-#7 创建我们第一个工具集-完成这个示例
在上一篇文章中,我们创建了一个例子:我们为一个空的package添加了一个菜单命令,并且在这个过程中了解了Visual Studio Command Table文件的作用和用法。
在这篇文章中,我们继续这个例子,手动为它添加一个工具窗。
为项目添加工具窗
我们将创建如下图所示的工具窗:
这个工具窗的功能非常简单:在FirstArgEdit和SecondArgEdit文本框里输入数字,在OperatorCombo下拉框里选择运算符(+、-、*或%) ,点击Calculate按钮后,运算结果显示在ResultEdit文本框中。
为了在StartupToolset示例中创建我们的工具窗,我们需要做下面的工作:
- 设计工具窗的界面
- 实现工具窗的功能
- 设置工具窗需要的资源
- 创建ToolWindowPane类,以便将这个工具窗嵌入到IDE中
- 将工具窗和package关联起来
- 编写显示工具窗的代码
我们曾在第4篇中为package添加过工具窗。正如我们在第4篇看到的那样,为了创建一个工具窗,我们至少需要两个类。第一个类是一个WinForm用户控件,它是工具窗的界面;第二个类继承自ToolWindowPane,通过它可以把工具窗的界面嵌入到Visual Studio IDE中。
第一步:设计用户界面
在StartupToolset项目里,添加一个名为CalculationControl的用户控件。把相应的控件从Toolbox中拖到该用户控件上,并且按照上图中给出的名字来命名各控件。设置ResultEdit控件的Anchor属性为[Top,Left,Right];设置OperationCombo控件的DropDownStyle属性为DropDownList,并给它的Items属性添加“+”, “-”, “*”, “/”, “%”五个选项。
第二步:实现工具窗的功能
实现一个工具窗的功能可以有很多种方式(设计模式)。特别是对于复杂的功能,我们可以创建一些互相协作的类来共同完成这些功能,我们也可以为VSPackage创建服务,这样我们的package和其他的package可以共用这些服务。
但是,在这篇文章里我们采用最简单的方式:直接在用户控件里添加实现功能的代码。
为CalculationControl用户控件的Load事件和CalculateButton按钮的Click事件添加事件处理方法,如下所示:
using System;
using System.Windows.Forms;
namespace MyCompany.StartupToolset
{
public partial class CalculationControl : UserControl
{
public CalculationControl()
{
InitializeComponent();
}
private void CalculationControl_Load(object sender, EventArgs e)
{
OperatorCombo.SelectedIndex = 0;
FirstArgEdit.Text = "0";
}
private void CalculateButton_Click(object sender, EventArgs e)
{
try
{
int firstArg = Int32.Parse(FirstArgEdit.Text);
int secondArg = Int32.Parse(SecondArgEdit.Text);
int result = 0;
switch (OperatorCombo.Text)
{
case "+":
result = firstArg + secondArg;
break;
case "-":
result = firstArg - secondArg;
break;
case "*":
result = firstArg * secondArg;
break;
case "/":
result = firstArg / secondArg;
break;
case "%":
result = firstArg % secondArg;
break;
}
ResultEdit.Text = result.ToString();
}
catch (SystemException)
{
ResultEdit.Text = "#Error";
}
}
}
}
我想代码就不用解释了吧。
第三步:设置资源
当我们的工具窗显示的时候,Visual Studio IDE会在这个工具窗的窗口标签那里显示一个图片。例如,当我们的工具窗和Solution Explorer显示在一起的时候,你可以在窗口标签那里看到这个图片:
不能把图片直接传给工具窗,必须利用图片资源:在初始化工具窗的时候,我们只能传递资源的标识。另外,由于这些资源标识是由VS IDE来处理的,所以这个图片必须放在VSPackage.resx文件中。
为了给工具窗添加“clock”图片,我们可以把这个图片文件添加到VSPackage.resx文件中,并用一个数字作为该图片资源的ID,在这里我们用300作为这个图片资源的ID。 (译者注:如果不知道怎样做bmp资源,可以从以前的示例SimpleToolWindow的Resources目录下拷贝一个bmp文件过来)
另外,我们自己的代码(不是IDE)也有可能用到一些资源,这些资源最好放在Resource.resx文件中,因为Visual Studio已经自动地帮我们创建了一个Resources类了,并且以静态属性的方式来表示放在该文件中的资源。
在Resources.resx文件中,添加如下的字符串资源,我们在后面会用到它们:
资源名 | 资源值 |
ToolWindowTitle |
Calculate Tool Windows |
CanNotCreateWindow |
Cannot create tool window. |
第四步:创建ToolWindowPane
负责工具窗界面的用户控件并不知道如何嵌入到VS IDE中。嵌入到IDE中的窗口对象(工具窗是其中一种)会包含很多由IDE提供的特性:例如它们可以停靠、浮动或者固定。IDE通过Windows frame和Window pane来提供这些特性。为了使我们的用户控件也有这些特性,它必须嵌入到一个Window pane里。
所以,把用户控件嵌入到IDE的关键,是去创建一个Window pane的类,这个类继承自ToolWindowPane,ToolWindowPane实现了IVsWindowPane接口。IDE利用这个接口来为工具窗提供上述特性。
在StartupToolset项目里,添加一个CalculationToolWindow.cs 文件,并且把下面的代码复制到这个文件里:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.Shell;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace Company.StartupToolset
{
[Guid("4B1BBBA2-9D83-45a4-8899-E7CB0296D27F")]
public class CalculationToolWindow : ToolWindowPane
{
private CalculationControl control;
public CalculationToolWindow()
: base(null)
{
Caption = Resources.ToolWindowTitle;
BitmapResourceID = 300;
BitmapIndex = 0;
control = new CalculationControl();
}
override public IWin32Window Window
{
get { return control; }
}
}
}
工具窗类以COM类的形式被IDE调用,所以我们需要为它指定一个GUID。在这个类的上面添加GuidAttribute,并指定一个guid。
用户控件CalculationControl的实例通过私有字段control来嵌入到tool window pane中。在这个类的构造函数里,我们创建了一个CalculationControl控件的实例,并利用Window属性来返回该控件实例的Win32句柄。
现在让我们看一下构造函数的代码:
public CalculationToolWindow()
: base(null)
{
Caption = Resources.ToolWindowTitle;
BitmapResourceID = 300;
BitmapIndex = 0;
control = new CalculationControl();
}
在上面这个构造函数里,我们用资源来设置工具窗的标题和图片。Caption是一个字符串类型的属性,所以我们可以给它一个字符串常量。但是在这里我用了和VSPackage向导一样的方式:通过在Resources.resx文件中指定的值来给Caption赋值。
工具窗的图片是根据BitmapResourceID和BitmapIndex这两个属性来决定的。第一个必须是一个整型的ID,这个ID值就是我们在VSPackage.resx文件中添加的图片资源的ID。IDE会把这个图片看作一个位图条(bitmap strip),BitmapIndex属性则指定了工具窗的图片在这个位图条中的索引。
这个构造函数没有参数,但是基类里的构造函数需要一个IServiceProvider类型的参数。由于我们并不需要这个参数,所以只需要传一个null过去就行了。当然,如果我们需要在工具窗中调用service,我们可以给它传一个IServiceProvider的实例。
我之所以提到这个,是因为VS 2008 SDK的文档误导我们说:“这个参数值不能是null(在Visual Basic里是Nothing),否则这个工具窗将不能加到vs壳里”。这是不正确的说法,你如果运行起来我们这个例子的话,你会看到我们的工具窗照样可以加到IDE里。
第五步:让我们的package知道这个的工具窗
我们的工具窗本身并不是一个独立的对象,它必须和package捆绑起来:包括何时或如何显示工具窗的逻辑,甚至可能包括一些交互逻辑和服务。
我们可以利用ProvideToolWindowAttribute来把工具窗和package关联起来,并且把工具窗的类型(在这里是CalculationToolWindow)作为参数传递给这个attribute:
...
[ProvideToolWindow(typeof(CalculationToolWindow))]
public sealed class StartupToolsetPackage : Package{...}
...
一个工具窗不仅能被所在的VSPackage调用,也能被其他的VSPackage调用。在前面的文章中(第5篇),我提到了一个按需加载package的模型。当其他的package调用这个package的工具窗的时候,该package才会被加载(前提是这个package在这之前没有被用到,否则早就被加载了)。这是通过和菜单命令类似的注册机制来实现的。regpkg.exe命令根据ProvideToolWindowAttribute去注册我们的工具窗,并且把它和对应的package关联起来。当其他的package试图对我们的工具窗做任何操作时,IDE就会加载我们的package(除非它已经被加载进来了)。
第六步:显示这个工具窗
在第四篇中我们看过显示工具窗的代码,在这里我们采用类似的方式来显示CalculationToolWindow,我们把这段代码放在菜单命令的事件处理方法里(这个事件处理方法我们已经在上一篇中创建了,但在当时只是用来显示一个消息框):
...
public sealed class StartupToolsetPackage : Package
{
...
private void ShowCalculateToolCallback(object sender, EventArgs e)
{
ToolWindowPane window = FindToolWindow(typeof(CalculationToolWindow), 0, true);
if ((null == window) || (null == window.Frame))
{
throw new NotSupportedException(Resources.CanNotCreateWindow);
}
IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame;
Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show());
}
...
}
提醒一下你它是如何工作的:关键是FindToolWindow方法,该方法负责查找ID为0的CalculationToolWindow的实例。如果没有找到,就会创建一个新的;通过调用工具窗的Frame属性的Show方法,就可以显示这个工具窗。
就这样,我们的工具窗可以通过点击相应的菜单项来显示出来了。
我们需要的工具
如果我问你,在你开发的时候最想要的是什么类型的工具,我猜排在前5的一定是“日志”。利用日志,调试和修复程序就容易的多。所以在这篇文章剩下的部分里,我们将为这个示例添加简单的日志功能。
为VSPackage添加日志
有很多方式可以为程序添加日志,例如,我们可以把文本消息发送到控制台,或发送到Trace或Debug output、Windows事件查看器甚至Windows调试日志。另外,Visual Studio也提供了一些其他的可选方案:
- Visual Studio有一个被称为活动日志(activity log)的的xml文件。我们可以把日志信息记录在这个文件里。对于记录重要的信息来说,活动日志非常重要。
- 另外,Visual Studio有一个输出窗口(output window),我们也可以把信息记录在这里。为了把我们的日志信息和其他的信息区分开来,我们可以在output window中创建自己的pane(例如版本控制工具或其他package创建的pane)。
在这篇文章中我们会在代码中加入这样的日志功能:当点击我们的工具窗的Calculate按钮时,我们把参数、操作符和计算结果记录到日志中。
什么是活动日志(activity log)?
在启动Visual Studio时,添加/log开关即可以启动Visual Studio的活动日志模式。在这种模式下,写在所谓的VS活动日志里的信息最终被保存在一个xml文件里,我们可以查看这个xml文件的内容,以便用于测试、验证、或解决问题。
如果在启动Visual Studio的时候没有加/log开关,发送到活动日志的信息就不会记录在这个xml文件里。
每一次你通过/log开关启动VS,上一次记录的ActivityLog.Xml日志文件就会被覆盖。这个文件位于你的用户配置(user profile)目录的Microsoft\VisualStudio\<Hive>\UserSettings子目录中。<Hive>取决于你运行的Visual Studio的版本(例如如果是VS 2008的话,<Hive>是9.0),如果你另外加了/rootsuffix开关的话,表明是VS的Experimental hive版本。所以,如果你是用vs 2008 sdk来开发package的话,<Hive>通常是9.0Exp。还有,一定要注意你的用户配置文件夹(user profile folder)的路径是由很多因素决定的(例如你的登录用户名、配置类型、操作系统等等)。
例如,如果你的系统是Windows Vista,你的用户名是jsmith,并且你有一个漫游配置文件(roaming profile),你可以在这个目录下找到活动日志文件:C:\Users\jsmith\AppData\Roaming\Microsoft\VisualStudio\9.0Exp\UserSettings。
Visual Studio也会在同一个目录下生成一个样式表文件(ActivityLog.xsl),所以如果用IE打开活动日志文件(ActivityLog.Xml)的话,会根据样式表文件定义的格式来以列表的形式展现日志。
活动日志文件会经常被重写,所以——根据我的经验——你可以在开着VS的时候查看这个文件。(译者注:本人认为关闭VS后再看这个文件内容也未尝不可,因为在VS不关闭的情况下ActivityLog.xml无法在IE下正常显示,只能用记事本之类的文件看。原文作者的意思应该是如果你在VS做了一个操作,可以在不关闭VS的情况下立刻用记事本之类的程序查看这个文件,以便检查这段操作记录下来的日志。)当你关闭了VS之后,样式表文件才会更新到这个目录下。如果你在打开VS之前或开着VS的时候删除了这个文件,那只能等VS关了之后才能重新得到这个文件。
使用Visual Studio活动日志(activity log)
你可以把活动日志当作一个表格。当你用他来记录一条消息的时候,会在活动日志表格里新增一行记录。每行记录包括如下的列:
列名 | 描述 |
Record ID |
标识每条日志的顺序号。IVsActivityLog服务会自动创建这个ID。 |
Type |
表示消息的类型,是__ACTIVITYLOG_ENTRYTYPE枚举的文本值。该枚举有三个选项:
|
Description |
日志的描述,由开发人员自定义。 |
GUID |
和这条日志相关的对象的GUID,是一个可选项。可以是任何值(例如一个CLSID、一个命令ID或一个package的ID等等) |
Hr |
和日志相关的HRESULT,是一个可选项。通常在为了记录一个COM方法的返回值时使用。 |
Source |
标识消息的来源。可以是package的名字,或者是开发者认为可以用来作为来源标识的任意字符串。 |
Time |
记录某条日志的时间,是由活动日志来决定的,开发人员不能设置它的值。 |
Path |
和日志相关的文件路径。如果用默认的样式表来显示活动日志的话,这一列的内容会合并到Description列中。 |
如果你想使用活动日志的话,必须要通过GetService方法来得到IVsActivityLog接口的实例。可以调用这个接口提供的一些方法来把消息记录到活动日志中。这些方法在被调用的时候,会往不同的列中写数据。每个方法都必须指定日志的类型,来源和日志描述,并且会为该日志自动创建一个Record ID,例如LogEntry方法和LogEntryGuidHr方法,但LogEntryGuidHr方法还会为该条日志添加GUID和Hr,而LogEntry则不会。
让我们看一下在代码里怎样把信息记录到活动日志里。在下面的代码段中,我们利用LogEntry方法记录了一条简单的信息。在这段代码中,我们添加了一段简单的逻辑:如果计算两个数的运算结果失败的话(例如除数为0),将会记录一条类型为error的日志;否则记录一条类型为information的日志。在CalculationButton_Click方法中,去调用LogCalculation方法:
private void CalculateButton_Click(object sender, EventArgs e)
{
try
{
int firstArg = Int32.Parse(FirstArgEdit.Text);
int secondArg = Int32.Parse(SecondArgEdit.Text);
int result = 0;
switch (OperatorCombo.Text)
{
case "+":
result = firstArg + secondArg;
break;
... // --- Omitted for clarity
}
ResultEdit.Text = result.ToString();
}
catch (SystemException)
{
ResultEdit.Text = "#Error";
}
//调用LogCalculation方法来记录日志
LogCalculation(FirstArgEdit.Text, SecondArgEdit.Text, OperatorCombo.Text, ResultEdit.Text);
}
当然,LogCalculation方法还没有定义,下面是该私有方法的定义:
private void LogCalculation(string firstArg, string secondArg,
string operation, string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3}",
firstArg, operation, secondArg, result);
IVsActivityLog log =
Package.GetGlobalService(typeof(SVsActivityLog)) as IVsActivityLog;
if (log == null) return;
log.LogEntry(
(result == "#Error")
?(UInt32) __ACTIVITYLOG_ENTRYTYPE.ALE_ERROR
: (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION,
"Calculation", message);
}
可以看出,用活动日志是非常简单的。不过要注意,在上面的代码中,我们用的是Package.GetGlobalService 这个静态方法来得到service。
使用output window
活动日志里的内容,是给package开发人员调试程序的时候用的。但在很多情况下,我们希望给package的最终用户显示一些消息。output window是用来显示这些消息的理想的地方。
我想我不必再介绍output window了吧,这就是output window(它通常位于VS IDE的底部):
output window有很多pane(在上图中显示的是“生成”这个pane)。当我们向output window中写信息的时候,我们实际上是向其中一个pane里写信息。我们可以用已有的pane,也可以创建自己的pane。在这个例子里,我们用output window中已有的“General(常规)”这个pane。
如果我问你怎样向output window里写信息,你一定会回答:“使用一个服务”,没错,是这样的,IVsOutputWindow服务可以帮我们向output window中写信息。我们可以把SVsOutputWindow类型作为参数来调用GetService方法,这样就可以得到IVsOutputWindow接口的实例。这个接口只有3个方法:GetPane、CreatePane、DeletePane。我想这三个方法名已经告诉我们一切了。我们可以用GetPane方法的返回值(是一个IVsOutputWindowPane接口的实例)来向一个pane中写入信息。
现在,让我们修改一下在CalculateButton_Click方法中调用的LogCalculation方法:
private void LogCalculation(string firstArg, string secondArg, string operation, string result)
{
string message = String.Format("Calculation executed: {0} {1} {2} = {3} ",
firstArg, operation, secondArg, result);
IVsOutputWindow outWindow =
Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow;
Guid generalWindowGuid = VSConstants.GUID_OutWindowGeneralPane;
IVsOutputWindowPane windowPane;
outWindow.GetPane(ref generalWindowGuid, out windowPane);
windowPane.OutputString(message);
}
红色部分是关键代码。为了向output window里的其中一个pane中写入信息,我们必须调用GetPane方法来获得这个pane的引用。在上面的代码段中,我们获得了General pane的引用。每一个pane都是由一个GUID标识的,VSConstants类的静态字段GUID_BuildOutputWindowPane的值就是General pane的GUID。OutputString方法负责把我们的信息写入该pane中。
运行我们的程序,然后在我们的CalculationToolWindow工具窗中试着做几次算术运算,相应的信息就会显示在输出来源为常规(General)的pane中:
总结
在这篇文章,我们完成了我们的例子:手动的添加了一个计算器的工具窗。我们的工具窗由两个互相协作的部分组成,其中:用户控件负责用户界面的展现和计算结果这个简单的“业务逻辑”;ToolWindowPane负责把该用户控件以工具窗的形式嵌入到IDE中。然后,我们在上一篇里已经创建好的菜单命令处理方法里,使用相关的代码来把这个工具窗显示出来。
接着,我们创建了我们这个工具集的第一个部分:为它添加了日志功能,可以将我们的工具窗里执行的算式记录下来。为了添加日志功能,我们使用了VS的活动日志和VS的output window两种方式。
VS的活动日志里的内容适合给package的开发者来看(可以用来检查、调试或修复package);VS的output window里的日志内容适合给package的最终用户来看(可以用来了解package正在做什么以及做了什么)。
在下一篇文章中,我们会重构这个例子,抽取一些代码和方法,用于创建我们工具集的新的部分。
原文链接:http://dotneteers.net/blogs/divedeeper/archive/2008/01/18/LearnVSXNowPart7.aspx
出处:http://www.cnblogs.com/default
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接,否则保留追究法律责任的权利。