无论您怎样努力尝试,都无法预测每个用户的需求。而且不管您是否能够按时或按产品版本获得回报,经常添加一些新功能都是非常诱人的好处,尽管这常常是单调乏味的工作。但是,如果您的应用程序具有足够的灵活性,用户通常就能够实现他们自己的解决方案。本月,Rod Stephens 阐述如何向您的 VB.NET 程序中添加脚本。然后,在用户请求新的功能时,您就可以让他们亲自动手。
最近,我一直在着手撰写我的最新著作的第二版草稿。出版社使用一种特殊的“字符样式”来标记应当在段落中设置为代码的文本,因此,我需要一种简单的方法来高亮显示、标记或取消标记文本。如果我让 Microsoft 对 Word 进行升级,以便为我提供这种能力,成功的机率会有多大?如果您一笑置之,您是对的。根本不可能。
事实上,我的小小心愿没有出现在 Microsoft 的待完成事项清单中并不是什么大事,因为 Word 有一个良好集成的脚本撰写环境,允许您使用 VBA 代码来控制该应用程序。在记录几个宏并对代码进行几分钟的测试之后,我在工具栏上拥有了自己的一组按钮,它们可以设置、清除或切换字符样式。(实际上为这些按钮设计可爱的小图片比编写代码所花的时间更长。是否觉得似曾相识?)
对于我们之中使用 Word 或其他 Office 应用程序的人员,撰写脚本无疑能节省大量的时间。一些好消息是,对于使用您的 应用程序的用户来说,它确实可以节省时间。
VB6 脚本
在 VB6 中,撰写脚本非常简单。首先,选择 Project 菜单中的 References 命令,并添加对 Microsoft 脚本控件的引用。在代码中,创建该控件的一个实例并将其语言属性设置为“VBScript”。
使用该控件的 AddObject 方法通知它允许脚本使用哪些个对象。该代码向 AddObject 传递所使用脚本的名称及相应的引用。例如,它可能调用某个 PictureBox“TheCanvas”。然后,脚本就可以按如下语句操作 PictureBox:
TheCanvas.Line (1440, 1440)-Step(2880, 1440), _ vbRed, BF
下一步,使用该控件的 AddCode 方法为它提供一些脚本代码。最后,调用 Run 方法执行由该脚本定义的例程。
清单 1 演示了 VB6Script 示例程序如何执行一个较小的硬编码脚本,该示例程序包含在本月的下载中。您可以仅简单地从文本文件或数据库中加载该脚本 ─ 或者让用户输入。
清单 1. 在 VB6 中使用 Microsoft 脚本控件撰写脚本比较简单。
' Make a MSScriptControl. (See source for Rod's ' fully commented code. I cut most. Ed.) Dim script_control As New _ MSScriptControl.ScriptControl script_control.Language = "VBScript" script_control.AddObject "TheForm", Me script_control.AllowUI = True ' Enter the script code. script_control.AddCode _ "Sub Main()" & vbCrLf & " TheForm.BackColor = _ vbRed" & vbCrLf & " TheForm.Caption = ""VB6 _ scripting is easy!""" & vbCrLf & " TheForm.Width _ = 5 * 1440" & vbCrLf & " TheForm.Height = _ 3 * 1440" & vbCrLf & "End Sub" ' Execute the Main subroutine. script_control.Run "Main"
VB.NET 脚本
对这种简单、历经时间检验以及有效的解决方案,您认为 Microsoft 可能会在 VB.NET 中使用类似的策略,对吗?不对,Microsoft 决定在 .NET 中采用一种更强大、更复杂的方法。这种新技术称为 VSA (Visual Studio for Applications)。[请参阅新闻组 microsoft.public.dotnet.scripting,http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfmicrosoftvsahierarchy.asp 和 http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfmicrosoftvsa.asp,获取有关 VSA 的更多信息,─ 编注]
ScriptDraw 示例程序(也包含在下载中)是一个允许您执行脚本的简单绘图程序。该程序包含一个定义两个类的 DLL:Segment(使用宽度和颜色来表示行分段)和 Picture(表示包含 Segment 集合的绘图)。主程序允许您单击和拖放,向程序全局的 Picture 对象中添加 Segment。没有十分特别的地方。
该程序允许您使用脚本以编程方式添加 Segment。与手工创建相比,它允许您构建更为复杂的图片。我已经在下载中包含了一些示例脚本,例如绘制几百个 Segment 来生成有趣的曲线。
允许撰写脚本的程序需要公开脚本所使用的对象。ScriptDraw 程序允许脚本操作其主 Picture 对象。与脚本控件相比,VSA 允许脚本以更多的方式与这些对象进行交互。但是,为了提供这些额外功能,VSA 需要了解有关这些对象的更多信息。比以前更多的信息!
为了获取它所需的这些额外信息,VSA 需要查看定义这些对象类的程序集。(您无法提供比这更多的信息!)而且,除非该程序集是独立编译的 DLL,否则它就不能工作。这是因为如果在可执行程序中定义这些对象,VSA 就无法获取所需的信息。这意味着您的应用程序至少需要包括两个编译模块:DLL 定义可编写脚本的类和 可执行主程序。
在这两个部分中构建应用程序并进行测试之后,您可以开始向主程序中添加 VSA。首先,添加对 VSA 库的引用。在“解决方案资源管理器”中,右键单击该项目的“引用”项并选择“添加引用”。在 .NET 选项卡中,选择 Microsoft.Vsa、Microsoft.VisualBasic.Vsa 以及 System.Reflection 库。
总共只有两个步骤:构建 IVsaSite 类和使用该类。遗憾的是,这两个步骤都相当长。
构建 IvsaSite 类
要使用 VSA,您需要构建一个实现 IVsaSite 接口的类。之后,VSA 将使用该对象与您的程序进行交互。该类存储对程序可编写脚本对象的引用。
清单 2 演示由 ScriptDraw 示例程序使用的 ScriptDrawVsaSite 类。
清单 2. 一个支持 VSA 的 IVsaSite 类。
Public Class ScriptDrawVsaSite Implements IVsaSite Private m_Objects As New Hashtable Public Sub AddObject(ByVal object_name As String, _ ByVal object_reference As Object) m_Objects.Add(object_name, object_reference) End Sub Public Sub GetCompiledState(ByRef pe() As Byte, _ ByRef debugInfo() As Byte) Implements _ Microsoft.Vsa.IVsaSite.GetCompiledState pe = Nothing debugInfo = Nothing End Sub Public Function GetEventSourceInstance(ByVal _ itemName As String, ByVal eventSourceName _ As String) As Object Implements _ Microsoft.Vsa.IVsaSite.GetEventSourceInstance Try Dim result As Object = _ m_Objects.Item(eventSourceName) Return result Catch ex As Exception Throw New VsaException_ (VsaError.EventSourceTypeInvalid) End Try End Function Public Function GetGlobalInstance(ByVal name _ As String) As Object Implements _ Microsoft.Vsa.IVsaSite.GetGlobalInstance Try Dim result As Object = m_Objects.Item(name) Return result Catch ex As Exception Throw New VsaException_ (VsaError.GlobalInstanceInvalid) End Try End Function Public Sub Notify(ByVal notify As String, ByVal _ info As Object) Implements _ Microsoft.Vsa.IVsaSite.Notify End Sub Public Function OnCompilerError(ByVal [error] As _ Microsoft.Vsa.IVsaError) As Boolean Implements Microsoft.Vsa.IVsaSite.OnCompilerError Dim msg1 As String = _ "Error on line " & [error].Line & vbCr & vbCr Dim msg2 As String = _ [error].LineText & vbCr Dim dlg As New dlgError dlg.rchError.Text = msg1 & msg2 & _ [error].Description dlg.rchError.Select(Len(msg1) + _ [error].StartColumn - 1, [error].EndColumn - _ [error].StartColumn + 1) dlg.rchError.SelectionColor = Color.Red dlg.rchError.SelectionFont = _ New Font(dlg.rchError.SelectionFont, _ FontStyle.Bold) If dlg.ShowDialog() = DialogResult.OK Then Return True Else Return False End If End Function End Class
该类包括一个命名为 m_Objects 的哈希表,它在该表中存储对可编写脚本对象的引用。
AddObject 方法使用对象的名称作为其关键字,向哈希表中添加一个新的对象。(这与脚本控件存储对象及相关名称的方式非常相似。)
虽然 GetCompiledState 子例程不是很有趣,但 GetEventSourceInstance 函数确实如此。如果脚本要捕获来自其中一个可编写脚本对象的事件,VSA 调用 GetEventSourceInstance 来获取对感兴趣对象的引用。例如,假设该脚本要为名为 btnExecute 的按钮定义一个 Click 事件处理程序。然后,VSA 使用 GetEventSourceInstance 函数来获取对名为 btnExecute 对象的引用。GetEventSourceInstance 只使用该名称在哈希表中查找并返回对象。
您的应用程序可以为操作脚本定义一个全局对象。正如 VSA 使用 GetEventSourceInstance 函数查找作为事件源的对象一样,它使用 GetGlobalInstance 函数检索对这些全局对象的引用。例如,ScriptDraw 程序在名为 ThePicture 的站点类中存储对其主 Picture 对象的引用。在该脚本调用 ThePicture.MakeSegment 时,VSA 使用 GetGlobalInstance 来获取对该对象的引用。与 GetEventSourceInstance 函数类似,GetGlobalInstance 只获取来自哈希表的对象并返回该对象。
下一个有趣的方法是 OnCompilerError,并且它比您想象的还要有趣得多。如果 VSA 编译脚本时出错,它就调用该方法,向它传递有关错误的信息。事实上,它传递许多有关错误的真正有用的信息。它为您提供错误描述、包含错误的行号、行文本以及该错误在行中的位置。
该版本的 OnCompilerError 在 RichTextBox(位于名为 dlgError 的窗体上)中显示错误消息和包含错误的行。它使用错误信息以红色高亮显示错误,因此,很容易就可以查明何处出错。这是对脚本控件的一个巨大的改进。通常,在人们被脚本控件弄模糊的时候,一提起它,就会想到某种模糊的东西,例如“对象不支持这种属性或方法”。
如果用户单击错误窗体的“Continue”按钮,错误对话框返回“OK”,而 OnCompilerError 返回 True,以通知 VSA 继续处理脚本。如果用户单击“Stop”,OnCompilerError 返回 False,以通知 VSA 停止处理脚本。
使用 IvsaSite 类
在构建好 IVsaSite 类之后,您就可以运行脚本。遗憾的是,运行一个 VSA 脚本也相当复杂。清单 3 演示了执行脚本的示例程序的代码。
清单 3. 该代码执行一个含有 VSA 的脚本。
' Execute the script. Private Sub ExecuteScript(ByVal script As String) Dim vsa_engine As New VsaEngine Dim vsa_site As New ScriptDrawVsaSite vsa_site.AddObject("ThePicture", g_Picture) vsa_site.AddObject("MyPicture", g_Picture) vsa_engine.RootMoniker = _ "ScriptDrawVsa://Picture/Draw" vsa_engine.Site = vsa_site vsa_engine.InitNew() vsa_engine.RootNamespace = "ScriptDrawNS" vsa_engine.RevokeCache() vsa_engine.GenerateDebugInfo = True Dim items As IVsaItems = vsa_engine.Items Dim reference_item As IVsaReferenceItem reference_item = CType(items.CreateItem_ ("system.dll", VsaItemType.Reference, _ VsaItemFlag.None), IVsaReferenceItem) reference_item.AssemblyName = "system.dll" reference_item = CType(items.CreateItem_ ("mscorlib.dll", VsaItemType.Reference, _ VsaItemFlag.None), IVsaReferenceItem) reference_item.AssemblyName = "mscorlib.dll" reference_item = CType(items.CreateItem_ ("system.drawing.dll", VsaItemType.Reference, _ VsaItemFlag.None), IVsaReferenceItem) reference_item.AssemblyName = "system.drawing.dll" Dim assembly_name As String = [Assembly]._ GetAssembly(GetType(Picture)).Location reference_item = CType(items.CreateItem_ (assembly_name, VsaItemType.Reference, _ VsaItemFlag.None), IVsaReferenceItem) reference_item.AssemblyName = assembly_name Dim global_item As IVsaGlobalItem global_item = CType(items.CreateItem("ThePicture",_ VsaItemType.AppGlobal, VsaItemFlag.None), _ IVsaGlobalItem) global_item.TypeString = _ "ScriptDrawObjects.Picture" Dim code_item As IVsaCodeItem = CType(items._ CreateItem("Script", VsaItemType.Code, _ VsaItemFlag.None), IVsaCodeItem) code_item.SourceText = script code_item.AddEventSource("MyPicture", _ "ScriptDrawObjects.Picture") vsa_engine.Compile() If Not vsa_engine.IsCompiled() Then vsa_engine.Close() Exit Sub End If vsa_engine.Run() Dim vsa_assembly As [Assembly] = _ vsa_engine.Assembly Dim my_type As Type = vsa_assembly.GetType_ (vsa_engine.RootNamespace & ".Script") Dim method_info As MethodInfo = _ my_type.GetMethod("Main") method_info.Invoke(Nothing, Nothing) vsa_engine.Close() End Sub
该代码创建一个新的、执行脚本的 VsaEngine。它还创建一个新的 ScriptDrawVsaSite 对象(以前的内容所描述的类)。
它使用站点对象的 AddObject 方法来保存对该程序全局 Picture 对象 g_Picture 的引用。它保存了两个对具有不同名称(ThePicture 和 MyPicture)的同一个对象的引用。出于某些原因,VSA 嘲笑我对全局变量和事件源使用相同对象名称的尝试。通过使用 ThePicture 作为全局对象并捕获由 MyPicture 引发的事件,我规避了这个问题。
下一步,该代码初始化 VSA 引擎。它将 RootMoniker 属性设置为 ScriptDrawVsa://Picture/Draw。该值的形式为 protocol://path,其中的 protocol 不是 一个标准协议(如 ftp 或 http)。这只是一个标识服务器中引擎实例的虚构字符串。它在服务器上应当是唯一的,这样,系统就可以从服务器的其他引擎通知该实例。
然后,该代码将引擎的 Site 属性设置为站点对象,并调用 InitNew 方法。然后,它将引擎的 RootNamespace 属性设置为 ScriptDrawNS。这是另外一个多少有点虚构的字符串。
程序再调用该引擎的 RevokeCache 方法。这样就将系统数据从全局程序集缓存 (GAC) 中清除出去(如果有的话)。如果您运行程序并执行一个脚本,该脚本可能存储于 GAC(读为“gack”)中。如果您执行一个新的脚本,该引擎将仍然使用旧的脚本,除非您将其清除出去。
接下来,如果脚本中有错误,则该代码将引擎的 GenerateDebugInfo 属设置为 True,使它调用 OnCompilerError。然后,它获取对该引擎的 Item 集合的引用,该集合包含有关脚本将要使用的库、可编写脚本的对象以及定义那些对象的程序集的信息。
程序使用该集合来添加对脚本可能需要的某些公共库的引用。本示例加载对 system.dll、mscorlib.dll 以及 system.drawing.dll 的引用。然后它加载对定义 Picture 和 Segment 类的程序集的引用。注意代码如何使用 [Assembly].GetAssembly 以获取程序集位置,而不是进行硬编码。
然后,程序生成一个描述全局对象的 IVsaGlobalItem 对象。程序将此对象命名为 ThePicture,并表明它来自于 ScriptDrawObjects.Picture 类。VSA 使用该类的信息来确定要使用该对象的脚本应当可以使用哪些公共属性和方法。然后,程序产生一个表示脚本代码的对象。
接下来,代码添加一个名为 MyPicture 的事件源,因此,脚本可以捕获“MyPicture”对象的事件。代码通知 VSA 该对象的类型为 ScriptDrawObjects.Picture。VSA 使用此信息来确定该对象可能引发的事件。
然后,代码调用引擎的 Compile 方法来编译脚本。如果编译失败,VSA 就调用 OnCompilerError 和站点类通知用户有关的错误。在这种情况下,引擎的 IsCompiled 属性仍然为 False。子例程检查该值,并在编译失败时退出。
现在,程序调用引擎的 Run 方法启动引擎。虽然这不会运行任何脚本代码,但是我们离目标又近了一步。
程序获得一个描述 Script 模块的 Type。这就是引擎根命名空间中的 Script 模块。它调用 Type 的 GetMethod 函数获取有关在脚本中定义的 Main 子例程的信息,并调用该子例程。.
关闭引擎就完成了代码。
与使用脚本控件相比,这看起来更为复杂,不是吗?
演示脚本
对于幕后的所有“丑陋”代码,现在是获得一些乐趣的时候。清单 4 演示了 ScriptDraw 示例程序的一个脚本。您将很快就会发现它是 VB.NET 代码,而不是 VBScript 代码。这是对脚本控件的一个相当大的改进,至少对喜欢 VB.NET 的开发人员是这样。(当然,如果您不喜欢,您就不会长途跋涉地从清单 2 一直阅读到清单 3。)
清单 4. 该脚本绘制一个摆线。
Imports System Imports System.Math Imports System.Drawing Imports ScriptDrawObjects Module Script Private Function X(ByVal t As Single) As Integer Return CInt(35 * (2 * Cos(t) + Cos(17 * t))) End Function Private Function Y(ByVal t As Single) As Integer Return CInt(35 * (2 * Sin(t) - Sin(17 * t))) End Function Private m_Random As New Random Public Sub Main() Const cx As Integer = 150 Const cy As Integer = 150 Dim t As Single = 0 Dim dt As Single = 0.01 Dim x1, y1, x2, y2 As Integer ThePicture.Clear() x2 = cx + X(t) y2 = cy + Y(t) t += dt Do While t < 2 * PI x1 = x2 y1 = y2 x2 = cx + X(t) y2 = cy + Y(t) ThePicture.MakeSegment(x1, y1, x2, y2, _ Color.Black, 1) t += dt Loop x1 = cx + X(0) y1 = cy + Y(0) ThePicture.MakeSegment(x1, y1, x2, y2, _ Color.Black, 1) End Sub Private Sub MyPicture_CreatedSegment(ByVal seg _ As Segment) Handles MyPicture.CreatedSegment seg.ForeColor = Color.FromArgb(&HFF000000 + _ QBColor(m_Random.Next(0, 16))) End Sub End Module
该代码以一些 Imports 语句开始。清单 3 中的代码为引擎提供它所需的引用,以对这些语句进行处理。该脚本定义了几个提供有关摆线各点坐标的函数,然后创建一个全局模块的 Random 对象。
Main 子例程定义一些变量,然后调用 ThePicture.Clear。在调用时,返回到清单 3 顶部附近的 AddObject。第一次调用将向名为 ThePicture 的站点对象添加对程序变量 g_Picture 的引用。现在,在脚本调用 ThePicture.Clear 时,引擎调用站点对象的 GetGlobalInstance 函数获取对 g_Picture 的引用,并调用该对象的 Clear 方法。
然后,脚本在摆线的一系列点中进行循环,调用 ThePicture.MakeSegment 将分段添加到图片中。MakeSegment 函数返回新的 Segment 对象,因此,程序可以保存对该对象的引用并对其进行操作。例如,它可能更改新的 Segment 坐标、宽度或颜色。
此示例完成一些更有趣的事情。只要脚本调用 Picture 对象的 MakeSegment 函数,Picture 对象就会引发其 CreatedSegment 事件,向事件处理程序传递对新的 Segment 的引用。该脚本的 MyPicture_CreatedSegment 子例程捕获事件并随机地设置其颜色。
图 1 显示结果(我对事件处理程序进行了注释,使所有分段为黑色,这样,此处的显示效果可能会好一些)。
小结
虽然在 VB.NET 中编写脚本不如在 VB6 中简单,但是它确实为您提供了一些新功能。它允许您编写 VB.NET 代码,而不是使用发展受限的 VBScript 语言,允许脚本代码捕获事件,并提供更加 完善的错误信息(如果脚本还不算完美的话)。
您必须确定这些好处是否能够证明,在独立的 DLL 中定义可编写脚本的类、构建 IVsaSite 类以及使用其RootMoniker、RootNamespace、引用和其他特色来初始化 VSA 引擎所带来的麻烦是值得的。
如果您认为不值得,则您仍然可以在 VB.NET 中使用 Microsoft 脚本控件。由于某种原因,使用 VBScript 来控制 VB.NET 应用程序似乎还不太令人满意。另一方面,您的用户可能已经了解 VBScript。而且如果他们属于典型的 VBA/VBScript Office 宏编程类型,则他们不会因为您让他们学习 VB.NET 而表示感谢。
有关 Hardcore Visual Basic 和 Pinnacle Publishing 的详细信息,请访问其网站 http://www.pinpub.com/
注:这不是 Microsoft Corporation 的网站, Microsoft 对该网站的内容不承担责任。
本文是从 Hardcore Visual Basic 2004 年 10 月号转载的。版权所有 2004,Pinnacle Publishing, Inc.(除非另行说明)。保留所有权利。Hardcore Visual Basic 是 Pinnacle Publishing, Inc. 独立发行的产品。未经 Pinnacle Publishing, Inc. 事先同意,不得以任何形式使用或复制本文的任何部分(评论文章中的简短引用除外)。要联系 Pinnacle Publishing, Inc.,请致电 1-800-788-1900。