Duncan Mackenzie
Microsoft Developer Network
2003 年 6 月 3 日
摘要:Duncan Mackenzie 向您展示了针对 Visual Studio .NET 开发环境的几种有用的宏,其中一种宏可以将您项目中的所有代码打印出来。(请注意,在示例文件中,程序员的注释使用的是英文,本文中将其译为中文是为了便于读者理解。)
应用于:
Microsoft® Visual Studio® .NET 2003
Microsoft® Visual Basic® .NET 2003
下载本文的源代码(英文)。
程序员的思维转向宏。自定义开发环境是我经常忽视的一项工作,但着手做时却非常重视。我在 Microsoft® Visual Studio® .NET 中花费的时间要多过在家中的时间(至少周末如此),因此会尽力享受并有效地利用这段时间。在本文中,我将展示几个宏,用来向 IDE 添加某些极其个性化的功能。我希望这些示例能对您自己进行自定义有所帮助。
如果您是在 Visual Studio .NET 中开发宏的新手,您可能会惊喜地发现两件事情:首先,Visual Studio .NET 包含用于编辑宏的完整 IDE(不太惊喜),该 IDE 与常规 Visual Studio .NET IDE 非常类似。其次,您不必使用脚本语言编写宏;您可以使用完整的 Microsoft® Visual Basic® .NET 语言来进行工作。下载中包括本文提到的所有三个示例宏,但是这些宏不会在您运行 .msi 时安装在主 \VSMacros 目录中。相反,它们被放入 MSDN\Coding4FunIssue5 下的“我的文档”文件夹。在这里您可以使用“加载宏项目”菜单命令(“工具”|“宏”|“加载宏项目”)在 Visual Studio .NET 中将它们打开。
编写您自己的宏的第一步是切换到“宏 IDE”。您可以使用 ALT+F11 或通过选择“工具”|“宏”|“宏 IDE”菜单项切换到“宏 IDE”。一旦进入此稍微缩减的 Visual Studio IDE(参见图 1)版本,您可以使用“项目资源管理器”窗口浏览加载的宏项目。
图 1:宏 IDE 是一个功能完备的开发环境,与完整的 Visual Studio .NET 环境十分相似。
要添加您自己的宏,只需在现有模块中创建新的公用 Sub(可能在 MyMacros 项目中),或创建新的公用模块来存放您的新代码。本文稍后我将向您展示如何创建您自己的新宏,不过我要先谈谈 Print Code 宏的代码。
在 Visual Basic 6.0 中,从“文件”菜单中选择“打印”时,您可以选择打印您项目中的所有代码(甚至是未在设计器中打开的代码文件)。许多程序员会在需要生成工作的硬拷贝时使用此功能。现在,我不能确定您多长时间会打印一次代码(通过纸张运行代码比较困难),但是如果您要在 Visual Studio .NET 中进行打印,您将需要分别打印每个代码文件。Visual Studio Team 的 Craig Skibo 发给我这个可以自动化打印过程的相对简单的宏,它演示了如何循环浏览 Visual Studio 项目中的所有项。
注意:在 Visual Basic 6.0 中,您还可以打印窗体的设计表面,但是此宏没有该功能。您也可以编写一些代码来实现该功能,但是不会简单,因为 Visual Studio .NET 未提供设计时打印 Windows 窗体的功能。
此打印宏所作的第一步是使用 DTE.ActiveSolutionProjects 来获取当前选定的一个或多个项目。
Sub PrintItemsInSelectedProject() Dim proj As Project Dim objProj As Object() objProj = DTE.ActiveSolutionProjects If objProj.Length = 0 Then Exit Sub End If proj = DTE.ActiveSolutionProjects(0) PrintItemsInSelectedProject(proj.ProjectItems) End Sub
DTE 是所有宏均可使用的特殊对象。它代表 IDE 本身,提供对当前项目组、活动文档等的访问。在这种情况下,虽然代码可以获取所有活动项目的集合,但我们感兴趣的只有一个项目,因此我们将第一个项从集合中提取出来。此例程中的最后一行可能有些混淆,它看起来像要调用它本身,但实际上它要调用同一例程的另一个重载。使用针对宏开发的完整功能的语言可以访问此类高级功能。
Private Sub PrintItemsInSelectedProject( _ ByVal projitems As ProjectItems) Dim projitem As ProjectItem For Each projitem In projitems If (IsPrintableFile(projitem) = True) Then If (projitem.IsOpen( _ EnvDTE.Constants.vsViewKindTextView)) Then projitem.Document.PrintOut() Else Dim doc As Document doc = projitem.Open( _ EnvDTE.Constants.vsViewKindTextView).Document doc.PrintOut() doc.Close(vsSaveChanges.vsSaveChangesNo) End If End If PrintItemsInSelectedProject(projitem.ProjectItems) Next End Sub
实际打印发生在此 PrintItemsInSelectedProject 的重载过程中。该代码循环检查项目中的所有项,通过查看文件的扩展名来检查该文件是否可以打印。(IsPrintableFile 是项目中的一个例程。如下所示。)
Function IsPrintableFile( _ ByVal projItem As ProjectItem) As Boolean Dim fileName As String Dim extensions As _ New System.Collections.Specialized.StringCollection ' 如果向项目中添加的 ' 文件为可打印的类型, ' 则将该文件类型的扩展名添加到 ' 此列表。 Dim exts As String() = {".cs", ".vb", _ ".aspx", ".xsd", ".xml", ".xslt", _ ".config", ".htm", ".html", ".css", _ ".js", ".vbs", ".wsf", ".txt", ".cpp", _ ".c", ".h", ".idl", ".def", ".rgs", ".rc"} extensions.AddRange(exts) fileName = projItem.FileNames(1) Return extensions.Contains( _ System.IO.Path.GetExtension(fileName).ToLower()) End Function
如果是可以打印的文件,代码将检查文件是否已经打开,如果已打开,则打印出文档。如果文件没有打开,将使用 Item 对象的 Open 方法来打开该文档,传入 vsViewKindTextView 以指定要查看该文件的代码或文本版本(而非可视化设计器视图)。打印该文档,然后将其关闭(因为宏必须将其打开),不保存任何更改。最后,一旦当前项目项被打印出来后,同一例程将调用当前项的所有子项。这就是此宏的全部内容,不过结果非常有用,不用为了确保打印项目中的所有文件而到处点击。
此宏包含在 VSUtil.vsmacros 文件(包括在本文的下载内容中)中。使用“工具”|“宏”|“加载宏项目”菜单项以将此项目加载到您自己的宏 IDE 和宏资源管理器。
虽然此打印宏可以做很多事情,但是它不会编辑任何文本,或与任何文本进行交互。为了说明如何使用代码,接下来我要讨论我经常在 Visual Studio .NET 中使用的三个宏。
第一个宏为我节省了大量时间,但是也暴露了缺陷,即不符合建议的命名标准。我总是使用匈牙利语表示法“m_”前缀表示某个变量是对应于同一名称的属性(减去前缀“m_”)的内部“成员”变量。我通常通过键入这些内部变量的声明列表来开始类(在开始为每个内部成员变量创建单独的属性声明这个繁琐的任务之前)。经过六个多月的手动过程后,我创建了此宏来获取当前选定的内部成员列表并为各个成员创建单独的属性例程。此宏也许不能直接用于您自己的代码,但是它说明了在您编写您自己的 Visual Studio .NET 宏时会用得上的一些通用技巧。开始,它将使用恰如其名的 GetCurrentlySelectedText 例程来获取当前选定的文本。
Private Function GetCurrentlySelectedText() As String If Not DTE.ActiveDocument Is Nothing Then Dim txt As TextSelection txt = CType(DTE.ActiveDocument.Selection, TextSelection) Return txt.Text Else Return String.Empty End If End Function
如果没有可用的选定文本,将返回空白字符串。接下来,使用正则表达式将选定文本逐行分析以确定相应的属性名称和数据类型。在文本分析过程中,一个 StringBuilder 类将用于构建新属性过程。该过程将在分析完成后作为一个文本块插入。
Dim line, originalCode As String originalCode = GetCurrentlySelectedText() If Not originalCode = String.Empty Then Dim variableName As String Dim publicName As String Dim dataType As String Dim propertyProcedures As New System.Text.StringBuilder Dim lines() As String lines = Split(originalCode, vbLf) Dim r As Regex r = New Regex( _ "(Dim|Private)\s*(?<varname>\S*)" & _ "\s*(As|As New)\s*(?<typename>\S*)", _ RegexOptions.IgnoreCase Or _ RegexOptions.ExplicitCapture) For Each line In lines line = line.Trim If Not line = String.Empty Then Dim mtch As Match mtch = r.Match(line) If mtch.Success Then variableName = _ mtch.Groups("varname").Value.Trim dataType = _ mtch.Groups("typename").Value.Trim '这假定在“m_”的私有变量前 '一直使用 '2 个字符的前缀 publicName = variableName.Substring(2) propertyProcedures.AppendFormat( _ "{0}Public Property {1} As {2}{0}" _ & " Get{0}" _ & " Return {3}{0}" _ & " End Get{0}" _ & " Set(ByVal Value As {2}){0}" _ & " {3} = Value{0}" _ & " End Set{0}" _ & "End Property{0}", _ vbCrLf, publicName, _ dataType, variableName) End If End If Next End If
插入新文本之前,选择点定位到当前所选内容的末尾。然后将该文本放到紧随内部变量组之后,同时创建了新 UndoContext 对象。
DTE.UndoContext.Open("ConvertProperties") Dim txt As TextSelection txt = CType(DTE.ActiveDocument.Selection, TextSelection) txt.Insert(propertyProcedures.ToString, _ vsInsertFlags.vsInsertFlagsInsertAtEnd _ Or vsInsertFlags.vsInsertFlagsContainNewText) txt.SmartFormat() DTE.UndoContext.Close()
进行任何更改之前,先创建一个新的 UndoContext 对象,然后使用 UndoContext.Close() 将其关闭,可以使用户撤消此整个宏操作。如果没有 UndoContext 对象,则只能分别撤消宏中的各个操作。此特定的示例中,因为我将整个插入作为一个操作,所以差别不会太明显。而在执行许多不同步骤的宏内,您就很想设置一个 UndoContext 对象了。
注意:打开已经打开的 UndoContext 将导致异常。您可以使用 UndoContext 对象的 IsOpen 方法( DTE.UndoContext.IsOpen())来检查现有的 UndoContext。
最终的宏将:
Dim m_fred As String Dim m_counter As Integer
转变为:
Public Property fred() As String Get Return m_fred End Get Set(ByVal Value As String) m_fred = Value End Set End Property Public Property counter() As Integer Get Return m_counter End Get Set(ByVal Value As Integer) m_counter = Value End Set End Property
我使用另一个简单的宏将 MSDN boilerplate 版权声明插入到我的代码文件的最顶端。通过 StartOfDocument 方法,我可以移至当前文档的最顶端,在此处我可以插入预先准备好的字符串的全部文本。
'代码下载中包含完整的字符串 Dim sCopyright As String _ = "'Copyright © {1} Microsoft Corporation{0}" & _ "'All rights reserved.{0}'{0}" & _ "'THIS CODE AND INFORMATION IS PROVIDED """ & _ "AS IS"" WITHOUT WARRANTY OF ANY KIND, EITHER{0}" & _ "'EXPRESSED OR IMPLIED...." Sub PasteCopyrightHeading() Dim crText As String '{0} = 行末 '{1} = 年 '{2} = 月 crText = String.Format(sCopyright, _ vbCrLf, CStr(Now.Year), _ String.Format("{0:Y}", Now)) Dim objTextSelection As TextSelection objTextSelection = _ CType(DTE.ActiveDocument.Selection, _ TextSelection) DTE.UndoContext.Open("PasteCopyright") objTextSelection.StartOfDocument(False) objTextSelection.Insert(crText) DTE.UndoContext.Close() End Sub
在实际的代码中,我不会将版权字符串打断和并置于多行,同样您也不应在您的代码中这样做。这会降低效率。(有关原因的详细信息,请参见本文 [英文]。)我只是出于格式目的才在示例代码中这样做。请注意,我没有检查版权字符串是否已经存在,而是只管进行添加。在此案例中,我的编程风格较为随意。因为我只是在为一位用户编写,而他可以完全接受这些宏中的缺陷。根据读者的不同,您也许需要查看文档并检查新文本是否已经存在。
我要向您展示的最后一个宏非常简单,但是它说明了自动化代码编辑器中的一个重要概念。我喜欢使用“区域”来整理代码,将所有属性例程聚集到一个“区域”,或将应用程序设置的保存/加载例程聚集在一起。因此我创建了一个宏以使此过程更加容易。我不想键入“区域”的开始,然后转至所需代码的末端再添加结束区域声明,我想只选择代码的一部分,然后点击“添加区域”按钮。不是很棘手,但是我发现该代码令人感兴趣。TextSelection 对象的 Insert 方法功能十分齐备(虽然缺少 Microsoft® IntelliSense® 支持会使其难以发掘所有可用的功能),可以使您在所选内容的开始或结尾处插入文本以及控制插入对选定区域的影响。
Public Sub InsertRegion() Dim regionName As String regionName = InputBox("Region Name?") If Not regionName.Trim = String.Empty Then DTE.UndoContext.Open("InsertRegion", False) Dim activeSelection As TextSelection Dim codeInQuestion As String activeSelection = CType(DTE.ActiveDocument.Selection, _ TextSelection) With activeSelection .Insert(String.Format("#Region ""{0}""{1}", _ regionName, vbCrLf), _ vsInsertFlags.vsInsertFlagsContainNewText Or _ vsInsertFlags.vsInsertFlagsInsertAtStart) .Insert(String.Format("{0}#End Region{0}", vbCrLf), _ vsInsertFlags.vsInsertFlagsInsertAtEnd Or _ vsInsertFlags.vsInsertFlagsContainNewText) End With DTE.UndoContext.Close() End If End Sub
通过在插入“#Region”行时指定 vsInsertFlagsInsertAtStart,将其放在当前选定文本的前面。包含 vsInsertFlagsContainNewText 标志意味着当前所选内容(突出显示的文本区域)将自动扩展以包括此新行。
这三个很小的宏是 Visual Studio .NET 2003 中能够进行的自定义和自动化的非常简单的示例,但是我希望它们有助于您入门。如果您想深入了解编写宏或 Visual Studio .NET 的其他自定义形式,建议使用以下资源:
在一些 Coding4Fun 专栏文章的结尾,我会提出几个编码问题,如果您对此感兴趣,可以研究研究。针对本文,问题在于创建宏、外接程序或其他 Visual Studio .NET 相关代码。宏当然会在 Visual Basic .NET 中,但是任何托管代码均可用于外接程序或其他工具。请将您的作品张贴到 GotDotNet(英文),并给我发送电子邮件([email protected][英文])说明您所作的工作以及您感兴趣的原因。您可以随时给我发送电子邮件,但请您只发送指向代码示例的链接,而不要发送示例本身(我先替我的收件箱谢谢您)。
您对嗜好者内容有何见解?请将您的见解发送至 [email protected](英文),并祝编码愉快!
从编码中获得乐趣