C#發(fā)現(xiàn)之旅第十四講 基于動態(tài)編譯的VB.NET腳本引擎
本章說明
在前面章節(jié)中,筆者使用了反射和動態(tài)編譯技術(shù)實現(xiàn)了快速ORM框架,在本章中筆者將繼續(xù)使用這些技術(shù)來實現(xiàn)一個VB.NET的腳本引擎,使得人們在開發(fā)中能實現(xiàn)類似MS Office那樣實現(xiàn)VBA宏的功能。本章配套的演示程序為/Files/xdesigner/MyVBAScript.zip.
腳本的原理
腳本,也可稱為宏,是一種應(yīng)用系統(tǒng)二次開發(fā)的技術(shù),它能在應(yīng)用系統(tǒng)提供的一個容器環(huán)境中運行某種編程語言,這種編程語言代碼調(diào)用應(yīng)用系統(tǒng)提供的編程接口,使得應(yīng)用系統(tǒng)暫時“靈魂附體”,無需用戶干預(yù)作而執(zhí)行一些自動的操作,此時應(yīng)用系統(tǒng)稱為“宿主”。
腳本也采用多種編程語言,比如JavaScript語言,VBScript語言或者其他的,若采用VB則稱為VB腳本。
下圖為腳本原理圖

下圖為常規(guī)編程開發(fā)軟件的原理圖

腳本相對于常規(guī)的軟件開發(fā)用的編程語言有著很大的不同。首先是腳本是不能獨立運行的,必須在某個應(yīng)用系統(tǒng)搭建的容器環(huán)境中運行,脫離這個環(huán)境則腳本代碼毫無作用,其邏輯和功能和應(yīng)用系統(tǒng)的功能聯(lián)系非常緊密。腳本代碼不會事先編譯,而是解釋執(zhí)行或者臨時編譯執(zhí)行的,而且腳本代碼的修改不會導(dǎo)致應(yīng)用系統(tǒng)的重新編譯和部署,而且腳本代碼發(fā)生修改,應(yīng)用系統(tǒng)不需要重新啟動即可應(yīng)用修改后的腳本代碼,而且運行腳本的應(yīng)用系統(tǒng)可以不是DLL,而是純粹的EXE。
腳本語言大多是動態(tài)語言,所謂動態(tài)語言就是程序代碼在編寫時已經(jīng)假設(shè)操作的對象的類型,成員屬性或方法的信息,而編譯器不會進行這方面的檢查。C#不是動態(tài)語言,是靜態(tài)語言,因為它在編譯時會嚴格的檢查代碼操作的對象的類型,成員信息,稍有不對則會報編譯錯誤。VB.NET源自VB,是動態(tài)語言,它在編譯時不會嚴格的檢查對象的類型及其成員信息,執(zhí)行后期綁定,而是在運行時檢查,若運行時發(fā)現(xiàn)對象類型和成員信息錯誤,則會報運行時錯誤。腳本技術(shù)應(yīng)當非常靈活和自由,袁某人覺得此時使用C#這種靜態(tài)語言不是明智之舉,而應(yīng)當使用類似VB.NET這樣的動態(tài)語言。
而常規(guī)的軟件開發(fā)而生成的軟件大多是事先編譯好的,和應(yīng)用系統(tǒng)是獨立的,軟件是調(diào)用應(yīng)用系統(tǒng)的功能而不是應(yīng)用系統(tǒng)的一部分。軟件代碼修改會導(dǎo)致軟件的重新編譯和部署,應(yīng)用系統(tǒng)必須提供DLL格式的程序集文件。
微軟的很多軟件產(chǎn)品有有VBA的功能,比如MS Office,甚至VS.NET集成開發(fā)環(huán)境也有VBA宏的功能。腳本提供給應(yīng)用系統(tǒng)二次開發(fā)的能力,而且這種二次開發(fā)能力簡單靈活,部署方便。
在應(yīng)用方面腳本技術(shù)帶來的最大好處就是簡單靈活,部署方便。腳本代碼以純文本的格式進行存儲,修改方便,而且腳本修改后,應(yīng)用系統(tǒng)無需重新啟動而能立即使用新的腳本,腳本代碼中能實現(xiàn)比較復(fù)制的邏輯控制,能響應(yīng)應(yīng)用系統(tǒng)的事件,能一定程度上擴展應(yīng)用系統(tǒng)的功能,這有點類似數(shù)據(jù)庫中的存儲過程。
但腳本功能運行在應(yīng)用系統(tǒng)提供的容器環(huán)境中,其功能是受到嚴格限制的,一些腳本還受到系統(tǒng)權(quán)限的限制。因此腳本只能有限的擴展應(yīng)用系統(tǒng)的功能,若所需功能比較復(fù)雜,腳本可能無法實現(xiàn),此時還得依賴傳統(tǒng)編程。不過在很多情況下,腳本還是能發(fā)揮很大的作用。【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
VB.NET腳本原理
VB.NET腳本就是采用VB.NET語法的腳本。VS.NET集成開發(fā)環(huán)境提供的宏也是采用VB.NET語法。微軟.NET框架提供了一個腳本引擎,那就是在程序集microsoft.visualbasic.vsa.dll中的類型Microsfot.VisualBasic.Vsa.VsaEngine,該類型在微軟.NET框架1.1和2.0中都有,使用起來不算容易,而且在微軟.NET框架2.0中VsaEngine類型標記為“已過時”。在此筆者不使用VsaEngine類型來實現(xiàn)VB.NET腳本,而是使用動態(tài)編譯技術(shù)來實現(xiàn)腳本引擎。
使用動態(tài)編譯技術(shù)實現(xiàn)VB.NET腳本引擎的原理是,程序?qū)⒂脩舻哪_本代碼字符串進行一些預(yù)處理,然后調(diào)用Microsoft.VisualBasic.VBCodeProvider類型的CompileAssemblyFromSource函數(shù)進行動態(tài)編譯,生成一個臨時的程序集對象,使用反射技術(shù)獲得程序集中的腳本代碼生成的方法,主程序?qū)凑彰Q來調(diào)用這些腳本代碼生成的方法。若用戶修改了腳本代碼,則這個過程重復(fù)一次。
VB.NET腳本引擎設(shè)計
這里筆者將用倒推法來設(shè)計VB.NET腳本引擎,也就是從預(yù)期的最終使用結(jié)果來反過來設(shè)計腳本引擎。
主程序?qū)凑彰Q來調(diào)用腳本方法,很顯然VB.NET代碼編譯生成的是一個.NET程序集類庫,為了方便起見,筆者將所有的VB.NET腳本方法集中到一個VB.NET腳本類型。筆者將腳本方法定義為靜態(tài)方法,主要有兩個好處,首先腳本引擎不必生成對象實例,其次能避免由于沒有生成對象實例而導(dǎo)致的空引用錯誤,這樣能減少腳本引擎的工作量。
在VB.NET語法中,可以使用代碼塊“public shared sub SubName()”來定義一個靜態(tài)過程,但筆者不能要求用戶在編寫VB.NET腳本代碼時使用“public shared sub SubName()”的VB.NET語法,而只能使用“sub SubName()”這樣比較簡單的語法。同樣用戶在腳本中定義全局變量時不能使用“private shared VarName as TypeName”的語法,而是簡單的使用“dim VarName as TypeName”的語法。這時筆者可以使用VB.NET語法的模塊的概念。在VB.NET語法中,將代碼塊“Module ModuleName ……. End Module”中的所有的代碼編譯為靜態(tài)的。比如把“sub SubName”編譯成“public shared sub SubName()”,把“dim VarName as TypeName”編譯為“public shared VarName as TypeName”。這樣借助VB.NET模塊的概念就能解決了這個問題。
在一些腳本中筆者經(jīng)常可以看見類似“window.left”或者“document.location”的方式來使用全局對象,若筆者在VB.NET中直接使用“window.left”之類的代碼必然報“window”對象或者變量找不到的編譯錯誤。
“window”全局變量一般映射到應(yīng)用程序的主窗體。比如“window.left”表示主窗體的左端位置,“window.width”標準主窗體的寬度等等。【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
“document”或者“window”等全局對象是映射到文檔或者主窗體等實例對象的,因此它們的成員不能定義成靜態(tài),為了能實現(xiàn)在腳本代碼中直接使用類似“window.left”的方法來直接使用全局對象,筆者又得使用VB.NET的一個語法特性。在Microsoft.VisualBasic.dll中有一個公開的特性類型“Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute”,該特性是隱藏的,可能不是微軟推薦使用,但在微軟.NET框架1.1和2.0都有這個特性類型,功能也是一樣的。對于一般的編程該特性是用不著的,它可附加在某個類型上,VB.NET編譯器會認為附加了該特性的類型的靜態(tài)屬性值就是全局對象。比如筆者定義了一個GlobalObject類型,附加了StandardModuleAttribute特性,它有一個名為Document的靜態(tài)屬性,在對于腳本中的“document.Location”代碼塊,VB.NET編譯器會針對“document”標識符檢索所有附加了StandardModuleAttribute的類型的靜態(tài)屬性,最后命中GlobalObject類型,于是會自動擴展為“GlobalObject.Document.Location”的代碼。這個過程是在編譯時進行的,在實際運行中不再需要進行這樣的查找,這樣的語法特點是C#所沒有的。上述的這些特點使得VB.NET語法更適合作為腳本的語法。
類似全局對象,在VB.NET語法中具有全局函數(shù)的功能,比如對于Asc函數(shù),它實際上是類型Microsoft.VisualBasic.Strings的一個靜態(tài)成員函數(shù),但在VB.NET中可以在任何時候任何地方直接使用,VB.NET編譯器會將代碼中的Asc函數(shù)自動擴展為“Microsoft.VisualBasic.Strings.Asc”。這個過程是在編譯時進行的,而運行時不再需要這樣的擴展。
.NET框架自帶VB.NET編譯器,它就是在.NET框架安裝目錄下的vbc.exe,在筆者的系統(tǒng)中VB.NET編譯器的路徑是“C:"WINDOWS"Microsoft.NET"Framework"v2.0.50727"vbc.exe”,參考MSDN中關(guān)于VB.NET編譯的命令行的說明,它支持一個名為“imports”的命令行參數(shù)指令。比如可以這樣調(diào)用VB.NET編譯器“vbc.exe /imports:Microsoft.VisualBasic,system,system.drawing 其他參數(shù)”,該參數(shù)的功能是從指定的程序集導(dǎo)入名稱空間。在VB.NET編譯器命令行中使用imports指令和在VB.NET代碼中使用Imports指令是不一樣的。在源代碼中使用Imports指令是用于減少代碼編寫量,而在命令行中使用imports指令是啟動指定名稱空間下的全局對象和全局函數(shù),若一個類型附加了StandardModuleAttribute特性,而且定義了一些靜態(tài)函數(shù)和屬性,但并沒有在編譯器命令行中導(dǎo)入帶類型所在的名稱空間,則VB.NET編譯器不會感知到該類型中定義的全局對象和全局函數(shù),因此在編寫VB.NET代碼時必須使用“類型名稱.靜態(tài)屬性或函數(shù)的名稱”的方式來調(diào)用全局對象和全局函數(shù)。比如若沒有在VB.NET編譯器的命令行參數(shù)中使用“/imports:Microsoft.VisualBasic”參數(shù),則Asc函數(shù)不再是全局函數(shù),若在代碼中直接使用Asc函數(shù)則必然報編譯錯誤,而必須使用“Microsoft.VisualBasic.Strings.Asc”的方式來使用,即使源代碼中使用了“Imports Microsoft.VisualBasic”,也只能用“Strings.Asc”的方式來使用函數(shù)。
如上所述,借助于StandardModuleAttribute特性和編譯器命令行參數(shù)imports,筆者就可以實現(xiàn)VB.NET的全局對象和全局函數(shù)了。
根據(jù)上述說明,筆者設(shè)計如下的參與動態(tài)編譯的VB.NET腳本代碼的結(jié)構(gòu)
|
Option Strict Off Imports System Imports Microsoft.VisualBasic Namespace NameSpaceXVBAScriptEngien Module mdlXVBAScriptEngine
sub 腳本方法1() 'VB.NET代碼 end sub
sub 腳本方法2() 'VB.NET代碼 end sub
End Module End Namespace |
其中斜體部分就是用戶提供的原始腳本代碼,而開頭和結(jié)尾部分是腳本引擎自動添加的,這樣能減少腳本引擎的使用難度。【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
在腳本引擎自動添加的代碼中使用了Imports語句引入的名稱空間,默認添加了System和Microsoft.VisualBasic兩個名稱空間,為了方便使用,可以讓用戶添加其他的名稱空間,比如腳本代碼中大量使用了System.Drawing名稱空間,則可以使用Imports語句導(dǎo)入System.Drawing名稱空間來減少腳本代碼量。
軟件開發(fā)
筆者新建一個XVBAEngine類型,該類型實現(xiàn)了腳本引擎的功能。腳本引擎包含了參數(shù)控制屬性,代碼生成器,動態(tài)編譯,分析和調(diào)用臨時程序集等幾個子功能。
參數(shù)控制屬性
筆者為腳本引擎類型定義了幾個屬性用于保存腳本引擎運行所必備的基礎(chǔ)數(shù)據(jù)。這些屬性中最重要的屬性就是用戶設(shè)置的原始腳本代碼文本。定義該屬性的代碼如下
|
/// <summary> /// 腳本代碼改變標記 /// </summary> private bool bolScriptModified = true;
/// <summary> /// 原始的VBA腳本文本 /// </summary> private string strScriptText = null; /// <summary> /// 原始的VBA腳本文本 /// </summary> public string ScriptText { get { return strScriptText; } set { if (strScriptText != value) { bolScriptModified = true; strScriptText = value; } } } |
在這里ScriptText屬性表示用戶設(shè)置的原始的VBA腳本代碼,實際參與動態(tài)編譯的腳本代碼和原始設(shè)置的原始的VBA腳本代碼是不一致的。當用戶修改了腳本代碼文本,則會設(shè)置bolScriptModified變量的值,腳本引擎運行腳本方法時會檢查這個變量的值來判斷是否需要重新動態(tài)編譯操作。
此外袁某人還定義了其他的一些控制腳本引擎的屬性,其定義的代碼如下
|
private bool bolEnabled = true; /// <summary> /// 對象是否可用 /// </summary> public bool Enabled { get { return bolEnabled; } set { bolEnabled = value; } }
private bool bolOutputDebug = true; /// <summary> /// 腳本在運行過程中可否輸出調(diào)試信息 /// </summary> public bool OutputDebug { get { return bolOutputDebug; } set { bolOutputDebug = value; } } |
編譯腳本
筆者為腳本引擎編寫了Compile函數(shù)用于編輯腳本。編譯腳本的過程大體分為生成腳本代碼文本、編譯腳本編譯、分析腳本程序集三個步驟。
生成腳本代碼文本
VB.NET腳本引擎使用的動態(tài)編譯技術(shù),而動態(tài)編譯技術(shù)的第一個部分就是代碼生成器,腳本大部分代碼都是由主程序提供的,因此其代碼生成器也就是將原始的腳本代碼進行一些封裝而已。【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
根據(jù)上述對運行時腳本的設(shè)計,用戶可以導(dǎo)入其他的名稱空間,于是腳本引擎定義了SourceImports屬性來自定義導(dǎo)入的名稱空間,定義該屬性的代碼如下
|
/// <summary> /// 源代碼中使用的名稱空間導(dǎo)入 /// </summary> private StringCollection mySourceImports = new StringCollection(); /// <summary> /// 源代碼中使用的名稱空間導(dǎo)入 /// </summary> public StringCollection SourceImports { get { return mySourceImports; } } |
在腳本引擎的初始化過程中,程序會默認添加上System和Microsoft.VisualBasic兩個名稱空間。隨后程序使用以下代碼來生成實際參與編輯的腳本代碼文本
|
// 生成編譯用的完整的VB源代碼 string ModuleName = "mdlXVBAScriptEngine"; string nsName = "NameSpaceXVBAScriptEngien"; System.Text.StringBuilder mySource = new System.Text.StringBuilder(); mySource.Append("Option Strict Off"); foreach (string import in this.mySourceImports) { mySource.Append(""r"nImports " + import); } mySource.Append(""r"nNamespace " + nsName); mySource.Append(""r"nModule " + ModuleName); mySource.Append(""r"n"); mySource.Append(this.strScriptText); mySource.Append(""r"nEnd Module"); mySource.Append(""r"nEnd Namespace"); string strRuntimeSource = mySource.ToString(); |
這段代碼功能也比較簡單,首先輸出“Option Strick Off”語句,然后使用mySourceImports輸出若干個Imports語句。這里的mySourceImports是一個字符串列表,用于存放引用的名稱空間,比如“System”,“Microsoft.VisualBasic”等等,用于組成VB.NET腳本的Imports語句。然后輸出Namespace和Module代碼塊來包括了用戶提供的原始代碼文本。這里的strSourceText就是用戶提供的原始代碼文本。最后變量 strRuntimeSource中就包含了實際運行的VB.NET代碼文本。【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
編譯腳本
程序生成完整的VB.NET腳本代碼文本后就可以編譯了,為了提高效率,這里袁某定義了一個靜態(tài)myAssemblies的哈希列表變量,定義該變量的代碼如下
|
/// <summary> /// 所有緩存的程序集 /// </summary> private static Hashtable myAssemblies = new Hashtable(); |
該列表緩存了以前編輯生成的程序集,鍵值就是腳本文本,鍵值就是程序集。若緩存區(qū)中沒有找到以前緩存的程序集那腳本引擎就可以調(diào)用VB.NET編譯器編輯腳本了。
為了豐富腳本引擎的開發(fā)接口,筆者使用以下代碼定義了ReferencedAssemblies屬性。
|
/// <summary> /// VB.NET編譯器參數(shù) /// </summary> private CompilerParameters myCompilerParameters = new CompilerParameters(); /// <summary> /// 引用的名稱列表 /// </summary> public StringCollection ReferencedAssemblies { get { return myCompilerParameters.ReferencedAssemblies; } } |
ReferencedAssemblies保存了編輯腳本時使用的程序集,在初始化腳本引擎時,系統(tǒng)已經(jīng)默認向該列表添加了mscorlib.dll、System.dll、System.Data.dll、System.Xml.dll、System.Drawing.dll、System.Windows.Forms.dll、Microsoft.VisualBasic.dll等.NET框架標準程序集,用戶可以使用該屬性添加第三方程序集來增強腳本引擎的功能。
在前面的說明中,為了實現(xiàn)全局對象和全局函數(shù),需要在VB.NET編譯器的命令上中使用imports指令導(dǎo)入全局對象和全局函數(shù)所在的名稱空間,為此筆者定義了一個VBCompilerImports的屬性來保存這些名稱空間,定義該屬性的代碼如下
|
/// <summary> /// VB編譯器使用的名稱空間導(dǎo)入 /// </summary> private StringCollection myVBCompilerImports = new StringCollection(); /// <summary> /// VB編譯器使用的名稱空間導(dǎo)入 /// </summary> public StringCollection VBCompilerImports { get { return myVBCompilerImports; } } |
在初始化腳本引擎時程序會在VBCompilerImports列表中添加默認的名稱空間Microsoft.VisualBasic。
準備和執(zhí)行編譯的腳本代碼和一些參數(shù)后,腳本引擎就來編譯腳本代碼生成臨時程序集了,筆者使用以下的代碼來進行編譯操作【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
|
// 檢查程序集緩存區(qū) myAssembly = (System.Reflection.Assembly)myAssemblies[strRuntimeSource]; if (myAssembly == null) { // 設(shè)置編譯參數(shù) this.myCompilerParameters.GenerateExecutable = false; this.myCompilerParameters.GenerateInMemory = true; this.myCompilerParameters.IncludeDebugInformation = true; if (this.myVBCompilerImports.Count > 0) { // 添加 imports 指令 System.Text.StringBuilder opt = new System.Text.StringBuilder(); foreach (string import in this.myVBCompilerImports) { if (opt.Length > 0) { opt.Append(","); } opt.Append(import.Trim()); } opt.Insert(0, " /imports:"); for (int iCount = 0; iCount < this.myVBCompilerImports.Count; iCount++) { this.myCompilerParameters.CompilerOptions = opt.ToString(); } }//if
if (this.bolOutputDebug) { // 輸出調(diào)試信息 System.Diagnostics.Debug.WriteLine(" Compile VBA.NET script "r"n" + strRuntimeSource); foreach (string dll in this.myCompilerParameters.ReferencedAssemblies) { System.Diagnostics.Debug.WriteLine("Reference:" + dll); } }
// 對VB.NET代碼進行編譯 Microsoft.VisualBasic.VBCodeProvider provider = new Microsoft.VisualBasic.VBCodeProvider(); #if DOTNET11 // 這段代碼用于微軟.NET1.1 ICodeCompiler compiler = provider.CreateCompiler(); CompilerResults result = compiler.CompileAssemblyFromSource( this.myCompilerParameters, strRuntimeSource ); #else // 這段代碼用于微軟.NET2.0或更高版本 CompilerResults result = provider.CompileAssemblyFromSource( this.myCompilerParameters, strRuntimeSource); #endif // 獲得編譯器控制臺輸出文本 System.Text.StringBuilder myOutput = new System.Text.StringBuilder(); foreach (string line in result.Output) { myOutput.Append(""r"n" + line); } this.strCompilerOutput = myOutput.ToString(); if (this.bolOutputDebug) { // 輸出編譯結(jié)果 if (this.strCompilerOutput.Length > 0) { System.Diagnostics.Debug.WriteLine("VBAScript Compile result" + strCompilerOutput); } }
provider.Dispose();
if (result.Errors.HasErrors == false) { // 若沒有發(fā)生編譯錯誤則獲得編譯所得的程序集 this.myAssembly = result.CompiledAssembly; } if (myAssembly != null) { // 將程序集緩存到程序集緩存區(qū)中 myAssemblies[strRuntimeSource] = myAssembly; } } |
在這段代碼中,首先程序設(shè)置編譯器的參數(shù),并為VB編譯器添加引用的程序集信息,VB.NET編譯器有個名為imports的命令行參數(shù)用于指定全局名稱空間。用法為“/imports:名稱空間1,名稱空間2”,在編譯器命令行中使用imports參數(shù)和在代碼文本中使用imports語句是有所不同的。
然后程序創(chuàng)建一個VBCodeProvider對象開始編譯腳本,對于微軟.NET框架1.1和2.0其操作過程是有區(qū)別的。對微軟.NET1.1還得調(diào)用provider的CreateCompilter函數(shù)創(chuàng)建一個IcodeCompiler對象,然后調(diào)用它的CompileAssemblyFromSource來編譯腳本,而對于微軟.NET框架2.0則是直接調(diào)用provider的CompileAssemblyFromSource來編譯腳本的。
編譯器編譯后返回一個CompilerResults的對象表示編譯結(jié)果,若發(fā)生編譯錯誤程序就輸出編譯錯誤信息。若編譯成功則程序使用編譯結(jié)果的CompileAssembly屬性獲得編輯腳本代碼生成的臨時程序集對象了。然后把程序集對象緩存到myAssemblies列表中。
分析臨時程序集
調(diào)用編譯器編譯腳本代碼后成功的生成臨時程序集后,腳本引擎需要分析這個程序集,獲得所有的可用的腳本方法,其分析代碼為
|
if (this.myAssembly != null) { // 檢索腳本中定義的類型 Type ModuleType = myAssembly.GetType(nsName + "." + ModuleName); if (ModuleType != null) { System.Reflection.MethodInfo[] ms = ModuleType.GetMethods( System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static); foreach (System.Reflection.MethodInfo m in ms) { // 遍歷類型中所有的靜態(tài)方法 // 對每個方法創(chuàng)建一個腳本方法信息對象并添加到腳本方法列表中。 ScriptMethodInfo info = new ScriptMethodInfo(); info.MethodName = m.Name; info.MethodObject = m; info.ModuleName = ModuleType.Name; info.ReturnType = m.ReturnType; this.myScriptMethods.Add(info); if (this.bolOutputDebug) { // 輸出調(diào)試信息 System.Diagnostics.Debug.WriteLine("Get vbs method """ + m.Name + """"); } }//foreach bolResult = true; }//if }//if |
在這段代碼中,程序首先獲得腳本模塊的類型,在這里類型全名為“NameSpaceXVBAScriptEngien. mdlXVBAScriptEngine”,然后使用反射獲得該類型中所有的公開或未公開的靜態(tài)成員方法對象,對于其中的每一個方法創(chuàng)建一個ScriptMethodInfo類型的腳本方法信息對象來保存這個方法的一些信息,將這些信息保存到myScriptMethods列表中供以后調(diào)用。
筆者配套定義了ScriptMethodInfo類型和myScriptMethods列表,定義它們的代碼如下
|
/// <summary> /// 所有腳本方法的信息列表 /// </summary> private ArrayList myScriptMethods = new ArrayList(); /// <summary> /// 腳本方法信息 /// </summary> private class ScriptMethodInfo { /// <summary> /// 模塊名稱 /// </summary> public string ModuleName = null; /// <summary> /// 方法名稱 /// </summary> public string MethodName = null; /// <summary> /// 方法對象 /// </summary> public System.Reflection.MethodInfo MethodObject = null; /// <summary> /// 方法返回值 /// </summary> public System.Type ReturnType = null; /// <summary> /// 指向該方法的委托 /// </summary> public System.Delegate MethodDelegate = null; } |
使用腳本方法信息列表,腳本引擎調(diào)用腳本方法時就不需要使用反射查找腳本方法了,只需要在腳本方法信息列表中快速的查找和調(diào)用。
調(diào)用腳本
腳本引擎前期完成的大量的工作就是為了最后能調(diào)用腳本,為此筆者定義了、Execute函數(shù)用于調(diào)用指定名稱的腳本方法。定義該函數(shù)的代碼如下
|
/// <summary> /// 執(zhí)行腳本方法 /// </summary> /// <param name="MethodName">方法名稱</param> /// <param name="Parameters">參數(shù)</param> /// <param name="ThrowException">若發(fā)生錯誤是否觸發(fā)異常</param> /// <returns>執(zhí)行結(jié)果</returns> public object Execute(string MethodName, object[] Parameters, bool ThrowException) { // 檢查腳本引擎狀態(tài) if (CheckReady() == false) { return null; } if (ThrowException) { // 若發(fā)生錯誤則拋出異常,則檢查參數(shù) if (MethodName == null) { throw new ArgumentNullException("MethodName"); } MethodName = MethodName.Trim(); if (MethodName.Length == 0) { throw new ArgumentException("MethodName"); } if (this.myScriptMethods.Count > 0) { foreach (ScriptMethodInfo info in this.myScriptMethods) { // 遍歷所有的腳本方法信息,不區(qū)分大小寫的找到指定名稱的腳本方法 if (string.Compare(info.MethodName, MethodName, true) == 0) { object result = null; if (info.MethodDelegate != null) { // 若有委托則執(zhí)行委托 result = info.MethodDelegate.DynamicInvoke(Parameters); } else { // 若沒有委托則直接動態(tài)執(zhí)行方法 result = info.MethodObject.Invoke(null, Parameters); } // 返回腳本方法返回值 return result; }//if }//foreach }//if } else { // 若發(fā)生錯誤則不拋出異常,安靜的退出 // 檢查參數(shù) if (MethodName == null) { return null; } MethodName = MethodName.Trim(); if (MethodName.Length == 0) { return null; } if (this.myScriptMethods.Count > 0) { foreach (ScriptMethodInfo info in this.myScriptMethods) { // 遍歷所有的腳本方法信息,不區(qū)分大小寫的找到指定名稱的腳本方法 if (string.Compare(info.MethodName, MethodName, true) == 0) { try { // 執(zhí)行腳本方法 object result = info.MethodObject.Invoke(null, Parameters); // 返回腳本方法返回值 return result; } catch (Exception ext) { // 若發(fā)生錯誤則輸出調(diào)試信息 System.Console.WriteLine("VBA:" + MethodName + ":" + ext.Message); } return null; }//if }//foreach }//if }//else return null; }//public object Execute |
這里函數(shù)參數(shù)為要調(diào)用的腳本方法的名稱,不區(qū)分大小寫,調(diào)用腳本使用的參數(shù)列表,還有控制是否拋出異常的參數(shù)。在函數(shù)里面,程序遍歷myScriptMethods列表中所有以前找到的腳本方法的信息,查找指定名稱的腳本方法,若找到則使用腳本方法的Invoke函數(shù)執(zhí)行腳本方法,如此陳旭就能調(diào)用腳本了。
為了豐富腳本引擎的編程接口,筆者還定義了HasMethod函數(shù)來判斷是否存在指定名稱的腳本方法,定義了ExecuteSub函數(shù)來安全的不拋出異常的調(diào)用腳本方法。
Window全局對象
在很多腳本中存在一個名為“window”的全局對象,該對象大多用于和用戶界面互換,并映射到應(yīng)用系統(tǒng)主窗體。在這里筆者仿造HTML的javascript腳本的window全局對象來構(gòu)造出自己的window全局對象。
參考javascript中的window全局對象,對筆者有參考意義的類型成員主要分為映射到屏幕大小或者主窗體的位置大小的屬性,還有延時調(diào)用和定時調(diào)用的方法,還有顯示消息框或輸入框的方法。
筆者建立一個XVBAWindowObject類型作為Window全局對象的類型。
成員屬性
筆者首先定義一個UserInteractive屬性,該屬性指定應(yīng)用系統(tǒng)是否能和用戶桌面交互。定義該屬性的代碼如下
|
protected bool bolUserInteractive = true; /// <summary> /// 是否允許和用戶交互,也就是是否顯示用戶界面 /// </summary> /// <remarks>當應(yīng)用程序為ASP.NET或者Windows Service程序時不能有圖形化用戶界面,因此需要設(shè)置該屬性為false.</remarks> public bool UserInteractive { get { return bolUserInteractive; } set { bolUserInteractive = value; } } |
一些應(yīng)用系統(tǒng),包括ASP.NET和Windows Service,它是不能和用戶交互的,不能有圖形化用戶界面,不能調(diào)用MessageBox函數(shù),不能使用.NET類庫中System.Widnows.Forms名稱空間下的大部分功能,若強行調(diào)用則會出現(xiàn)程序錯誤。這個腳本引擎設(shè)計目標是可以運行在任何程序類型中的,包括WinForm,命令行模式,ASP.NET和Windows Service。因此在這里筆者定義了UserInteractive屬性用于關(guān)閉window全局對象的某些和用戶互換相關(guān)的功能,比如顯示消息框,延時調(diào)用和定時調(diào)用等等,主動關(guān)閉這些功能對應(yīng)用系統(tǒng)的影響是不大的。
筆者還定義了其他的一些屬性,其定義的代碼如下
|
protected string strSystemName = "應(yīng)用程序"; /// <summary> /// 系統(tǒng)名稱 /// </summary> public string SystemName { get { return strSystemName; } set { strSystemName = value; } }
protected XVBAEngine myEngine = null; /// <summary> /// 腳本引擎對象 /// </summary> public XVBAEngine Engine { get { return myEngine; } }
protected System.Windows.Forms.IWin32Window myParentWindow = null; /// <summary> /// 父窗體對象 /// </summary> public System.Windows.Forms.IWin32Window ParentWindow { get { return myParentWindow; } set { myParentWindow = value; } }
/// <summary> /// 屏幕寬度 /// </summary> public int ScreenWidth { get { if (bolUserInteractive) { return System.Windows.Forms.Screen.PrimaryScreen.Bounds.Width; } else { return 0; } } }
/// <summary> /// 屏幕高度 /// </summary> public int ScreenHeight { get { if (bolUserInteractive) { return System.Windows.Forms.Screen.PrimaryScreen.Bounds.Height; } else { return 0; } } } |
這里的ParentWindow屬性表示應(yīng)用系統(tǒng)的主窗體。
延時調(diào)用和定時調(diào)用
在Window全局對象中,筆者使用System.Windows.Forms.Timer對象實現(xiàn)了延時調(diào)用和定時調(diào)用,由于定時器對象屬于用戶互換相關(guān)的功能,因此延時調(diào)用和定時調(diào)用受到UserInteractive屬性的影響。筆者使用以下代碼來實現(xiàn)延時調(diào)用的功能
|
private string strTimeoutMethod = null; private System.Windows.Forms.Timer myTimer; /// <summary> /// 設(shè)置延時調(diào)用 /// </summary> /// <param name="MinSecend">延時的毫秒數(shù)</param> /// <param name="MethodName">調(diào)用的方法名稱</param> public void SetTimeout(int MinSecend, string MethodName) { // 若不支持和用戶互換則本功能無效 if ( bolUserInteractive == false) return; if (myEngine == null) return; if (myIntervalTimer != null) { // 取消當前的演示處理 myIntervalTimer.Stop(); } strTimerIntervalMethod = null; if (myTimer == null) { // 若定時器不存在則創(chuàng)建新的定時器對象 myTimer = new System.Windows.Forms.Timer(); myTimer.Tick += new EventHandler(myTimer_Tick); } // 設(shè)置定時器 myTimer.Interval = MinSecend; // 設(shè)置腳本方法名稱 strTimeoutMethod = MethodName; // 啟動定時器 myTimer.Start(); } /// <summary> /// 清除延時調(diào)用 /// </summary> public void ClearTimeout() { if (myTimer != null) { // 停止定時器 myTimer.Stop(); } // 清空延時調(diào)用的腳本方法名稱 strTimeoutMethod = null; }
/// <summary> /// 延時調(diào)用的定時器事件處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void myTimer_Tick(object sender, EventArgs e) { myTimer.Stop(); if (myEngine != null && strTimeoutMethod != null) { // 獲得腳本方法 string m = strTimeoutMethod.Trim(); strTimeoutMethod = null; if (myEngine.HasMethod(m)) { // 若腳本引擎中定義了該名稱的腳本方法則安全的執(zhí)行該腳本方法 myEngine.ExecuteSub(m); } } } |
SetTimeout函數(shù)用于實現(xiàn)延時調(diào)用,它的參數(shù)為延時調(diào)用的毫秒數(shù)和腳本方法名稱。在該函數(shù)中程序初始化一個名為myTimer定時器,設(shè)置它的Interval屬性為指定的毫秒數(shù),然后啟動該定時器。而在myTimer的定時事件處理中程序停止myTimer定時器,然后調(diào)用腳本引擎的ExecuteSub函數(shù)運行指定名稱的無參數(shù)腳本方法。使用SetTimeout只會運行一次腳本方法,在調(diào)用SetTimeout函數(shù)準備延時調(diào)用后可以調(diào)用ClearTimeout函數(shù)來立即取消延時調(diào)用。
筆者使用以下代碼來實現(xiàn)定時調(diào)用的功能
|
/// <summary> /// 定時調(diào)用使用的定時器控件 /// </summary> private System.Windows.Forms.Timer myIntervalTimer = null; /// <summary> /// 定時調(diào)用的腳本方法的名稱 /// </summary> private string strTimerIntervalMethod = null;
/// <summary> /// 設(shè)置定時運行 /// </summary> /// <param name="MinSecend">時間間隔毫秒數(shù)</param> /// <param name="MethodName">方法名稱</param> public void SetInterval(int MinSecend, string MethodName) { if (bolUserInteractive == false) { // 若不能和用戶互換則退出處理 return; } // 檢查參數(shù) if (MethodName == null || MethodName.Trim().Length == 0) { return; } if (this.myEngine == null) { return; }
if (myTimer != null) { //取消當前的延時調(diào)用功能 myTimer.Stop(); } strTimeoutMethod = null;
if (myEngine.HasMethod(MethodName.Trim()) == false) return; strTimerIntervalMethod = MethodName.Trim();
if (myIntervalTimer == null) { // 初始化定時調(diào)用的定時器控件 myIntervalTimer = new System.Windows.Forms.Timer(); myIntervalTimer.Tick += new EventHandler(myIntervalTimer_Tick); }
myIntervalTimer.Interval = MinSecend; } /// <summary> /// 清除定時運行 /// </summary> public void ClearInterval() { if (myIntervalTimer != null) { // 停止定時調(diào)用 myIntervalTimer.Stop(); } strTimerIntervalMethod = null; } /// <summary> /// 定時調(diào)用的定時器事件處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void myIntervalTimer_Tick(object sender, EventArgs e) { if (myIntervalTimer != null) { // 設(shè)置定時調(diào)用的腳本方法名稱 strTimerIntervalMethod = strTimerIntervalMethod.Trim(); } if (strTimerIntervalMethod == null || strTimerIntervalMethod.Length == 0 || myEngine == null || myEngine.HasMethod(strTimerIntervalMethod) == false) { if (myIntervalTimer != null) { // 若沒找到指定名稱的腳本方法則停止定時調(diào)用 myIntervalTimer.Stop(); } return; } // 安全的執(zhí)行指定名稱的腳本方法 myEngine.ExecuteSub(strTimerIntervalMethod); } |
SetInterval函數(shù)用于實現(xiàn)定時調(diào)用,它的參數(shù)為兩次調(diào)用之間的時間間隔,以及腳本方法名稱。在該函數(shù)中程序初始化一個名為myIntervalTimer的定時器,設(shè)置它的Interval屬性為指定的時間間隔,然后啟動該定時器。在myIntervalTimer的定時事件處理中程序調(diào)用腳本引擎的ExecuteSub函數(shù)運行指定名稱的無參數(shù)腳本方法。SetInterval會無休止的定時調(diào)用腳本方法,直到調(diào)用ClearInterval函數(shù)終止定時調(diào)用。
延時調(diào)用和定時調(diào)用是相互排斥的過程,啟動延時調(diào)用會停掉定時調(diào)用,而啟用定時調(diào)用會停掉延時調(diào)用。
映射應(yīng)用程序主窗體
Window全局對象定義了一些屬性用于映射應(yīng)用程序主窗體,筆者定義一個Title屬性應(yīng)用映射主窗體的文本,其代碼如下
|
/// <summary> /// 窗體標題 /// </summary> public string Title { get { System.Windows.Forms.Form frm = myParentWindow as System.Windows.Forms.Form; if (frm == null) { return ""; } else { return frm.Text; } } set { System.Windows.Forms.Form frm = myParentWindow as System.Windows.Forms.Form; if (frm != null) { frm.Text = value; } } } |
類似的,筆者定義了Left,Top、Width和Height屬性用于映射主窗體的左邊位置、頂邊位置,寬度和高度。
借助于這些Title、Left、Top、Width和Height屬性,用戶就可以在腳本中獲得和設(shè)置主窗體的一些屬性了。
這些屬性全都是和用戶互換相關(guān)的功能,因此都受UserInteractive屬性控制。若ASP.NET程序和Windows Service程序使用的腳本調(diào)用這些屬性將不會產(chǎn)生任何效果。對于WinForm程序,運行腳本前應(yīng)當將主窗體設(shè)置到window全局對象的ParentWindow屬性上。
顯示消息框
Window全局對象還定義了一些函數(shù)用于顯示一些消息對話框?qū)崿F(xiàn)用戶互換。主要代碼為
|
/// <summary> /// 將對象轉(zhuǎn)化為用于顯示的文本 /// </summary> /// <param name="objData">要轉(zhuǎn)換的對象</param> /// <returns>顯示的文本</returns> private string GetDisplayText(object objData) { if (objData == null) return "[null]"; else return Convert.ToString(objData); }
/// <summary> /// 顯示消息框 /// </summary> /// <param name="objText">提示信息的文本</param> public void Alert(object objText) { if (bolUserInteractive == false) return; System.Windows.Forms.MessageBox.Show( myParentWindow, GetDisplayText(objText), SystemName, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Information); } /// <summary> /// 顯示錯誤消息框 /// </summary> /// <param name="objText">提示信息的文本</param> public void AlertError(object objText) { if (bolUserInteractive == false) return; System.Windows.Forms.MessageBox.Show( myParentWindow, GetDisplayText(objText), SystemName, System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Exclamation); }
/// <summary> /// 顯示一個提示信息框,并返回用戶的選擇 /// </summary> /// <param name="objText">提示的文本</param> /// <returns>用戶是否確認的信息</returns> public bool ConFirm(object objText) { if (bolUserInteractive == false) return false; return (System.Windows.Forms.MessageBox.Show( myParentWindow, GetDisplayText(objText), SystemName, System.Windows.Forms.MessageBoxButtons.YesNo, System.Windows.Forms.MessageBoxIcon.Question) == System.Windows.Forms.DialogResult.Yes); }
/// <summary> /// 顯示一個信息輸入框共用戶輸入 /// </summary> /// <param name="objCaption">輸入信息的提示</param> /// <param name="objDefault">默認值</param> /// <returns>用戶輸入的信息</returns> public string Prompt(object objCaption, object objDefault) { if (bolUserInteractive == false) return null; return dlgInputBox.InputBox( myParentWindow, GetDisplayText(objCaption), SystemName, GetDisplayText(objDefault)); }
/// <summary> /// 顯示一個文本選擇對話框 /// </summary> /// <param name="objCaption">對話框標題</param> /// <param name="objFilter">文件過濾器</param> /// <returns>用戶選擇的文件名,若用戶取消選擇則返回空引用</returns> public string BrowseFile(object objCaption, object objFilter) { using (System.Windows.Forms.OpenFileDialog dlg = new System.Windows.Forms.OpenFileDialog()) { dlg.CheckFileExists = true; if (objCaption != null) { dlg.Title = this.GetDisplayText(objCaption); } if (objFilter != null) dlg.Filter = GetDisplayText(objFilter); if (dlg.ShowDialog(myParentWindow) == System.Windows.Forms.DialogResult.OK) return dlg.FileName; } return null; } /// <summary> /// 顯示一個文件夾選擇對話框 /// </summary> /// <param name="objCaption">對話框標題</param> /// <returns>用戶選擇了一個文件夾則返回該路徑,否則返回空引用</returns> public string BrowseFolder(object objCaption) { using (System.Windows.Forms.FolderBrowserDialog dlg = new System.Windows.Forms.FolderBrowserDialog()) { if (objCaption != null) { dlg.Description = this.GetDisplayText(objCaption); } dlg.RootFolder = System.Environment.SpecialFolder.MyComputer; if (dlg.ShowDialog(myParentWindow) == System.Windows.Forms.DialogResult.OK) return dlg.SelectedPath; else return null; } } |
調(diào)用這些方法,腳本能顯示簡單的消息框,顯示文件選擇對話框或文件夾選擇對話框以實現(xiàn)和用戶的互換。當前這些函數(shù)都受到UserInteractive屬性的控制。
這里定義了一個Alert方法用于顯示一個簡單的消息框,在VB中可以調(diào)用MsgBox方法來實現(xiàn)相同的功能,但MsgBox方法是VB運行庫的方法,不受UserInteractive屬性的控制,因此不建議使用,而使用Alert方法。
測試腳本引擎
腳本引擎設(shè)計和開發(fā)完畢后,袁某就可以編寫應(yīng)用程序來測試使用腳本引擎了。在這里筆者仿造Windows記事本開發(fā)了一個簡單的文本編輯器,其用戶界面如下。

在一個標準的C# WinForm項目中筆者新建一個名為frmMain的主窗體,上面放置工具條,下面放置一個名為txtEditor的多行文本框。工具條中放上新增,打開,保存,另存為等按鈕并添加事件處理以實現(xiàn)簡單文本編輯器的功能。
主窗體中還定義了諸如Function_New,Function_Open,Function_Save等等函數(shù)用于實現(xiàn)對文檔的新建、打開文件和保存文件等處理。而工具條上的按鈕就是調(diào)用這些功能函數(shù)。定義這些功能函數(shù)的代碼如下【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
|
/// <summary> /// 執(zhí)行新建文檔 /// </summary> public bool Function_New() { if (QuerySave()) { txtEditor.Text = ""; txtEditor.Modified = false; strFileName = null; return true; } return false; }
/// <summary> /// 執(zhí)行打開文件操作 /// </summary> public bool Function_Open() { if (QuerySave() == false) { return false ; } using (OpenFileDialog dlg = new OpenFileDialog()) { dlg.Filter = "文本文件(*.txt)|*.txt|所有文件|*.*"; dlg.CheckPathExists = true; if (dlg.ShowDialog(this) == DialogResult.OK) { System.IO.StreamReader reader = new System.IO.StreamReader( dlg.FileName, System.Text.Encoding.GetEncoding("gb2312")); txtEditor.Text = reader.ReadToEnd(); reader.Close(); strFileName = dlg.FileName; txtEditor.Modified = false; return true; } } return false; }
/// <summary> /// 執(zhí)行保存文檔操作 /// </summary> /// <returns>操作是否成功</returns> public bool Function_Save() { if (strFileName == null) { using (SaveFileDialog dlg = new SaveFileDialog()) { dlg.Filter = "文本文件(*.txt)|*.txt|所有文件|*.*"; dlg.CheckPathExists = true; dlg.OverwritePrompt = true; if (dlg.ShowDialog(this) == DialogResult.OK) { strFileName = dlg.FileName; } else { return false; } } } System.IO.StreamWriter writer = new System.IO.StreamWriter( strFileName, false, System.Text.Encoding.GetEncoding( "gb2312" )); writer.Write(this.txtEditor.Text); writer.Close(); this.txtEditor.Modified = false; return true; }
/// <summary> /// 執(zhí)行另存為操作 /// </summary> public bool Function_SaveAs() { using (SaveFileDialog dlg = new SaveFileDialog()) { dlg.Filter = "文本文件(*.txt)|*.txt|所有文件|*.*"; dlg.CheckPathExists = true; dlg.OverwritePrompt = true; if (dlg.ShowDialog(this) == DialogResult.OK) { strFileName = dlg.FileName; System.IO.StreamWriter writer = new System.IO.StreamWriter( strFileName, false, System.Text.Encoding.GetEncoding("gb2312")); writer.Write(this.txtEditor.Text); writer.Close(); this.txtEditor.Modified = false; return true; } } return false; }
/// <summary> /// 執(zhí)行全選操作 /// </summary> public void Function_SelectAll() { txtEditor.SelectAll(); }
/// <summary> /// 執(zhí)行剪切操作 /// </summary> public void Function_Cut() { txtEditor.Cut(); }
/// <summary> /// 執(zhí)行復(fù)制操作 /// </summary> public void Function_Copy() { txtEditor.Copy(); }
/// <summary> /// 執(zhí)行粘帖操作 /// </summary> public void Function_Paste() { txtEditor.Paste(); } /// <summary> /// 執(zhí)行刪除操作 /// </summary> public void Function_Delete() { txtEditor.SelectedText = ""; } |
文檔對象
筆者袁某在主窗體中定義了一個DocumentClass的套嵌類型,該類型就是腳本中使用的document全局對象的類型,其代碼為
|
/// <summary> /// 腳本中使用的文檔對象類型,本對象是對 frmMain 的一個封裝 /// </summary> public class DocumentClass { /// <summary> /// 初始化對象 /// </summary> /// <param name="frm"></param> internal DocumentClass(frmMain frm) { myForm = frm; }
internal frmMain myForm = null; /// <summary> /// 設(shè)置或返回文檔文本內(nèi)容 /// </summary> public string Text { get { return myForm.txtEditor.Text; } set { myForm.txtEditor.Text = value; } } /// <summary> /// 向文檔添加文本內(nèi)容 /// </summary> /// <param name="text">要添加的文本內(nèi)容</param> public void AppendText(string text) { myForm.txtEditor.AppendText(text); } /// <summary> /// 設(shè)置獲得文檔中選擇的部分 /// </summary> public string Selection { get { return myForm.txtEditor.SelectedText; } set { myForm.txtEditor.SelectedText = value; } } /// <summary> /// 文檔文件名 /// </summary> public string FileName { get { return myForm.FileName; } } /// <summary> /// 新建文檔 /// </summary> /// <returns>操作是否成功</returns> public bool New() { return myForm.Function_New(); } /// <summary> /// 保存文檔 /// </summary> /// <returns>操作是否成功</returns> public bool Save() { return myForm.Function_Save(); } /// <summary> /// 文檔另存為 /// </summary> /// <returns>操作是否成功</returns> public bool SaveAs() { return myForm.Function_SaveAs(); } /// <summary> /// 打開文件 /// </summary> /// <returns>操作是否成功</returns> public bool Open() { return myForm.Function_Open(); } /// <summary> /// 剪切 /// </summary> public void Cut() { myForm.Function_Cut(); } /// <summary> /// 復(fù)制 /// </summary> public void Copy() { myForm.Function_Copy(); } /// <summary> /// 粘帖 /// </summary> public void Paste() { myForm.Function_Paste(); } /// <summary> /// 刪除 /// </summary> public void Delete() { myForm.Function_Delete(); } /// <summary> /// 全選 /// </summary> public void SelectAll() { myForm.Function_SelectAll(); }
}//public class DocumentClass |
DocumentClass類型表示記事本當前處理的文檔對象。
創(chuàng)建全局對象容器
為了在腳本代碼中使用document,window這樣的全局對象,筆者得創(chuàng)建一個類型為GlobalObject的全局對象容器,定義該類型的代碼如下
|
namespace MyVBAScript.Global { /// <summary> /// 定義VB.NET腳本使用的全局對象容器類型 /// </summary> [Microsoft.VisualBasic.CompilerServices.StandardModuleAttribute()] public class GlobalObject { internal static XVBAWindowObject myWindow = null; /// <summary> /// 全局的 window 對象 /// </summary> public static XVBAWindowObject Window { get { return myWindow; } }
internal static frmMain.DocumentClass myDocument = null; /// <summary> /// 全局 document 對象 /// </summary> public static frmMain.DocumentClass Document { get { return myDocument; } } } } |
在這個腳本全局對象容器類型中,筆者添加了StandardModuleAttribute特性,并定義了Window和Document兩個靜態(tài)屬性。未來我們將腳本要操作的window對象和document對象設(shè)置到這兩個靜態(tài)屬性中。
和其他類型不一樣,筆者設(shè)置該類型的名稱空間為MyVBAScript.Global,這樣是為了將全局對象和其他類型區(qū)別開來,減少VB.NET編譯器的工作量。
初始化腳本引擎
在窗體的加載事件中我們初始化腳本引擎,其代碼為
|
private void frmMain_Load(object sender, EventArgs e) { //初始化窗體
// 創(chuàng)建腳本引擎 myVBAEngine = new XVBAEngine(); myVBAEngine.AddReferenceAssemblyByType(this.GetType()); myVBAEngine.VBCompilerImports.Add("MyVBAScript.Global"); // 設(shè)置腳本引擎全局對象 MyVBAScript.Global.GlobalObject.myWindow = new XVBAWindowObject(this, myVBAEngine, this.Text); MyVBAScript.Global.GlobalObject.myDocument = new DocumentClass(this); // 加載演示腳本文本 string strDemoVBS = System.IO.Path.Combine(System.Windows.Forms.Application.StartupPath, "demo.vbs"); if (System.IO.File.Exists(strDemoVBS)) { System.IO.StreamReader reader = new System.IO.StreamReader(strDemoVBS, System.Text.Encoding.GetEncoding("gb2312")); string script = reader.ReadToEnd(); reader.Close(); myVBAEngine.ScriptText = script; if (myVBAEngine.Compile() == false) { this.txtEditor.Text = "編譯默認腳本錯誤:"r"n" + myVBAEngine.CompilerOutput; } // 刷新腳本方法列表 this.RefreshScriptMethodList(); } }
|
這里程序首先創(chuàng)建了一個名為myVBAEngine的腳本引擎對象,然后向它的VBCompilerImports列表添加了全局對象容器類型所在的名稱空間MyVBAScript.Global。
然后程序創(chuàng)建一個文檔對象,并填充VB腳本引擎用的全局對象容器,設(shè)置它的Window和Document的屬性值。
程序試圖加載應(yīng)用程序所在目錄下的demo.vbs文件中的內(nèi)容作為默認加載的腳本代碼,若成功加載并編譯成功則調(diào)用RefreshScriptMethodList來更新用戶界面中的可用腳本方法列表,定義RefreshScriptMethodList函數(shù)的代碼如下
|
/// <summary> /// 刷新“運行腳本”按鈕的下拉菜單項目,顯示所有可以執(zhí)行的腳本方法名稱 /// </summary> private void RefreshScriptMethodList() { // 情況腳本方法名稱列表 this.btnRunScript.DropDownItems.Clear(); // 獲得腳本引擎中所有的腳本方法名稱 string[] names = myVBAEngine.ScriptMethodNames; if (names != null && names.Length > 0) { // 將腳本方法名稱添加到“運行腳本”的下拉菜單項目中 foreach (string name in names) { ToolStripMenuItem item = new ToolStripMenuItem(); item.Text = name; item.Click += new EventHandler(ScriptItem_Click); btnRunScript.DropDownItems.Add(item); } myStatusLabel.Text = "共加載 " + names.Length + " 個腳本方法"; } else { ToolStripMenuItem item = new ToolStripMenuItem(); item.Enabled = false; item.Text = "沒有加載任何腳本方法"; btnRunScript.DropDownItems.Add(item); myStatusLabel.Text = "沒有加載任何腳本方法"; } } |
這個函數(shù)的功能是,使用腳本引擎的ScriptMethodNames屬性獲得所有可用腳本方法的名稱,然后添加到工具條的“運行腳本”的下拉菜單中,于是可以到達如下的界面效果。

編輯腳本
工具條上有一個“編輯腳本”的按鈕,該按鈕是點擊事件處理過程為
|
/// <summary> /// 編輯腳本按鈕事件處理 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btnLoadScript_Click(object sender, EventArgs e) { //顯示腳本文本編輯對話框 using (dlgEditScript dlg = new dlgEditScript()) { int VersionBack = myVBAEngine.ScriptVersion; dlg.VBAEngine = this.myVBAEngine; dlg.ShowDialog(this); if( VersionBack != myVBAEngine.ScriptVersion ) { // 若腳本引擎內(nèi)容發(fā)生改變則刷新腳本方法下拉菜單項目 RefreshScriptMethodList(); } } } |
這段代碼中使用了腳本引擎的ScriptVersion屬性,腳本引擎中每進行一次編譯時都會更新因此ScriptVersion屬性,因此比較該屬性可以判斷腳本引擎中當前執(zhí)行的腳本代碼是否修改過。【袁永福原創(chuàng),轉(zhuǎn)載請注明出處】
dlgEditScript是一個腳本代碼編輯對話框,其用戶界面如下

運行腳本
該文本編輯器中,只能點擊工具條的“運行腳本”的下拉菜單的某個項目才能運行腳本方法。在屬性該下拉菜單的RefreshScriptMethodList中為每個菜單項目的點擊事件綁定了ScriptItem_Click方法,該ScriptItem_Click代碼為
|
/// <summary> /// 運行腳本的下拉菜單項目點擊事件處理 /// </summary> /// <param name="sender"></param> /// <param name="args"></param> private void ScriptItem_Click(object sender, System.EventArgs args) { ToolStripMenuItem item = (ToolStripMenuItem)sender; try { // 調(diào)用腳本執(zhí)行指定名稱的腳本方法 myVBAEngine.Execute(item.Text, null, true); } catch (Exception ext) { System.Console.WriteLine("執(zhí)行腳本 " + item.Text + " 錯誤:" + ext.ToString()); MessageBox.Show(this, "執(zhí)行腳本 " + item.Text + " 錯誤:" + ext.Message); } } |
ScriptItem_Click方法中,首先獲得用戶點擊的菜單項目,然后調(diào)用腳本引擎的Execute方法來執(zhí)行腳本,菜單項目顯示的文本就是腳本方法的名稱。
演示用腳本代碼說明
程序目錄下有一個demo.vbs的文本文件,該文件內(nèi)容就是演示實用的VBA.NET腳本。該腳本代碼為
|
sub 顯示當前使用的腳本代碼() document.text = window.engine.ScriptText end sub
sub 插入當前時間() document.Selection = DateTime.Now().ToString("yyyy年MM月dd日HH:mm:ss") end sub
sub 屏幕狀態(tài)() window.alert("屏幕大小:" & window.ScreenWidth & " * " & window.ScreenHeight _ & vbcrlf & "窗體位置:" & window.left & " " & window.top _ & vbcrlf & "窗體大小:" & window.Width & " * " & window.height ) end sub
sub ShowText() window.alert( document.text ) end sub
sub MoveWindow() window.left = 100 end sub
sub ShowFileName() window.alert( "當前文件名為:" & document.FileName ) end sub
sub AniMoveWindow() window.left = window.left - 10 if( window.left > 10 ) window.SetTimeout( 500 , "AniMoveWindow" ) end if end sub
dim Rate as double sub 模擬顯示正鉉曲線() Rate = Rate + 0.1 if( Rate > 50 ) exit sub end if dim strText as new string( " " , 50 + cint( math.sin( Rate ) * 30 )) document.AppendText( vbcrlf & strText & "######" ) window.SetTimeout( 100 , "模擬顯示正鉉曲線") window.Title = math.sin( Rate ) end sub
dim strTitle as string = "《C#發(fā)現(xiàn)之旅》系列課程的VB.NET腳本演示袁永福編寫版權(quán)所有2008年" dim TitleCount as integer sub 在標題欄顯示移動字幕() TitleCount = TitleCount + 1 if( TitleCount > strTitle.Length ) TitleCount = 0 exit sub end if window.Title = strTitle.SubString( strTitle.Length - TitleCount , TitleCount ) window.SetTimeOut( 100 , "在標題欄顯示移動字幕") end sub |
這里說明一下“模擬顯示正鉉曲線”這個腳本方法,首先定義一個Rate的全局變量作為計數(shù)器,每執(zhí)行一次該計數(shù)器加一,若超過50則退出方法,腳本中使用sin函數(shù)計算出空白字符串的長度生成一個空白字符串,然后使用文檔對象的AppendText方法向當前編輯的文檔添加空白字符和結(jié)尾字符,這里腳本調(diào)用window對象的SetTimeout方法來延期調(diào)用這個腳本方法自己。于是這個腳本方法每隔100毫秒執(zhí)行一次,并使用文本模擬顯示正鉉曲線,若顯示了50次則停止執(zhí)行。
這樣袁某就完成了一個簡單的文本編輯器程序,而且該程序能使用VBA.NET腳本引擎來擴展功能,能方便的進行二次開發(fā)。
部署腳本引擎
在實際開發(fā)中,開發(fā)人員可以將XVBAEngine等C#代碼拷貝到應(yīng)用程序中即可添加腳本功能,也可以將修改本C#工程的屬性使其單獨編譯成一個DLL然后供其他.NET程序使用。部署起來非常方便。
小結(jié)
在本次課程中,筆者使用動態(tài)編譯技術(shù)實現(xiàn)了VBA.NET腳本引擎,目前很多商業(yè)軟件,比如OFFICE,VS.NET等等都具有二次開發(fā)用的VBA腳本技術(shù),使用本課程介紹的知識我們也可以為應(yīng)用系統(tǒng)配置功能強大的腳本引擎,這能比較大的提高應(yīng)用系統(tǒng)的靈活性。腳本引擎技術(shù)是一個非常實用的軟件開發(fā)技術(shù),值得推廣。
posted on 2009-07-27 09:39 袁永福 電子病歷,醫(yī)療信息化 閱讀(6104) 評論(6) 收藏 舉報
在本章中筆者將使用動態(tài)編譯技術(shù)來實現(xiàn)一個VB.NET的腳本引擎,使得人們在開發(fā)中能實現(xiàn)類似MS Office那樣實現(xiàn)VBA宏的功能。本章配套的演示程序為http://www.rzrgm.cn/Files/xdesigner/MyVBAScript.zip.
浙公網(wǎng)安備 33010602011771號