240
一线老司机

(翻译)LearnVSXNow!-#7 创建我们第一个工具集-完成这个示例

     在上一篇文章中,我们创建了一个例子:我们为一个空的package添加了一个菜单命令,并且在这个过程中了解了Visual Studio Command Table文件的作用和用法。

     在这篇文章中,我们继续这个例子,手动为它添加一个工具窗。

 

为项目添加工具窗

     我们将创建如下图所示的工具窗:

image

     这个工具窗的功能非常简单:在FirstArgEditSecondArgEdit文本框里输入数字,在OperatorCombo下拉框里选择运算符(+、-、*或%) ,点击Calculate按钮后,运算结果显示在ResultEdit文本框中。

     为了在StartupToolset示例中创建我们的工具窗,我们需要做下面的工作:

  1. 设计工具窗的界面
  2. 实现工具窗的功能
  3. 设置工具窗需要的资源
  4. 创建ToolWindowPane类,以便将这个工具窗嵌入到IDE中
  5. 将工具窗和package关联起来
  6. 编写显示工具窗的代码

     我们曾在第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显示在一起的时候,你可以在窗口标签那里看到这个图片:

image

     不能把图片直接传给工具窗,必须利用图片资源:在初始化工具窗的时候,我们只能传递资源的标识。另外,由于这些资源标识是由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赋值。

     工具窗的图片是根据BitmapResourceIDBitmapIndex这两个属性来决定的。第一个必须是一个整型的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也提供了一些其他的可选方案:

  1. Visual Studio有一个被称为活动日志(activity log)的的xml文件。我们可以把日志信息记录在这个文件里。对于记录重要的信息来说,活动日志非常重要。
  2. 另外,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枚举的文本值。该枚举有三个选项:

  • ALE_ERROR
  • ALE_WARNING
  • ALE_INFORMATION

 

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的底部):

image

 

     output window有很多pane(在上图中显示的是“生成”这个pane)。当我们向output window中写信息的时候,我们实际上是向其中一个pane里写信息。我们可以用已有的pane,也可以创建自己的pane。在这个例子里,我们用output window中已有的“General(常规)”这个pane。

     如果我问你怎样向output window里写信息,你一定会回答:“使用一个服务”,没错,是这样的,IVsOutputWindow服务可以帮我们向output window中写信息。我们可以把SVsOutputWindow类型作为参数来调用GetService方法,这样就可以得到IVsOutputWindow接口的实例。这个接口只有3个方法:GetPaneCreatePaneDeletePane。我想这三个方法名已经告诉我们一切了。我们可以用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中:

image

 

总结

     在这篇文章,我们完成了我们的例子:手动的添加了一个计算器的工具窗。我们的工具窗由两个互相协作的部分组成,其中:用户控件负责用户界面的展现和计算结果这个简单的“业务逻辑”;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

posted @ 2010-03-13 19:01  明年我18  阅读(2224)  评论(4编辑  收藏  举报