在前面的文章中,创建了一个示例:我们向一个空的package添加了一个菜单命令功能,同时探索了Visual Studio Command Table文件的作用和用法。本文我们将手动为其添加一个tool window。
为工程添加一个tool window
我们将创建一个像下面这样的tool window
我们的tool window功能很简单:在FirstArgEdit 和 SecondArgEdit 输入框中输入数字,在 OperatorCombo中选择运算操作 (+, - , *, / 或 %).,点击Calculate 在 ResultEdit 输入框中显示结果。
为了将这个tool window集成到StartupToolset示例中,我们必须按如下步骤进行:
1. 设计tool window的用户界面
2. 实现tool window的功能
3. 为tool window设置资源
4. 创建一个ToolWindowPane类将用户界面集成到IDE中
5. 将tool window和package关联
6. 编写显示tool window的代码
在第四讲中我们已经增加过一个tool window。正如所看到的,创建一个tool window至少需要两个类。一个是WinForms的user control,一个继承自ToolWindowPane,能够将tool window的UI嵌入到IDE中。
第一步:设计用户界面
为StartupToolset工程添加一个user control,命名为CalculateControl.cs,按照上面提到的名字命名各个控件。设置OperationCombo为DropDownList并且添加“+”, “-“, “*”, “/”, “%”。
第二步:实现tool window的功能
有很多种实现tool window功能的方式,对于复杂的功能还可以创建一些辅助类型。在VSPackages下还可以创建一个服务允许我们的Package和其他的Package共用。
然而,本文我们用最简单的形式:直接在User Control中用代码实现。添加CalculateControl(译者注:原文是CalculationControl,但是更上文的CalculateControl.cs的命名冲突,以下的代码都以CalculateControl代替CalculationControl)的Load事件和Calculate的(译者注:原文为CaluclateButton,上文命名Button用的是Calculate所以这里用Calculate,代码部分也同样处理了)单击事件,并添加如下代码:
namespace MyCompany.StartupToolset
{
public partial class CalculateControl : UserControl
{
public CalculateControl()
{
InitializeComponent();
}
private void CalculateControl_Load(object sender, EventArgs e)
{
OperatorCombo.SelectedIndex = 0;
FirstArgEdit.Text = "0";
}
private void Calculate_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";
}
}
}
}
我想代码就不需要解释了吧。
第三步:设置资源
当tool window显示的时候,Visual Studio IDE会为每个tool window在tab状态的时候显示一个bitmap,比如Solution Explorer在tab状态的时候显示的bitmap:
我们不能显示地为我们的tool window设置bitmap。必须使用资源:在初始化tool window的时候只要传递一个资源标识。因为那些资源是由VS IDE维护的,资源必须包含在VSPackage.resx文件中。
准备一张“clock”的bitmap,添加这个bitmap到VSPackage.resx,用一个整数标识,这里用300。
我们可能希望只在我们的Package里面使用某些资源而不是IDE。最好的放置这些资源的地方是Resources.resx文件,因为Visual Studio自动生成一个Resources类来放置这些资源。
在Resources.resx中添加如下待用的资源:
Resource Name | Value |
ToolWindowTitle | Calculate Tool Windows |
CanNotCreateWindow | Cannot create tool window. |
第四步:创建一个ToolWindowPane
我们创建的User Control不知道如何集成到IDE中。集成到IDE的Window对象能够享受IDE提供的很多功能:例如可以被停靠、浮动或固定等。IDE宿主了(hosts)所谓的Window frames和Window panes来提供这些特性。我们的控件应该被嵌入到Window pane中。
关键在于创建一个代表Window pane的类型。这个类型继承自ToolWindowPane,ToolWindowPane实现了IVsWindowPane接口,这个接口负责集成工作。
添加一个C#源文件,CalculationToolWindow.cs复制下面代码:
using System.Runtime.InteropServices;
using Microsoft.VisualStudio.Shell;
using System.Windows.Forms;
namespace MyCompany.StartupToolset
{
[Guid("4B1BBBA2-9D83-45a4-8899-E7CB0296D27F")]
public class CalculationToolWindow : ToolWindowPane
{
private CalculateControl control;
public CalculationToolWindow() : base(null)
{
Caption = Resources.ToolWindowTitle;
BitmapResourceID = 300;
BitmapIndex = 0;
control = new CalculateControl();
}
override public IWin32Window Window
{
get { return control; }
}
}
}
这个类代表了一个COM类型,所以需要一个GUID定义这个类型,因此为类型添加一个Guid属性。CalculateControl实例通过一个control变量嵌入到tool window pane中。在构造函数中实例化这个control。重写的Window属性用来获得一个代表这个control的Win32句柄。
现在来看看构造函数:
public CalculationToolWindow() : base(null)
{
Caption = Resources.ToolWindowTitle;
BitmapResourceID = 300;
BitmapIndex = 0;
control = new CalculateControl();
}
构造函数内部用Resources设置Caption属性和BitmapResourceID属性。Caption是字符串类型,所以可以用一个字符串常量,但是这里我模仿向导的做法使用Resources.resx来指定。bitmap 用 BitmapResourceID 和 BitmapIndex 来设置。 BitmapResourceID 必须是个整型,我们的bitmap已经在 VSPackage.resx 文件中了。IDE 会把这个bitmap作为一个bitmap带(bitmap strip),BitmapIndex 设置了bitmap带中的索引。
我们的构造函数没有参数,但基类的构造函数需要一个IServiceProvider参数。这里只传递null。如果我们的window pane需要使用一些服务的话,可以通过传递这个参数提供。
我之所以提及这一点,因为VS 2008 SDK文档误导说:这个参数不能为null,否则tool window将不能被加载到shell中。事实上这是不正确的。
第五步:让package知道这个tool window
Tool window本身不是一个独立的对象,需要捆绑到package:这包含了何时以及如何显示tool window的逻辑,以及一些交互逻辑和服务。
我们通过为package增加ProvideToolWindow属性关联tool window 和package。必须强制为这个属性提供一个tool window 类的参数,这里是CalculationToolWindow:
…
[ProvideToolWindow(typeof(CalculationToolWindow))]
…
public sealed class StartupToolsetPackage : Package
一个tool window不仅仅能被这个package定义,也可以被其他VSPackages定义。在前面的文章(第五讲)中,我描述了package的“按需加载(on-demand load)”,一个package仅在其他的package使用(be aware of)已经存在的tool window时被加载。这是通过注册机制实现的,就像菜单命令一样。regpk.exe会应用这个属性注册这个package并关联这个tool window。当其他的package想要在tool window上做点动作,IDE会加载我们的package(如果还没加载)。
第六步:显示tool window
在第四讲中的代码显示了tool window。我们使用相同的模型来显示CalculationToolWindow:在我们前面写的事件处理函数中:
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());
}
简单的工作过程:用来查找CalculationToolWindow 实例的FindToolWindow方法是关键,并赋予ID 0。如果不能找到实例,将自动创建一个。Tool window本身依靠Show方法显示。
到这里,我们的tool window可以显示并工作了。
我们需要什么样的工具
如果问你:在你开发应用程序的过程中最想要的是什么工具,我想排在前五的一定有”logging”。有了记录,调试和修复应用程序就容易的多。所以在本文剩下的部分我们来为我们的应用增加一个简单的记录功能。
记录VSPackages
在应用程序中有很多记录活动情况的方法,我们可以将文本发送到控制台显示,跟踪到output窗口等。Visual Studio还有其他的选择:
1.Visual Studio有一个叫做活动记录(activity log)的XML文件。我们可以将信息放在这个文件中并显示。活动记录对于开发package很有用。
2.Visual Studio的output window允许我们发送和显示信息。如果我们想要区分一般的output信息和我们自己的信息,我们也可以自己创建一个output面板。
在这部分我们将加入logging的代码,当Calculate按钮按下时发送消息。
什么是VS Active Log?
通过设置/log命名行开关,Visual Studio可以在激活logging的模式下工作。在这个模式下VSPackage的信息被写入到一个叫做VS Active Log的XML文件中,可以通过浏览这个文件帮助测试和检查。
如果没有设置/log命令行,发送到活动记录(activity log)的信息不会被保存下来。
每次开始一个新的Visual Studio /log会话(session),之前的文件记录会被覆盖。这个文件叫做ActivityLog.Xml,这个文件的路径在你用户文件夹下的Microsoft\VisualStudio\<Hive>\UserSettings路径下。<Hive>取决于你使用的Visual Studio的版本(VS 2008是9.0),如果使用的是/rootsuffix开关则是9.0Exp。
所以,如果你用VS 2008 SDK开发packages时,<Hive>通常是9.0Exp。不要忘了你的账号指定的个人文件夹也决定着这个路径。
例如,如果你以jsmith登陆Windows Vista系统并且有个漫游账户(roaming profile),你将在下面的路径下找到这个文件:
C:\Users\jsmith\AppData\Roaming\Microsoft\VisualStudio\9.0Exp\UserSettings
Visual Studio将ActivityLog.xsl放在相同的路径下,所以当你用IE打开这个文件时,将被转换成表格显示。(译者注:实际上用IE打开ActivityLog.xsl并不能显示一个表格,相反ActivityLog.Xml却可以)
活动记录文件是定时刷新的,所以我的经验是,你可以在一个VS会话过程中查看(译者注:就是说可以在VS还没关闭时查看)。每次关闭VS 2008时,这个xsl文件将生成在这个路径。
使用Visual Studio活动记录
你可以像处理表格一样处理活动记录文件,当你记录一个新的信息时,新的行被添加到表格中。记录包含如下几个列:
列 | 描述 |
Record ID | 定义某个条目。IVsActivityLog接口自动生成 |
Type | 消息类型,是__ACTIVITYLOG_ENTRYTYPE枚举类型,这个枚举类型用下面相应的名字表示不同的消息类型: ALE_ERROR ALE_WARNING ALE_INFORMATION |
Description | 描述事件的文本,用户可以自定义 |
GUID | GUID值是可选的,如果指定的话可以是任何值( 比如a CLSID, a command ID, a package ID等) |
Hr | HRESULT是可选的。通常用来记录对应COM方法调用的返回值。 |
Source | 定义了消息的源。这个字符串可以是任何开发者认为的跟这条记录相关的源,比如package的名字。 |
Time | 消息被加入到log的时间。不需要开发者设置,由log自行生成。 |
Path | 可以添加一个文件的路径。当用默认的表格式样时,这个字段将和 description合并显示。 |
如果想要使用活动记录,必须通过SVsActivityLog 的GetService获得一个IVsActivityLog的引用。这个接口提供了一些方法来向活动记录添加信息。在我们的代码中如何向活动记录添加信息呢?在下面的例子中使用LogEntry,增加了简单的错误处理逻辑。Calculae_Click方法中添加了LogCalculation方法:
private void Calculate_Click(object sender, EventArgs e)
{
…
catch (SystemException)
{
ResultEdit.Text = "#Error";
}
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方法来获取SVsActivityLog服务。
使用output窗口
活动记录可以保存我们在开发package时关心的信息。在很多情况下我们想要显示package的使用情况。Output窗口是个理想的地方。我想我不必对output窗口做过多的介绍,这就是output窗口:
这个窗口拥有一个pane,当我们向output窗体输出信息的时候,实际上是向pane输出。可以用已有的pane,也可以创建我们自己的pane。在这个例子中我们将使用已有的pane。
如果我问你怎样向output窗口写入信息,你一定会回答:使用服务。是的,没错。我们有IVsOutputWindow接口,可以从SVsOutputWindow类型中GetService获取这个接口。这个接口只有三个方法: GetPane, CreatePane, DeletePane。我想顾名思义,真正对output窗口的操作应该是GetPane方法返回的IVsOutputWindowPane实例了。
现在我们修改一下在Calculate_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);
}
高亮部分是关键操作。我们需要先获取一个GetPane方法返回的引用。在这个例子中pane是有一个GUID标识的。VSConstants有个这个pane的GUID常量。OutputString将我们的信息输出到window pane中。运行程序,可以试试做几次运算:
总结
篇文章中我们完成了一个示例程序,手动将calculation tool window添加到工程中。这个tool window由两个协作的部分组成:一个user control负责用户接口和简单的业务逻辑,一个tool window pane负责将控件与IDE集成。还将菜单命令关联到了这个tool window。
我们完成了toolset的第一个部分:增加了一个记录功能以记录我们执行的计算动作,尝试了活动记录和output窗体两种形式。
VS活动记录尤其适合package的开发者,而output window则适合用户观察package的运行。
在下一篇中,我们将使用这个例子重构和提取代码。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义