Working with scripter
This chapter provides information about how to use the scripter component in your application. How to run scripts, how to integrate Delphi objects with the script, and other tasks are covered here.
Getting started
To start using scripter, you just need to know one property (SourceCode) and one method (Execute). Thus, to start using scripter to execute a simple script, drop it on a form and use the following code (in a button click event, for example):
Scripter.SourceCode.Text := 'ShowMessage(''Hello world!'');';
Scripter.Execute;
And you will get a "Hello world!" message after calling Execute method. That's it. From now, you can start executing scripts. To make it more interesting and easy, drop a TAdvMemo component in form and change code to:
Scripter.SourceCode := AdvMemo1.Lines;
Scripter.Execute;
Now you can just type scripts at runtime and execute them.
From this point, any reference to scripter object (methods, properties, events) refers to TatCustomScripter object and can be applied to TatPascalScripter and TatBasicScripter - except when explicit indicated. The script examples will be given in Pascal syntax.
Cross-language feature: TatScripter and TIDEScripter
TMS Scripter provides a single scripter component that allows cross-language and cross-platform scripting: TatScripter.
Replacing old TatPascalScripter and TatBasicScripter by the new TatScripter is simple and straightforward. It's full compatible with the previous one, and the cross-language works smoothly. There only two things that are not backward compatible by default, but you can change it using properties. The differences are:
OptionExplicit property now is "true" by default
The new TIDEScripter component requires that all variables are declared in script, different from TatPascalScripter or TatBasicScripter. So, if you want to keep the old default functionality, you must set OptionExplicit property to false.ShortBooleanEval property now is "true" by default
The new TIDEScripter component automatically uses short boolean evaluation when evaluation boolean expressions. If you want to keep the old default functionality, set ShortBooleanEval to false.
In addition to the changes above, the new TatScripter and TIDEScripter includes the following properties and methods:
New DefaultLanguage property
TScriptLanguage = (slPascal, slBasic);
property DefaultLanguage: TScriptLanguage;
TatScripter and descendants add the new property DefaultLanguage which is the default language of the scripts created in the scripter component using the old way (Scripter.Scripts.Add). Whenever a script object is created, the language of this new script will be specified by DefaultLanguage. The default value is slPascal. So, to emulate a TatBasicScripter component with TatScripter, just set DefaultLanguage to slBasic. If you want to use Pascal language, it's already set for that.
New AddScript method
function AddScript(ALanguage: TScriptLanguage): TatScript;
If you create a script using old Scripts.Add method, the language of the script being created will be specified by DefaultLanguage. But as an alternative you can just call AddScript method, which will create a new TatScript object in the Scripts collection, but the language of the script will be specified by ALanguage parameter. So, for example, to create a Pascal and a Basic script in the TatScripter component:
MyPascalScript := atScripter1.AddScript(slPascal);
MyBasicScript := atScripter1.AddScript(slBasic);
Using cross-language feature
There is not much you need to do to be able to use both Basic and Pascal scripts. It's just transparent, from a Basic script you can call a Pascal procedure and vice-versa.
Common tasks
Calling a subroutine in script
If the script has one or more functions or procedures declared, than you can directly call them using ExecuteSubRoutine method:
Pascal script:
procedure DisplayHelloWorld;
begin
ShowMessage('Hello world!');
end;
procedure DisplayByeWorld;
begin
ShowMessage('Bye world!');
end;
Basic script:
sub DisplayHelloWorld
ShowMessage("Hello world!")
end sub
sub DisplayByeWorld
ShowMessage("Bye world!")
end sub
CODE:
Scripter.ExecuteSubRoutine('DisplayHelloWorld');
Scripter.ExecuteSubRoutine('DisplayByeWorld');
This will display "Hello word!" and "Bye world!" message dialogs.
Returning a value from script
Execute method is a function, which result type is Variant. Thus, if script returns a value, then it can be read from Delphi code. For example, calling a script function "Calculate":
Pascal script:
function Calculate;
begin
result := (10+6)/4;
end;
Basic script:
function Calculate
Calculate = (10+6)/4
end function
CODE:
FunctionValue := Scripter.ExecuteSubRoutine('Calculate');
FunctionValue will receive a value of 4. Note that you don't need to declare a function in order to return a value to script. Your script and code could be just:
Pascal script:
result := (10+6)/4;
CODE:
FunctionValue := Scripter.Execute;
Tip
In Basic syntax, to return a function value you must use
"FunctionName = Value" syntax. You can also return values in Basic
without declaring a function. In this case, use the reserved word
"MAIN": MAIN = (10+6)/4
.
Passing parameters to script
Another common task is to pass values of variables to script as parameters, in order to script to use them. To do this, just use same Execute and ExecuteSubRoutine methods, with a different usage (they are overloaded methods). Note that parameters are Variant types:
Pascal script:
function Double(Num);
begin
result := Num*2;
end;
Basic script:
function Double(Num)
Double = Num*2
End function
CODE:
FunctionValue := Scripter.ExecuteSubRoutine('Double', 5);
FunctionValue will receive 10. If you want to pass more than one parameter, use a Variant array or an array of const:
Pascal script:
function MaxValue(A,B);
begin
if A > B then
result := A
else
result := B;
end;
procedure Increase(var C; AInc);
begin
C := C + AInc;
end;
CODE:
var
MyVar: Variant;
begin
FunctionValue := Scripter.ExecuteSubRoutine('MaxValue', VarArrayOf([5,8]));
Scripter.ExecuteSubRoutine('Increase', [MyVar, 3]);
end;
Note
To use parameter by reference when calling script subroutines, the variables must be declared as variants. In the example above, the Delphi variable MyVar must be of Variant type, otherwise the script will not update the value of MyVar.
Note
Script doesn't need parameter types, you just need to declare their names.
Accessing Delphi objects
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.
You can also register the parameter hint for the method using UpdateParameterHints 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 parameter, 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.
Accessing 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 "Value" 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 should 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).
To define an indexed property as the default property of a class, set the property TatClass.DefaultProperty after defining the property in Scripter. In above script example (Memo1.Lines.Strings[i]), if the 'Strings' is set as the default property of TStrings class, the string lines of the memo can be accessed by "Memo1.Lines[i]".
Code example (defining TStrings class with Strings default property):
procedure TForm1.PrepareScript;
begin
Scripter.AddComponent(Memo1);
with Scripter.DefineClass(TStrings) do
begin
DefaultProperty := DefineProp('Strings', tkString,
GetStringsProc, SetStringsProc, nil, false, 1);
end;
end;
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 method. 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;
Delphi 2010 and up - Registering using new RTTI
Taking advantage of new features related to RTTI and available from Delphi 2010, TMS Scripter implements methods to make easier the registration of classes, letting them available for use in scripts. So far we need to manually define each method/property of a class (except published properties) - at least there's a nice utility program named "ImportTool" - but from now we can register almost all members of a class automatically and with minimum effort, as seen below.
Registering a class in scripter
To register a class in Scripter, usually we use TatCustomScripter.DefineClass method to define the class, and helper methods to define each class member, and also we need to implement wrapper methods to make the calls for class methods, as well as getters and setters for properties. Example:
with Scripter.DefineClass(TMyClass) do
begin
DefineMethod('Create', 0, tkClass, TMyClass, __TMyClassCreate, true);
DefineMethod('MyMethod', tkNone, nil, __TMyClassMyMethod);
(...)
DefineProp('MyProp', tkInteger, __GetTMyClassMyProp, __SetTMyClassMyProp);
(...)
end;
With new features, just call TatCustomScripter.DefineClassByRTTI method to register the class in scripter, and automatically all their methods and properties:
Scripter.DefineClassByRTTI(TMyClass);
This method has additional parameters that allow you to specify exactly what will be published in scripter:
procedure TatCustomScripter.DefineClassByRTTI(
AClass: TClass;
AClassName: string = '';
AVisibilityFilter: TMemberVisibilitySet = [mvPublic, mvPublished];
ARecursive: boolean = False);
AClass: class to be registered in scripter;
AClassName: custom name for registered class, the original class name is used if empty;
AVisibilityFilter: register only members whose visibility is in this set, by default only public and published members are registered, but you can register also private and protected members;
ARecursive: if true, scripter will also register other types (classes, records, enumerated types) which are used by methods and properties of class being defined. These types are recursively defined using same option specified in visibility filter.
Registering a record in scripter
Since scripter does not provide support for records yet, our recommended solution is to use wrapper classes (inherited from TatRecordWrapper) to emulate a record structure by implementing each record field as a class property. Example:
TRectWrapper = class(TatRecordWrapper)
(...)
published
property Left: Longint read FLeft write FLeft;
property Top: Longint read FTop write FTop;
property Right: Longint read FRight write FRight;
property Bottom: Longint read FBottom write FBottom;
end;
While scripter still remains using classes to emulated records, is no longer necessary to implement an exclusive wrapper class for each record, because now scripter implements a generic wrapper. Thus a record (and automatically all its fields) can be registered into scripter by TatCustomScripter.DefineRecordByRTTI method, as in example below:
Scripter.DefineRecordByRTTI(TypeInfo(TRect));
The method only receives a pointer parameter to record type definition:
procedure TatCustomScripter.DefineRecordByRTTI(ATypeInfo: Pointer);
Records registered in scripter will work as class and therefore need to be instantiated before use in your scripts (except when methods or properties return records, in this case scripter instantiates automatically). Example:
var
R: TRect;
begin
R := TRect.Create;
try
R.Left := 100;
// do something with R
finally
R.Free;
end;
end;
What is not supported
Due to Delphi RTTI and/or scripter limitations, some features are not supported yet and you may need some workaround for certain operations.
Scripter automatically registers only methods declared in public and published clauses of a class, since methods declared as private or protected are not accessible via RTTI. When defining a class with private and protected in visibility filter, scripter will only define fields and properties declared in these clauses.
If a class method has overloads, scripter will register only the first method overload declared in that class.
Methods having parameters with default values, when automatically defined in scripter, are registered with all parameters required. To define method with default parameters, use DefineMethod method, passing number of default arguments in ADefArgCount parameter, and implement the method handler (TMachineProc) to check the number of arguments passed to method by using TatVirtualMachine.InputArgCount function.
Event handlers are not automatically defined by scripter. You must implement a TatEventDispatcher descendant class and use DefineEventAdapter method.
Some methods having parameters of "uncommon" types (such as arrays and others) are not defined in scripter, since Delphi does not provide enough information about these methods.
Accessing Delphi functions, variables and constants
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.
Script-based libraries
Script-based library is the concept where a script can "use" other script (to call procedures/functions, get/set global variables, etc.).
Take, for example, the following scripts:
// Script1
uses Script2;
begin
Script2GlobalVar := 'Hello world!';
ShowScript2Var;
end;
// Script2
var
Script2GlobalVar: string;
procedure ShowScript2Var;
begin
ShowMessage(Script2GlobalVar);
end;
When you execute the first script, it "uses" Script2, and then it is able to read/write global variables and call procedures from Script2.
The only issue here is that script 1 must "know" where to find Script2.
When the compiler reaches a identifier in the uses clause, for example:
uses Classes, Forms, Script2;
Then it tries to "load" the library in several ways. This is the what the compiler tries to do, in that order:
1. Tries to find a registered Delphi-based library with that name
In other words, any library that was registered with
RegisterScripterLibrary. This is the case for the imported VCL that is
provided with Scripter Studo, and also for classes imported by the
import tool. This is the case for Classes
, Forms
, and other units.
2. Tries to find a script in Scripts collection where UnitName matches the library name
Each TatScript object in the Scripter.Scripts collection has a UnitName
property. You can manually set that property so that the script object
is treated as a library in this situations. In the example above, you
could add a script object, set its SourceCode property to the script 2
code, and then set UnitName to 'Script2'. This way, the script1 could
find the script2 as a library and use its variables and functions.
3. Tries to find a file which name matches the library name
(if LibOptions.UseScriptFiles is set to true)
If LibOptions.UseScriptFiles is set to true, then the scripter tries to
find the library in files. For example, if the script has uses Script2;
,
it looks for files named "Script2.psc". There are several
sub-options for this search, and LibOptions property controls this
options:
LibOptions.SearchPath:
It is a TStrings object which contains file paths where the scripter must search for the file. It accepts two constants: "$(CURDIR)" (which contains the current directory) and "$(APPDIR)" (which contains the application path).LibOptions.SourceFileExt:
Default file extension for source files. So, for example, if SourceFileExt is ".psc", the scripter will look for a file named 'Script2.psc'. The scripter looks first for compiled files, then source files.LibOptions.CompileFileExt:
Default file extension for compiled files. So, for example, if CompileFileExt is ".pcu", the scripter will look for a file name 'Script2.pcu'. The scripter looks first for compiled files, then source files.LibOptions.UseScriptFiles:
Turns on/off the support for script files. If UseScriptFiles is false, then the scripter will not look for files.
Declaring forms in script
A powerful feature in scripter is the ability to declare forms and use DFM files to load form resources. With this feature you can declare a form to use it in a similar way than Delphi: you create an instance of the form and use it.
Take the folowing scripts as an example:
// Main script
uses
Classes, Forms, MyFormUnit;
var
MyForm: TMyForm;
begin
{Create instances of the forms}
MyForm := TMyForm.Create(Application);
{Initialize all forms calling its Init method}
MyForm.Init;
{Set a form variable. Each instance has its own variables}
MyForm.PascalFormGlobalVar := 'my instance';
{Call a form "method". You declare the methods in the form script like procedures}
MyForm.ChangeButtonCaption('Another click');
{Accessing form properties and components}
MyForm.Edit1.Text := 'Default text';
MyForm.Show;
end;
// My form script
{$FORM TMyForm, myform.dfm}
var
MyFormGlobalVar: string;
procedure Button1Click(Sender: TObject);
begin
ShowMessage('The text typed in Edit1 is ' + Edit1.Text +
#13#10 + 'And the value of global var is ' + MyFormGlobalVar);
end;
procedure Init;
begin
MyFormGlobalVar := 'null';
Button1.OnClick := 'Button1Click';
end;
procedure ChangeButtonCaption(ANewCaption: string);
begin
Button1.Caption := ANewCaption;
end;
The sample scripts above show how to declare forms, create instances, and use their "methods" and variables. The second script is treated as a regular script-based library, so it follows the same concept of registering and using. See the related topic for more info.
The $FORM directive is the main piece of code in the form script. This directive tells the compiler that the current script should be treated as a form class that can be instantiated, and all its variables and procedures should be treated as form methods and properties. The directive should be in the format {$FORM FormClass, FormFileName}, where FormClass is the name of the form class (used to create instances, take the main script example above) and FormFileName is the name of a DFM form which should be loaded when the form is instantiated. The DFM form file is searched the same way that other script-based libraries, in other words, it uses LibOptions.SearchPath to search for the file.
As an option to load DFM files, you can set the form resource through TatScript.DesignFormResource string property. So, in the TatScript object which holds the form script source code, you can set DesignFormResource to a string which contains the dfm-file content in binary format. If this property is not empty, then the compiler will ignore the DFM file declared in $FORM directive, and will use the DesignFormResource string to load the form.
The DFM file is a regular Delphi-DFM file format, in text format. You cannot have event handlers define in the DFM file, otherwise a error will raise when loading the DFM.
Another thing you must be aware of is that all existing components in the DFM form must be previously registered. So, for example, if the DFM file contains a TEdit and a TButton, you must add this piece of code in your application (only once) before loading the form:
RegisterClasses([TEdit, TButton]);
Otherwise, a "class not registered" error will raise when the form is instantiated.
Declaring classes in script (script-based classes)
It's now possible to declare classes in a script. With this feature you can declare a class to use it in a similar way than Delphi: you create an instance of the class and reuse it.
Declaring the class
Each class must be declared in a separated script, in other words, you
need to have a script for each class you want to declare.
You turn the script into a "class script" by adding the $CLASS directive in the beginning of the script, followed by the class name:
// Turn this script into a class script for TSomeClass
{$CLASS TSomeClass}
Methods and properties
Each global variable declared in a class script actually becomes a
property of the class. Each procedure/function in script becomes a class
method.
The main routine of the script is always executed when a new instance of the class is created, so it can be used as a class initializer and you can set some properties to default value and do some proper class initialization.
// My class script
{$CLASS TMyClass}
uses Dialogs;
var
MyProperty: string;
procedure SomeMethod;
begin
ShowMessage('Hello, world!');
end;
// class initializer
begin
MyProperty := 'Default Value';
end;
Using the classes
You can use the class from other scripts just by creating a new instance
of the named class:
uses MyClassScript;
var
MyClass: TMyClass;
begin
MyClass := TMyClass.Create;
MyClass.MyProperty := 'test';
MyClass.SomeMethod;
end;
Implementation details
The classes declared in script are "pseudo" classes. This means that
no new Delphi classes are created, so for example although in the sample
above you call TMyClass.Create, the "TMyClass" name is just meaning to
the scripting system, there is no Delphi class named TMyClass. All
objects created as script-based classes are actually instances of the
class TScriptBaseObject. You can change this behavior to make instances
of another class, but this new class must inherit from TScriptBaseObject
class. You define the base class for all "pseudo"-classes objects in
scripter property ScriptBaseObjectClass.
Memory management
Although you can call Free method in scripts to release memory
associated with instances of script-based classes, you don't need to do
that.
All objects created in script that are based on script classes are eventually destroyed by the scripter component.
Limitations
Since scripter doesn't create new real Delphi classes, there are some
limitations about what you can do with it. The main one is that
inheritance is not supported. Since all classes in script are actually
the same Delphi class, you can't create classes that inherit from any
other Delphi class except the one declared in TScriptBaseObject class.
Using the Refactor
Every TatScript object in Scritper.Scripts collection has its own refactor object, accessible through Refactor property. The Refactor object is just a collection of methods to make it easy and safe to change source code. As long as new versions of TMS Scripter are released, some new refactoring methods might be added. For now, these are the current available methods:
procedure UpdateFormHeader(AFormClass, AFileName: string); virtual;
Create (or update) the FORM directive in the script giving the AFormClass (form class name) and AFileName (form file name). For example, the code below:
UpdateFormHeader('TMyForm', 'myform.dfm');
will create (or update) the form directive in the script as following (in this case, the example is in Basic syntax):
#FORM TMyForm, myform.dfm
function DeclareRoutine(ProcName: string): integer; overload;
Declare a routine named ProcName in source code, and return the line number of the declared routine. The line number returned is not the line where the routine is declared, but the line with the first statement. For example, in Pascal, it returns the line after the "begin" of the procedure.
function DeclareRoutine(AInfo: TatRoutineInfo): integer; overload; virtual;
Declare a routine in source code, and return the line number of the declared routine. The line number returned is not the line where the routine is declared, but the line with the first statement. For example, in Pascal, it returns the line after the "begin" of the procedure.
This method uses the AInfo property to retrieve information about the procedure to be declared. Basicaly is uses AInfo.Name as the name of routine to be declared, and also uses AInfo.Variables to declare the parameters. This is a small example:
AInfo.Name := 'MyRoutine';
AInfo.IsFunction := true;
AInfo.ResultTypeDecl := 'string';
with AInfo.Variables.Add do
begin
VarName := 'MyParameter';
Modifier := moVar;
TypeDecl := 'integer';
end;
with AInfo.Variables.Add do
begin
VarName := 'SecondPar';
Modifier := moNone;
TypeDecl := 'TObject';
end;
ALine := Script.DeclareRoutine(AInfo);
The script above will declare the following routine (in Pascal):
function MyRoutine(var MyParameter: integer; SecondPar: TObject): string;
procedure AddUsedUnit(AUnitName: string); virtual;
Add the unit named AUnitName to the list of used units in the uses clause. If the unit is already used, nothing is done. If the uses clause is not present in the script, it is included. Example:
AddUsedUnit('Classes');
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;
The TatVBScriptLibrary library
The TatVBScriptLibrary adds many VBScript-compatible functions. It's useful to give to your end-user access to the most common functions used in VBScript, making it easy to write Basic scripts for those who are already used to VBScript.
How to use TatVBScriptLibrary
Unlike to TatSystemLibrary, the TatVBScriptLibrary is not automatically
added to scripter components. To add the library to scripter and thus
make use of the functions, you just follow the regular steps described
in the section Delphi-based libraries,
which are described here again:
a. First, you must use the uVBScriptLibrary
unit in your Delphi code:
uses uVBScriptLibrary;
b. Then you just add the library to the scripter component, from Delphi code:
atBasicScripter1.AddLibrary(TatVBScriptLibrary);
or, enable the VBScript libraries from the script code itself, by adding VBScript in the uses clause:
'My Basic Script
uses VBScript
Functions added by TatVBScriptLibrary
The following functions are added by the TatVBScriptLibrary (refer to
MSDN documentation for the explanation of each function):
- Asc
- Atn
- CBool
- CByte
- CCur
- CDate
- CDbl
- Cint
- CLng
- CreateObject
- CSng
- CStr
- DatePart
- DateSerial
- DateValue
- Day
- Fix
- FormatCurrency
- FormatDateTime
- FormatNumber
- Hex
- Hour
- InputBox
- InStr
- Int
- IsArray
- IsDate
- IsEmpty
- IsNull
- IsNumeric
- LBound
- LCase
- Left
- Len
- Log
- LTrim
- Mid
- Minute
- Month
- MonthName
- MsgBox
- Replace
- Right
- Rnd
- RTrim
- Second
- Sgn
- Space
- StrComp
- String
- Timer
- TimeSerial
- TimeValue
- UBound
- UCase
- Weekday
- WeekdayName
- Year
Debugging scripts
TMS Scripter contains components and methods to allow run-time script debugging. There are two major ways to debug scripts: using scripter component methods and properties, or using debug components. Use of methods and properties gives more flexibility to programmer, and you can use them to create your own debug environment. Use of components is a more high-level debugging, where in most of case all you need to do is drop a component and call a method to start debugging.
Using methods and properties for debugging
Scripter component has several properties and methods that allows script debugging. You can use them inside Delphi code as you want. They are listed here:
property Running: boolean;
Read/write property. While script is being executed, Running is true. Note that the script might be paused but still running. Set Running to true is equivalent to call Execute method.
property Paused: boolean read GetPaused write SetPaused;
Read/write property. Use it to pause script execution, or get script back to execution.
procedure DebugTraceIntoLine;
Executes only current line. If the line contains a call to a subroutine, execution point goes to first line of subroutine. Similar to Trace Into option in Delphi.
procedure DebugStepOverLine;
Executes only current line and execution point goes to next line in code. If the current line contains a call to a subroutine, it executes the whole subroutine. Similar to Step Over option in Delphi.
procedure DebugRunUntilReturn;
Executes code until the current subroutine (procedure, function or script main block) is finished. Execution point stops one line after the line which called the subroutine.
procedure DebugRunToLine(ALine: integer);
Executes script until line specified by ALine. Similar to Run to Cursor option in Delphi.
function DebugToggleBreakLine(ALine: integer): pSimplifiedCode;
Enable/disable a breakpoint at the line specified by ALine. Execution stops at lines which have breakpoints set to true.
function DebugExecutionLine: integer;
Return the line number which will be executed.
procedure Halt;
Stops script execution, regardless the execution point.
property Halted: boolean read GetHalted;
This property is true in the short time period after a call to Halt method and before script is effectively terminated.
property BreakPoints: TatScriptBreakPoints read GetBreakPoints;
Contains a list of breakpoints set in script. You can access breakpoints using Items[Index] property, or using method BreakPointByLine(ALine: integer). Once you access the breakpoint, you can set properties Enabled (which indicates if breakpoint is active or not) and PassCount (which indicates how many times the execution flow will pass through breakpoint until execution is stopped).
property OnDebugHook: TNotifyEvent read GetOnDebugHook write SetOnDebugHook;
During debugging (step over, step into, etc.) OnDebugHook event is called for every step executed.
property OnPauseChanged: TNotifyEvent read GetOnPauseChanged write SetOnPauseChanged;
property OnRunningChanged: TNotifyEvent read GetOnRunningChanged write SetOnRunningChanged;
These events are called whenever Paused or Running properties change.
Using debug components
TMS Scripter has specific component for debugging (only for VCL applications). It is TatScriptDebugDlg. Its usage is very simple: drop it on a form and assign its Scripter property to an existing script component. Call Execute method and a debug dialog will appear, displaying script source code and with a toolbar at the top of window. You can then use tool buttons or shortcut keys to perform debug actions (run, pause, step over, and so on). Shortcut keys are the same used in Delphi:
- F4: Run to cursor
- F5: Toggle breakpoint
- F7: Step into
- F8: Step Over
- F9: Run
- Shift+F9: Pause
- Ctrl+F2: Reset
- Shift+F11: Run until return
Form-aware scripters - TatPascalFormScripter and TatBasicFormScripter
TatPascalFormScripter and TatBasicFormScripter are scripters that descend from TatPascalScripter and TatBasicScripter respectively. They have the same functionality of their ancestor, but in addition they already have registered the components that are owned by the form where scripter component belongs to.
So, if you want to use scripter to access components in the form, like buttons, edits, etc., you can use form-aware scripter without needing to register form components.
C++ Builder issues
Since TMS Scripter works with objects and classes types and typecasting, it might be some tricky issues to do some tasks in C++ Builder. This section provides useful information on how to write C++ code to perform some common tasks with TMS Scripter.
Registering a class method for an object
Let's say you have created a class named testclass, inherited from TObject:
[in .h file]
class testclass : public Tobject
{
public:
AnsiString name;
int number;
virtual __fastcall testclass();
};
[in .cpp file]
__fastcall testclass::testclass()
: Tobject()
{
this->name = "test";
this->number = 10;
ShowMessage("In constructor");
}
If you want to add a class method "Create" which will construct a testclass from script and also call the testclass() method, you must register the class in script registration system:
scr->DefineMethod("create", 0, Typinfo::tkClass, __classid(testclass), constProc, true);
Now you must implement constProc method which will implement the constructor method itself:
void __fastcall TForm1::constProc(TatVirtualMachine* avm)
{
testclass *l_tc;
l_tc = (testclass *) avm->CurrentObject;
l_tc = new testclass;
avm->ReturnOutputArg((long)(l_tc));
}