Search Results for

    Show / Hide Table of Contents

    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;
    

    C++Builder example

    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:

    1. 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.

    2. 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);
    

    C++Builder example

    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');
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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);
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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));
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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;
    

    C++Builder example

    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)); 
    }
    
    In This Article
    Back to top TMS Scripter v7.36
    © 2002 - 2025 tmssoftware.com