Extending the scripting system
Workflow Studio provides a scripting system which can be used in several places of Workflow Studio framework. Expressions use the scripting system, and the script block as well, which runs scripts. You can increase integration between Workflow Studio and your Delphi application by extendeding the scripting system.
You can register your own classes, methods, properties, variables, functions and procedures so they can be accessible from script. The following topics describe some common tasks to extend the scripting system.
The examples shown in the following topics use a Scripter object and
describe how to register new components, methods, classes and properties
to a scripter component by using methods like DefineClass, DefineProp,
etc.. However, Workflow Studio creates a new scripter component instance
for each script block that is used in a workflow instance. Due to that,
you must use OnGlobalScripterCreate event to make sure you initialize
all scripter components in the system. The following steps show how to
do that. The OnGlobalScripterCreate is a global variable of type
TNotifyEvent declared in Wf.Script.pas
unit. First of all, you need to
set that global variable to a method in your application:
// PrepareScripter is a method in any of your existing and instantiated classes
OnGlobalScripterCreate := PrepareScripter;
Then you declare your global initialization method PrepareScripter. The Sender parameter is the scripter component, so you just need to typecast it to a generic TwfCustomScripter class. Here is an example:
procedure TMyDataModule.PrepareScripter(Sender: Tobject);
begin
with TwfCustomScripter(Sender) do
begin
// Examples:
AddComponent(Form1);
DefineMethod({...});
end;
end;
Accessing Delphi objects
The following topics show how to register Delphi objects in the scripting system.
Registering Delphi components
One powerful feature of scripter is to access Delphi objects. This way you can make reference to objects in script, change its properties, call its methods, and so on. However, every object must be registered in scripter so you can access it. For example, suppose you want to change caption of form (named Form1). If you try to execute this script:
SCRIPT:
Form1.Caption := 'New caption';
you will get "Unknown identifier or variable not declared: Form1". To make scripter work, use AddComponent method:
CODE:
Scripter.AddComponent(Form1);
Now scripter will work and form's caption will be changed.
Access to published properties
After a component is added, you have access to its published properties. That's why the caption property of the form could be changed. Otherwise you would need to register property as well. Actually, published properties are registered, but scripter does it for you.
Class registering structure
Scripter can call methods and properties of objects. But this methods and properties must be registered in scripter. The key property for this is TatCustomScripter.Classes property. This property holds a collection of registered classes (TatClass object), which in turn holds its collection of registered properties and methods (TatClass.Methods and TatClass.Properties). Each registered method and property holds a name and the wrapper method (the Delphi written code that will handle method and property).
When you registered Form1 component in the previous example, scripter automatically registered TForm class in Classes property, and registered all published properties inside it. To access methods and public properties, you must registered them, as showed in the following topics.
Calling methods
To call an object method, you need to register it. For instance, if you want to call ShowModal method of a newly created form named "Form2". So we must add the form it to scripter using AddComponent method, and then register ShowModal method:
CODE:
procedure Tform1.ShowModalProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(TCustomForm(CurrentObject).ShowModal);
end;
procedure TForm1.PrepareScript;
begin
Scripter.AddComponent(Form2);
with Scripter.DefineClass(TCustomForm) do
begin
DefineMethod('ShowModal', 0, tkInteger, nil, ShowModalProc);
end;
end;
SCRIPT:
ShowResult := Form2.ShowModal;
This example has a lot of new concepts. First, component is added with AddComponent method. Then, DefineClass method was called to register TCustomForm class. DefineClass method automatically check if TCustomForm class is already registered or not, so you don't need to do test it.
After that, ShowModal is registered, using DefineMethod method. Declaration of DefineMethod is:
function DefineMethod(AName: string; AArgCount: integer; AResultDataType: TatTypeKind;
AResultClass: TClass; AProc: TMachineProc; AIsClassMethod: boolean = false): TatMethod;
AName receives 'ShowModal' - it's the name of method to be used in script.
AArgCount receives 0 - number of input arguments for the method (none, in the case of ShowModal).
AResultDataType receives tkInteger - it's the data type of method result. ShowModal returns an integer. If method is not a function but a procedure, AResultDataType should receive tkNone.
AResultClass receives nil - if method returns an object (not this case), then AResultClass must contain the object class. For example, TField.
AProc receives ShowModalProc - the method written by the user that works as ShowModal wrapper.
And, finally, there is ShowModalProc method. It is a method that works as the wrapper: it implements a call to ShowModal. In this case, it uses some useful methods and properties of TatVirtualMachine class:
property CurrentObject - contains the instance of object where the method belongs to. So, it contains the instance of a specified TCustomForm.
method ReturnOutputArg - it returns a function result to scripter. In this case, returns the value returned by TCustomForm.ShowModal method.
More method calling examples
In addition to previous example, this one illustrates how to register and call methods that receive parameters and return classes. In this example, FieldByName:
SCRIPT:
AField := Table1.FieldByName('CustNo');
ShowMessage(AField.DisplayLabel);
CODE:
procedure TForm1.FieldByNameProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(integer(TDataset(CurrentObject).FieldByName(GetInputArgAsString(0))));
end;
procedure TForm1.PrepareScript;
begin
Scripter.AddComponent(Table1);
with Scripter.DefineClass(TDataset) do
begin
DefineMethod('FieldByName', 1, tkClass, TField, FieldByNameProc);
end;
end;
Very similar to Calling methods example. Some comments:
FieldByName method is registered in TDataset class. This allows use of FieldByName method by any TDataset descendant inside script. If FieldByName was registered in a TTable class, script would not recognize the method if component was a TQuery.
DefineMethod call defined that FieldByName receives one parameters, its result type is tkClass, and class result is TField.
Inside FieldByNameProc, GetInputArgAsString method is called in order to get input parameters. The 0 index indicates that we want the first parameter. For methods that receive 2 or more parameters, use GetInputArg(1), GetInputArg(2), and so on.
To use ReturnOutputArg in this case, we need to cast resulting TField as integer. This must be done to return any object. This is because ReturnOutputArg receives a Variant type, and objects must then be cast to integer.
Acessing non-published properties
Just like methods, properties that are not published must be registered. The mechanism is very similar to method registering, with the difference we must indicate one wrapper to get property value and another one to set property value. In the following example, the "AsFloat" property of TField class is registered:
SCRIPT:
AField := Table1.FieldByName('Company');
ShowMessage(AField.Value);
CODE:
procedure TForm1.GetFieldValueProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(TField(CurrentObject).Value);
end;
procedure TForm1.SetFieldValueProc(AMachine: TatVirtualMachine);
begin
with AMachine do
TField(CurrentObject).Value := GetInputArg(0);
end;
procedure TForm1.PrepareScript;
begin
with Scripter.DefineClass(TField) do
begin
DefineProp('Value', tkVariant, GetFieldValueProc, SetFieldValueProc);
end;
end;
DefineProp is called passing a tkVariant indicating that Value property is Variant type, and then passing two methods GetFieldValueProc and SetFieldValueProc, which, in turn, read and write value property of a TField object. Note that in SetFieldValueProc method was used GetInputArg (instead of GetInputArgAsString). This is because GetInputArg returns a variant.
Registering indexed properties
A property can be indexed, specially when it is a TCollection descendant. This applies to dataset fields, grid columns, string items, and so on. So, the code below illustrates how to register indexed properties. In this example, Strings property of TStrings object is added in other to change memo content:
SCRIPT:
ShowMessage(Memo1.Lines.Strings[3]);
Memo1.Lines.Strings[3] := Memo1.Lines.Strings[3] + ' with more text added';
CODE:
procedure TForm1.GetStringsProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(TStrings(CurrentObject).Strings[GetArrayIndex(0)]);
end;
procedure TForm1.SetStringsProc(AMachine: TatVirtualMachine);
begin
with AMachine do
TStrings(CurrentObject).Strings[GetArrayIndex(0)] := GetInputArgAsString(0);
end;
procedure TForm1.PrepareScript;
begin
Scripter.AddComponent(Memo1);
with Scripter.DefineClass(TStrings) do
begin
DefineProp('Strings', tkString, GetStringsProc, SetStringsProc, nil, false, 1);
end;
end;
Some comments:
DefineProp receives three more parameters than DefineMethod: nil (class type of property, it's nil because property is string type), false (indicating the property is not a class property) and 1 (indicating that property is indexed by 1 parameter. This is the key param. For example, to register Cells property of the grid, this parameter show be 2, since Cells depends on Row and Col).
In GetStringsProc and SetStringsProc, GetArrayIndex method is used to get the index value passed by script. The 0 param indicates that it is the first index (in the case of Strings property, the only one).
Retrieving name of called method or property
You can register the same wrapper for more than one method or property. In this case, you might need to know which property or method was called. In this case, you can use CurrentPropertyName or CurrentMethodName. The following example illustrates this usage.
procedure TForm1.GenericMessageProc(AMachine: TatVirtualMachine);
begin
with AMachine do
if CurrentMethodName = 'MessageHello' then
ShowMessage('Hello')
else if CurrentMethodName = 'MessageWorld' then
ShowMessage('World');
end;
procedure TForm1.PrepareScript;
begin
with Scripter do
begin
DefineMethod('MessageHello', 1, tkNone, nil, GenericMessageProc);
DefineMethod('MessageWorld', 1, tkNone, nil, GenericMessageProc);
end;
end;
Registering methods with default parameters
You can also register methods which have default parameters in scripter. To do that, you must pass the number of default parameters in the DefineMethod property. Then, when implementing the method wrapper, you need to check the number of parameters passed from the script, and then call the Delphi method with the correct number of parameters. For example, let's say you have the following procedure declared in Delphi:
function SumNumbers(A, B: double; C: double = 0; D: double = 0; E: double = 0): double;
To register that procedure in scripter, you use DefineMethod below. Note that the number of parameters is 5 (five), and the number of default parameters is 3 (three):
Scripter.DefineMethod('SumNumbers', 5 {number of total parameters},
tkFloat, nil, SumNumbersProc, false, 3 {number of default parameters});
Then, in the implementation of SumNumbersProc, just check the number of input parameters and call the function properly:
procedure TForm1.SumNumbersProc(AMachine: TatVirtualMachine);
begin
with AMachine do
begin
case InputArgCount of
2: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1)));
3: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1),
GetInputArgAsFloat(2)));
4: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1),
GetInputArgAsFloat(2), GetInputArgAsFloat(3)));
5: ReturnOutputArg(SumNumbers(GetInputArgAsFloat(0), GetInputArgAsFloat(1),
GetInputArgAsFloat(2), GetInputArgAsFloat(3), GetInputArgAsFloat(4)));
end;
end;
end;
Acessing Delphi functions, variables and constants
The following topics describe how to register regular procedures, functions and global variables in scripting system.
Overview
In addition to access Delphi objects, scripter allows integration with regular procedures and functions, global variables and global constants. The mechanism is very similar to accessing Delphi objects. In fact, scripter internally consider regular procedures and functions as methods, and global variables and constants are props.
Registering global constants
Registering a constant is a simple task in scripter: use AddConstant method to add the constant and the name it will be known in scripter:
CODE:
Scripter.AddConstant('MaxInt', MaxInt);
Scripter.AddConstant('Pi', pi);
Scripter.AddConstant('MyBirthday', EncodeDate(1992, 5, 30));
SCRIPT:
ShowMessage('Max integer is ' + IntToStr(MaxInt));
ShowMessage('Value of pi is ' + FloatToStr(pi));
ShowMessage('I was born on ' + DateToStr(MyBirthday));
Access the constants in script just like you do in Delphi code.
Acessing global variables
To register a variable in scripter, you must use AddVariable method. Variables can be added in a similar way to constants: passing the variable name and the variable itself. In addition, you can also add variable in the way you do with properties: use a wrapper method to get variable value and set variable value:
CODE:
var
MyVar: Variant;
ZipCode: string[15];
procedure TForm1.GetZipCodeProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(ZipCode);
end;
procedure TForm1.SetZipCodeProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ZipCode := GetInputArgAsString(0);
end;
procedure TForm1.PrepareScript;
begin
Scripter.AddVariable('ShortDateFormat', ShortDateFormat);
Scripter.AddVariable('MyVar', MyVar);
Scripter.DefineProp('ZipCode', tkString, GetZipCodeProc, SetZipCodeProc);
Scripter.AddObject('Application', Application);
end;
procedure TForm1.Run1Click(Sender: TObject);
begin
PrepareScript;
MyVar := 'Old value';
ZipCode := '987654321';
Application.Tag := 10;
Scripter.SourceCode := Memo1.Lines;
Scripter.Execute;
ShowMessage('Value of MyVar variable in Delphi is ' + VarToStr(MyVar));
ShowMessage('Value of ZipCode variable in Delphi is ' + VarToStr(ZipCode));
end;
SCRIPT:
ShowMessage('Today is ' + DateToStr(Date) + ' in old short date format');
ShortDateFormat := 'dd-mmmm-yyyy';
ShowMessage('Now today is ' + DateToStr(Date) + ' in new short date format');
ShowMessage('My var value was "' + MyVar + '"');
MyVar := 'My new var value';
ShowMessage('Old Zip code is ' + ZipCode);
ZipCode := '109020';
ShowMessage('Application tag is ' + IntToStr(Application.Tag));
Calling regular functions and procedures
In scripter, regular functions and procedures are added like methods. The difference is that you don't add the procedure in any class, but in scripter itself, using DefineMethod method. The example below illustrates how to add QuotedStr and StringOfChar methods:
SCRIPT:
ShowMessage(QuotedStr(StringOfChar('+', 3)));
CODE:
{ TSomeLibrary }
procedure TSomeLibrary.Init;
begin
Scripter.DefineMethod('QuotedStr', 1, tkString, nil, QuotedStrProc);
Scripter.DefineMethod('StringOfChar', 2, tkString, nil, StringOfCharProc);
end;
procedure TSomeLibrary.QuotedStrProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(QuotedStr(GetInputArgAsString(0)));
end;
procedure TSomeLibrary.StringOfCharProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(StringOfChar(GetInputArgAsString(0)[1], GetInputArgAsInteger(1)));
end;
procedure TForm1.Run1Click(Sender: TObject);
begin
Scripter.AddLibrary(TSomeLibrary);
Scripter.SourceCode := Memo1.Lines;
Scripter.Execute;
end;
Since there is no big difference from defining methods, the example above introduces an extra concept: libraries. Note that the way methods are defined didn't change (a call to DefineMethod) and neither the way wrapper are implemented (QuotedStrProc and StringOfCharProc). The only difference is the way they are located: instead of TForm1 class, they belong to a different class named TSomeLibrary. The following topic covers the use of libraries.
Using libraries
Libraries are just a concept of extending scripter by adding more components, methods, properties, classes to be available from script. You can do that by manually registering a single component, class or method. A library is just a way of doing that in a more organized way.
Delphi-based libraries
In script, you can use libraries for registered methods and properties. Look at the two codes below, the first one uses libraries and the second use the mechanism used in this doc until now:
CODE 1:
type
TExampleLibrary = class(TatScripterLibrary)
protected
procedure CurrToStrProc(AMachine: TatVirtualMachine);
procedure Init; override;
class function LibraryName: string; override;
end;
class function TExampleLibrary.LibraryName: string;
begin
result := 'Example';
end;
procedure TExampleLibrary.Init;
begin
Scripter.DefineMethod('CurrToStr', 1, tkInteger, nil, CurrToStrProc);
end;
procedure TExampleLibrary.CurrToStrProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(CurrToStr(GetInputArgAsFloat(0)));
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
Scripter.AddLibrary(TExampleLibrary);
Scripter.SourceCode := Memo1.Lines;
Scripter.Execute;
end;
CODE 2:
procedure TForm1.PrepareScript;
begin
Scripter.DefineMethod('CurrToStr', 1, tkInteger, nil, CurrToStrProc);
end;
procedure TForm1.CurrToStrProc(AMachine: TatVirtualMachine);
begin
with AMachine do
ReturnOutputArg(CurrToStr(GetInputArgAsFloat(0)));
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
PrepareScript;
Scripter.SourceCode := Memo1.Lines;
Scripter.Execute;
end;
Both codes do the same: add CurrToStr procedure to script. Note that scripter initialization method (Init in Code 1 and PrepareScript in Code 2) is the same in both codes. And so is CurrToStrProc method - no difference. The two differences between the code are:
The class where the methods belong to. In Code 1, methods belong to a special class named TExampleLibrary, which descends from TatScripterLibrary. In Code 2, the belong to the current form (TForm1).
In Code 1, scripter preparation is done adding TExampleLibrary class to scripter, using AddLibrary method. In Code 2, PrepareScript method is called directly.
So when to use one way or another? There is no rule - use the way you feel more confortable. Here are pros and cons of each:
Declaring wrapper and preparing methods in an existing class and object
- Pros: More convenient. Just create a method inside form, or datamodule, or any object.
- Cons: When running script, you must be sure that object is instantiated. It's more difficult to reuse code (wrapper and preparation methods).
Using libraries, declaring wrapper and preparing methods in a TatScripterLibrary class descendant
- Pros: No need to check if class is instantiated - scripter does it automatically. It is easy to port code - all methods are inside a class library, so you can add it in any scripter you want, put it in a separate unit, etc..
- Cons: Just the extra work of declaring the new class.
In addition to using AddLibrary method, you can use RegisterScripterLibrary procedure. For example:
RegisterScripterLibrary(TExampleLibrary);
RegisterScripterLibrary(TAnotherLibrary, True);
RegisterScripterLibrary is a global procedure that registers the library in a global list, so all scripter components are aware of that library. The second parameter of RegisterScripterLibrary indicates if the library is load automatically or not. In the example above, TAnotherLibrary is called with Explicit Load (True), while TExampleLibrary is called with Explicit Load false (default is false).
When explicit load is false (case of TExampleLibrary), every scripter that is instantiated in application will automatically load the library.
When explicit load is true (case of TAnotherLibrary), user can load the library dinamically by using uses directive:
SCRIPT:
uses Another;
// Do something with objects and procedures register by TatAnotherLibrary
Note that "Another" name is informed by TatAnotherLibrary.LibraryName class method.
The TatSystemLibrary library
There is a library that is added by default to all scripter components,
it is the TatSystemLibrary. This library is declared in the
uSystemLibrary
unit. It adds commonly used routines and functions to
scripter, such like ShowMessage and IntToStr.
Functions added by TatSystemLibrary
The following functions are added by the TatSystemLibrary (refer to Delphi documentation for an explanation of each function):
- Abs
- AnsiCompareStr
- AnsiCompareText
- AnsiLowerCase
- AnsiUpperCase
- Append
- ArcTan
- Assigned
- AssignFile
- Beep
- Chdir
- Chr
- CloseFile
- CompareStr
- CompareText
- Copy
- Cos
- CreateOleObject
- Date
- DateTimeToStr
- DateToStr
- DayOfWeek
- Dec
- DecodeDate
- DecodeTime
- Delete
- EncodeDate
- EncodeTime
- EOF
- Exp
- FilePos
- FileSize
- FloatToStr
- Format
- FormatDateTime
- FormatFloat
- Frac
- GetActiveOleObject
- High
- Inc
- IncMonth
- InputQuery
- Insert
- Int
- Interpret (*)
- IntToHex
- IntToStr
- IsLeapYear
- IsValidIdent
- Length
- Ln
- Low
- LowerCase
- Machine (*)
- Now
- Odd
- Ord
- Pos
- Raise
- Random
- ReadLn
- Reset
- Rewrite
- Round
- Scripter (*)
- SetOf (*)
- ShowMessage
- Sin
- Sqr
- Sqrt
- StrToDate
- StrToDateTime
- StrToFloat
- StrToInt
- StrToIntDef
- StrToTime
- Time
- TimeToStr
- Trim
- TrimLeft
- TrimRight
- Trunc
- UpperCase
- VarArrayCreate
- VarArrayHighBound
- VarArrayLowBound
- VarIsNull
- VarToStr
- Write
- WriteLn
All functions/procedures added are similar to the Delphi ones, with the exception of those marked with a "*", explained below:
procedure Interpret(AScript: string);
Executes the script source code specified by Ascript parameter
function Machine: TatVirtualMachine;
Returns the current virtual machine executing the script.
function Scripter: TatCustomScripter;
Returns the current scripter component.
function SetOf(array): integer;
Returns a set from the array passed. For example:
MyFontStyle := SetOf([fsBold, fsItalic]);
Removing functions from the System library
To remove a function from the system library, avoiding the end-user to use the function from the script, you just need to destroy the associated method object in the SystemLibrary class:
MyScripter.SystemLibrary.MethodByName('ShowMessage').Free;