翻譯這篇文章源於我的一個通用工資計算平台的想法,在工資的計算中,不可避免的需要使用到自定義公式,然而對於自定義公式的實現,我自己想了一些,也在網上搜索了很多,解決辦法大致有以下幾種:
1. 自己寫代碼去解析公式。這種方法的缺點是,解析的代碼很難實現,如果公式的功能比較完整,如增加條件判斷或自定義函數。不亞於實現了一個簡單的語言編譯囂或解釋囂。所以,只能實現一些諸如加減乘除之類的簡單公式。
2. 打包成SQL傳給數據庫去執行。這顯然不是一種好辦法。而且需要與特定的數據庫和表結構進行適應。
3. 我想到在foxpro中有宏替換功能&,那不如就借用它的這個功能,即利用foxpro寫一個dll,在這個dll中實現了將字符串轉換成指令執行的功能,然後在delphi中加載這個dll,將公式傳入dll中的函數執行。這應該是一個辦法,但我還沒有去實現它。
4. 內嵌腳本語言。
也只有第四種辦法比較理想的,於是我就找到了RemObjects Pascal Script,安裝,並翻譯了這篇使用說明。
再把應用范圍擴大一點,其實在編譯型程序中嵌入腳本語言可以解決很多應用程序自動化的問題,在了解並實際寫了幾個RemObjects Pascal Script的實從程序後。內心還是蠻興奮的。
PS01 - Using the RemObjects Pascal Script
使用RemObjects Pascal Script
This article provides an overview of the new RemObjects Pascal Script and explains how to create some simple scripts.
這篇文章提供了RemObjects Pascal Script的一個概覽,以及說明了如何去創建一些簡單的腳本。
Pascal Script comprises two different parts:
Compiler (uPSCompiler.pas)
Runtime (uPSRuntime.pas)
Pascal Script由兩個部分組成:
編譯囂(UPSCompiler.pas)
運行時(uPSRuntime.pas)
The two parts have no interdependencies on each other. You can use them directly, or you can use them in the TPSScript component, which can be found in the uPSComponent.pas unit, and wraps them both in one easy to use class.
這兩部分之間是沒有相互依賴的。你可以直接使用她們,或才你可以透過TPSSCript組件來使用她們,TPSSCript組件在uPSComponent.pas單元中,她對上述兩個部分進行一些包裝以便我們可以很容易的使用。
To use the component version of Pascal Script, you must first place it on your form or data module, set or assign the script property, call the Compile method, and call the Execute method. Compile errors, warnings or hints can be found in the CompilerMessages array property, while runtime errors can be found by reading the ExecErrorToString property.
要使用Pascal Script組件,你首先要將它從組件面板中拖置窗體或module中,然後設置它的script屬性,然後調用它的Compile方法進行編譯,再然後調用它的Execute方法來執行腳本。編譯的errors,hints,warnings可以通過其屬性CompilerMessages取得,這個屬性是一個數組。如果是運行時的錯誤,則可以通過屬性ExecErrorToString取得。
The following example will compile and execute an empty script ("begin end."):
下面的例子將編譯並執行一個空腳本("begin end."):
var
Messages: string;
compiled: boolean;
begin
ce.Script.Text := 'begin end.';
Compiled := Ce.Compile;
for i := 0 to ce.CompilerMessageCount -1 do
Messages := Messages +
ce.CompilerMessages[i].MessageToString +
#13#10;
if Compiled then
Messages := Messages + 'Succesfully compiled'#13#10;
ShowMessage('Compiled Script: '#13#10+Messages);
if Compiled then begin
if Ce.Execute then
ShowMessage('Succesfully Executed')
else
ShowMessage('Error while executing script: '+
Ce.ExecErrorToString);
end;
end;
By default, the component only adds a few standard functions to the scripting engine (the exact list can be found at the top of uPSComponents.pas).
缺省情況下,組件只加入一少部分標准的functions到腳本引擎中(具體可以在uPSComponents.pas單元頭中找到)
Besides the standard functions, there are a few libraries included with Pascal Script:
TPSDllPlugin Allow scripts to use dll functions, the syntax is like:
function FindWindow(C1, C2: PChar): Longint; external 'FindWindowA@user32.dll stdcall';
TPSImport_Classes Import library for TObject and the Classes unit.
TPSImport_DateUtils Import library for date/time related functions.
TPSImport_ComObj Access COM Objects from your scripts.
TPSImport_DB Import library for db.pas.
TPSImport_Forms Import library for the Forms & Menus units.
TPSImport_Controls Import library to Controls.pas and Graphics.pas.
TPSImport_StdCtrls Import library for ExtCtrls and Buttons.
除了這些標准的functions之外,Pascal Script還包含了一少部分程式庫:
TPSDllPlugin 允許腳本可以使用外部DLL函數,其調用語法類似下例:
function FindWindow(C1, C2: PChar): Longint; external 'FindWindowA@user32.dll stdcall';
TPSImport_Classes 導入對應於TObject和Classes單元的libraries;
TPSImport_DateUtils 導入日期時間相關的libraries;
TPSImport_ComObj 在腳本中訪問COM對象;
TPSImport_DB 導入對應於db.pas單元的libraries;
TPSImport_Forms 導入對應於Forms和Menus單元的libraries;
TPSImport_Controls 導入對應於Controls.pas和Graphics.pas單元的libraries;
TPSImport_StdCtrls 導入對應於ExtCtrls和Buttons的libraries.
To use these libraries, add them to your form or data module, select the [...] button next to the plugins property of the TPSCompiler component, add a new item and set the Plugin property to the plugin component.
要使用這些libraries,將它們從組件面板中拖至窗體數據data module中,然後設置TPSCompiler的plugins屬性,在其中增加條目,並將條目指向這些plugin組件。
Besides the standard libraries, you can easily add new functions to the scripting engine. In order to do that, create a new method you would like to expose to the scripting engine, for example:
除了這些標准的libraries之外,你還可以很方便地向腳本引擎中添加新的函數。要做到這一點,創建一個你要加入到腳本中的method,如下例:
procedure TForm1.ShowNewMessage(const Message: string);
begin
ShowMessage('ShowNewMessage invoked:'#13#10+Message);
end;
Then, assign an event handler to the OnCompile event and use the AddMethod method of TPSCompiler to add the actual method:
然後,在TPSCompiler 的OnCompile事件中將該方法添加入腳本中:
procedure TForm1.CECompile(Sender: TPSScript);
begin
Sender.AddMethod(Self, @TForm1.ShowNewMessage,
'procedure ShowNewMessage
(const Message: string);');
end;
A sample script that uses this function could look like this:
這樣, 你就可以在腳本中使用這個函數,如下:
begin
ShowNewMessage('Show This !');
end.
Advanced Features
高級功能
Pascal Script includes a preprocessor that allows you to use defines ({$IFDEF}, {$ELSE}, {$ENDIF}) and include other files in your script ({$I filename.inc}). To enable these features, you must set the UsePreprocessor property to true and the MainFileName property to match the name of the script in the Script property. The Defines property specifies which defines are set by default, and the OnNeedFile event is called when an include file is needed.
Pascal Script包含了一個預處理程序,以便你可以在腳本中使用編譯預定義(defines)({$IFDEF}, {$ELSE}, {$ENDIF}) 以及在腳本中包含其它腳本 ({$I filename.inc})。要達到這個功能,你需要設置UsePreprocessor屬性為true,and the MainFileName property to match the name of the script in the Script property. 。Defines屬性指定要缺省時定義哪些defines;OnNeedFile事件代碼在需要包含的文件時被執行。
function TForm1.ceNeedFile(Sender: TObject;
const OrginFileName: String;
var FileName, Output: String): Boolean;
var
path: string;
f: TFileStream;
begin
Path := ExtractFilePath(ParamStr(0)) + FileName;
try
F := TFileStream.Create(Path, fmOpenRead or fmShareDenyWrite);
except
Result := false;
exit;
end;
try
SetLength(Output, f.Size);
f.Read(Output[1], Length(Output));
finally
f.Free;
end;
Result := True;
end;
When these properties are set, the CompilerMessages array property will include the file name these messages occur in.
當這些屬性被設置以後,CompilerMessages屬性可就將可能包含這些文件名。
Additionally, you can call scripted functions from Delphi. The following sample will be used as a script:
另外,你可以在Delphi中調用腳本裡的函數。如下函數被定義在腳本中,後面將會在delphi中被調用:
function TestFunction(Param1: Double; Data: String): Longint;
begin
ShowNewMessage('Param1: '+FloatToString(param1)
+#13#10+'Data: '+Data);
Result := 1234567;
end;
begin
end.
Before this scripted function can be used, it has to be checked to match its parameter and result types, which can be done in the OnVerifyProc event.
在使用調用這個函數之前,必須對其進行一個校驗,校驗其參數和返回值類型,在OnVerifyProc執行這個校驗。
procedure TForm1.CEVerifyProc(Sender: TPSScript;
Proc: TPSInternalProcedure;
const Decl: String;
var Error: Boolean);
begin
if Proc.Name = 'TESTFUNCTION' then begin
if not ExportCheck(Sender.Comp, Proc,
[btS32, btDouble, btString], [pmIn, pmIn]) then begin
Sender.Comp.MakeError('', ecCustomError, 'Function header for
TestFunction does not match.');
Error := True;
end
else begin
Error := False;
end;
end
else
Error := False;
end;
The ExportCheck function checks if the parameters match. In this case, btu8 is a boolean (the result type), btdouble is the first parameter, and btString the second parameter. [pmIn, pmIn] specifies that both parameters are IN parameters. To call this scripted function, you have to create an event declaration for this function and call that.
ExportCheck函數檢查參數的匹配情況。在這個例子中,btu8是一個布爾型(返回值類型),btdouble是第一個參數,btString是第二個參數。[pmIn, pmIn]表示兩個參數都是輸入參數。要調用這個腳本函數,你需要為它創建一個函數類型聲明。
type
TTestFunction = function (Param1: Double;
Data: String): Longint of object;
//...
var
Meth: TTestFunction;
Meth := TTestFunction(ce.GetProcMethod('TESTFUNCTION'));
if @Meth = nil then
raise Exception.Create('Unable to call TestFunction');
ShowMessage('Result: '+IntToStr(Meth(pi, DateTimeToStr(Now))));
It's also possible to add variables to the script engine, which can be used from within the script. To do this, you have to use the AddRegisteredVariable function. You can set this in the OnExecute event :
還可以向腳本引擎中添加變量,然後就可以在腳本中使用這些變量 。要做到這一點,你需要使用AddRegisteredVariable函數。可以在OnExecute設置它:
procedure TForm1.ceExecute(Sender: TPSScript);
begin
CE.SetVarToInstance('SELF', Self);
// ^^^ For class variables
VSetInt(CE.GetVariable('MYVAR'), 1234567);
end;
To read this variable back, after the script has finished executing, you can use the OnAfterExecute event:
若要再去讀取這個變量的值,在腳本執行完成後,在OnAfterExecute事件中訪問:
VGetInt(CE.GetVariable('MYVAR')).
Registering external variables to the script engine is also possible. It's a two step process, first, in the OnCompile event, add the types to the script using the AddRegisteredPTRVariable function.
注冊一個外部變量到腳本引擎中也是可以的。這需要兩個步驟,首先在OnCompile事件中使用AddRegisteredPTRVariable函數將變量類型添加到腳本中。
procedure TMyForm.PSScriptCompile(Sender: TPSScript);
begin
Sender.AddRegisteredPTRVariable('MyClass', 'TButton');
Sender.AddRegisteredPTRVariable('MyVar', 'Longint');
end;
This will register the external MyClass and MyVar variables. Second, attach a pointer to these variables in the OnExecute event:
這樣就注冊了MyClass 和 MyVar這兩個變量。第二步,在OnExecute中通過將變量值的地址指針傳給變量來實現給變量賦值:
procedure TMyForm.PSScriptExecute(Sender: TPSScript);
begin
PSScript.SetPointerToData('MyVar', @MyVar, PSScript.FindBaseType(bts32));
PSScript.SetPointerToData('Memo1', @Memo1, PSScript.FindNamedType('TMemo'));
end;
There are two types of variables in Pascal Script, base types, which are simple types (see the table below), and class types. Base types are registered in the uPSUtils.pas unit and can be located using the FindBaseType function. Class types have to be located by name, using the FindNamedType. Changes to these variables have a direct effect on the actual variable.
在Pascal Script中有兩種類型的變量,一種是基本類型,包含一些簡單的類型,下面會列出;另一種是類類型。基本類型是在uPSUtils.pas被注冊進去的,可以通過FindBaseType函數找到。類類型需要使用FindNamedType函數通過名稱找到。改變這些變量將直接地影響到實際的變量。
Base types:
btU8 Byte
btS8 Shortint
btU16 Word
btS16 Smallint
btU32 Longword
btS32 Longint
btS64 Int64
btSingle Single
btDouble Double
btExtended Extended
btVariant Variant
btString String
btWideString WideString
btChar Char
btWideChar WideChar
The component version of Pascal Script also supports execution of scripted functions. This works by using the ExecuteFunction method.
Pascal Script組件同樣也支持腳本函數。這通過ExecuteFunction來調用。
ShowMessage(CompExec.ExecuteFunction([1234.5678, 4321,
'test'],
'TestFunction'));
This will execute the function called 'TestFunction' with 3 parameters, a float, an integer and a string. The result will be passed back to ShowMessage.
這個例子將執行一個名為TestFunction的函數,這個函數包含3個參數,一個float,一個integer和一個string。函數的返回值傳回給ShowMessage。
Notes:
For some functions and constants, it might be necessary to add: uPSCompiler.pas, uPSRuntime.pas and/or uPSUtils.pas to your uses list.
The script engine never calls Application.ProcessMessages by itself, so your application might hang, while the script is running. To avoid this, you can add Application.ProcessMessages to the TPSScript.OnLine event.
It's possible to import your own classes in the script engine. Pascal Script includes a tool to create import libraries in the /Unit-Importing/ directory.
It's possible to import your own classes in the script engine. Pascal Script includes a tool to create import libraries in the Bin directory.
For examples on how to use the compiler and runtime separately, see the Import and Kylix samples.
The Debug requires SynEdit http://synedit.sourceforge.net/.
注意:
一些必要的函數和常量應該被加入到uses 列表中:uPSCompiler.pas, uPSRuntime.pas, uPSUtils.pas;
腳本引擎不會自行調用Application.ProcessMessages,因此在腳本執行時你的應用程序可能會終止。要避免這一點,你可以將Application.ProcessMessages加入到TPSScript的OnLine事件中;
可能需要在腳本中引入你自己的類,Pascal Script包含一個工具以便創建引入的庫,這個工具在Unit-Importing目錄中;
可能需要在腳本中引入你自己的類,Pascal Script包含一個工具以便創建引入的庫,這個工具在Bin目錄中;
安裝目錄中可以找到單獨使用Comiler和Runtim的實例;
腳本調試需要SynEdit
http://synedit.sourceforge.net/.
--------------------
fey
2007/2/27夜