• (翻译)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

  • 相关阅读:
    leetcode bugfree note
    leetcode 419
    leetcode 165
    leetcode 155
    leetcode 204
    leetcode 28
    将二叉搜索树转为有序双向链表
    leetcode 397
    ABAP 动态内表创建/赋值
    ABAP 屏幕下拉框值根据选择框填值赋值
  • 原文地址:https://www.cnblogs.com/default/p/1685190.html
Copyright © 2020-2023  润新知