Programming Tools:prgtools

Generating code Foundation

The FormDesigner is able to generate code for the forms it creates. This allows to recreate those forms at runtime. Code can be generated in either functional or object-oriented style. The type of source code which is generated by the FormDesigner is selected in the menu system ("Form->FUNCTION Code" or "Form->CLASS Code"). As an alternative, code of the default type is created by clicking an icon in the tool palette. The default source code type is selected in the Settings dialog.

FUNCTION Code

Generating FUNCTION code for a form allows to recreate the form at runtime using code laid out in procedural fashion. Unlike CLASS code which creates a class, the form and its elements is defined in code which is located in a single function.

The option for generating FUNCTION code is intended primarily for developers which are new to Xbase++, or which are unfamiliar with the object-oriented programming paradigm used with the Xbase Part library. Studying FUNCTION code allows to you to familiarize yourself with creating and manipulating Xbase Parts using code that is simple and organized in sequential fashion. In addition, FUNCTION code can be integrated in existing projects. However, no provisions are made to distinguish between the user and the implementation-level of a form. This causes the contents to be overwritten each time FUNCTION code is generated into an existing file.

Before source code is generated by the FormDesigner, it is recommended to check the order in which the Xbase Parts are created in the form. Otherwise, navigating between form elements using the tab key may not activate the controls in the desired order. The order can be checked and/or modified in the Tab Order (menu items: Edit->Tab Order).

A sample form can be found in the ..\SAMPLES\BASICS\XPPFD directory which was created by the FormDesigner. It is used as an example for further explanations and is shown below:

Example for a customer form

The form contains two grouped XbpSLEs to edit database fields. Pushbuttons are arranged below the Group box for database navigation and to close the form. Both types of source code for this form, functional and object-oriented, exist in the ..\SAMPLES\BASICS\XPPFD directory. It can be compiled and executed. For the source code to function correctly, the FormDesigner makes some simplifying assumptions that must be considered when the code is integrated into an application program.

The functional source code starts with all necessary #include directives and declares the Main procedure plus some LOCAL variables. Then all databases that are accessed by a form are opened with the USE command. The sample form uses only one database:

#include "Gra.ch" 
#include "Xbp.ch" 
#include "Appevent.ch" 

PROCEDURE Main 
   LOCAL nEvent, mp1, mp2 
   LOCAL oDlg, oXbp, drawingArea, oXbp1, aEditControls := {} 

   // Path is abbreviated only in the documentation 
   USE ..\SAMPLES\BASICS\XPPFD\CUSTOMER.DBF NEW EXCLUSIVE 

The LOCAL variable aEditControls is initialized with an empty array. It is used to collect all Xbase Parts that access database fields. It provides a comfortable way to access all Xbase Parts in a user-defined function that edit data in the form.

Since the FormDesigner cannot make any assumptions about possible record locking strategies in a multi-user scenario, the databases are opened for exclusive access. Therefore, the program lines that open databases with the USE command are most likely subject to change when the source code is integrated into an application program.

After the databases are opened, the form is created as an instance of the XbpDialog class and a reference to the drawing area is assigned to a LOCAL variable.

oDlg := XbpDialog():new( AppDesktop(), , ; 
                         {148,98}, {334,182}, , .F.) 
oDlg:border := XBPDLG_SIZEBORDER 
oDlg:title := "New Form" 
oDlg:create() 

drawingArea := oDlg:drawingArea 
drawingArea:setFontCompoundName( "8.Helv.normal" ) 

These lines of code define parent, position and size of the form plus border type, window title and font. This code most probably needs to be modified except for position and size. Especially the parent window should be checked if it is correct. The FormDesigner assumes the desktop window will be used as parent and writes the function AppDesktop() to retrieve it.

The LOCAL variable drawingArea references the drawing area of the form. It is used as parent for subsequent Xbase Parts:

oXbp1 := XbpStatic():new( drawingArea, , {12,48}, {300,96} ) 
oXbp1:caption := "Customer" 
oXbp1:type := XBPSTATIC_TYPE_GROUPBOX 
oXbp1:create() 

These four lines of code create the group box used to group the entry fields in the form. Since it serves as parent for the XbpSLEs, the XbpStatic object is assigned to a separate LOCAL variable oXbp1. This variable is used in the source code to define the parent for XbpSLE objects:

oXbp := XbpStatic():new( oXbp1, , {12,48}, {72,24} ) 
oXbp:caption := "Lastname:" 
oXbp:options := XBPSTATIC_TEXT_VCENTER+XBPSTATIC_TEXT_RIGHT 
oXbp:create() 

oXbp := XbpSLE():new( oXbp1, , {96,48}, {192,24} ) 
oXbp:bufferLength := 20 
oXbp:tabStop := .T. 
oXbp:dataLink := {|x| IIf( PCount()==0, ; 
                         Trim( CUSTOMER->LASTNAME ), ; 
                               CUSTOMER->LASTNAME := x ) } 
oXbp:create():setData() 
AAdd( aEditControls, oXbp ) 

When Xbase Parts access database fields but have no caption, an XbpStatic object is always created to display text left to the entry field. The text defaults to the field name and is displayed right-aligned in mixed case.

For the program logic, the :dataLink code block is essential since it performs read and write access to a single field. In functional style source code, literals appear in the code block for alias and field name. Therefore, the code must be adapted, for example, when a different alias name is specified in the USE command.

The :bufferLength instance variable limits the number of characters that can be entered into the edit buffer of an XbpSLE object. It equals to the length of the corresponding database field, so an XbpSLE cannot edit more characters than can be stored in a field. Since blank spaces are valid characters for an XbpSLE, the :dataLink code block must remove trailing spaces when it reads a database field. Otherwise the edit buffer is already full after read access and no changes will be possible when the XbpSLE is in insert mode. Characters can only be overwritten in this case.

in functional code, the LOCAL variable oXbp always receives the reference to an Xbase Part which is not used as parent. In the example above, it first references an XbpStatic object. After the :create()method is executed, a new object reference is assigned to this variable (oXbp := XbpSLE():new(...)). This does not overwrite or destroy the XbpStatic object. When the :create() method is finished, the XbpStatic object is already added to the child list of the parent, which is the Group box referenced in oXbp1. A reference to the XbpStatic object could be retrieved with the expression oXbp1:childList()[1]. After the XbpSLE object is created, it is added to the aEditControls array. All Xbase Parts that access database fields via the :dataLink code block are added to this array and can be accessed in a program using this variable.

The source code generated for the creation of Xbase Parts can be quite extensive and may look complex at the beginning. However, it consists only of a simple pattern that is repeated:

oXbp := Xbp...():new( <parent>, , <position>, <size> ) 
oXbp:<configuration> := <value> 
oXbp:create() 

This pattern corresponds to the "life cycle" of Xbase Parts. An instance of a class is created with the :new() method. The instance (object) gets assigned configuration values. According to the configuration, system resources are requested from the operating system with the :create() method. After :create(), the Xbase Part functions and is referenced in the child list of its parent. Therefore, the LOCAL variable oXbp can be reused.

The FormDesigner generates standardized source code and releases a programmer from a lot of 'typing work'. Source code for the layout and the creation of a form, or for accessing database fields (:dataLink), can be standardized. The limit of the FormDesigner is reached, however, when it comes to program flow or logic. This cannot be standardized and it remains a programmer's task to adapt generated source code in order to integrate it into an application program.

But in programming there are situations common to many applications. Moving the record pointer is an example. For this, a set of predefined pushbuttons can be selected in the FormDesigner and inserted in the form. The following lines of code are an example for a pushbutton that moves the record pointer to the next record:

oXbp := XbpPushButton():new( drawingArea, , {12,12}, {72,24} ) 
oXbp:caption := "Next" 
oXbp:create() 
oXbp:activate := {|| Gather( aEditControls ), ; 
                     CUSTOMER->( DbSkip( 1) ), ; 
                     Scatter( aEditControls ) } 

In this case, the program is controlled by the :activate code block which is evaluated when the pushbutton is clicked. The function Gather() writes the current values from the form into the database. Then, the record pointer is moved by the DbSkip() function, and finally values from the new record are transferred to the form with the Scatter() function. The new values are then visible on the screen.

The most important aspects of the FormDesigner can be summarized as follows:

The FormDesigner generates source code based on the visual representation of a form.
The order with which Xbase Parts are defined in a form must be adjusted to correspond with user interaction.
Source code that controls program flow is created to a very limited extent. There is only a set of pushbuttons which perform common tasks with their :activate code block.
The generated source code must be modified when integrating it into an application program.
CLASS Code

The Xbase++ FormDesigner is able to create source code for a form in object-oriented style. This includes the declaration of a class (whose instances display the form) plus code for the :init() and :create()methods. For the creation of object-oriented code, the same rules are valid as for functional code (refer to the previous section).

Before source code is generated by the FormDesigner, it is recommended to check the order in which the Xbase Parts are created in the form. Otherwise, navigating between form elements using the tab key may not activate the controls in the desired order. The order can be checked and/or modified in the Tab Order (menu items: Edit->Tab Order). In addition, the names of the instance variables of the class should be defined. If they are not defined, the FormDesigner creates default names for instance variables, or it uses field names as symbols when an Xbase Part accesses a database field. For example, the entry fields defined in the form will be referenced via instance variables named "SLE1", "SLE2" and so on in the resulting code. Therefore, the names of the instance variables should be edited to increase code readability prior to generating the source code. This is accomplished in the variable names dialog (menu item: Edit->Variable Names).

The object-oriented code takes advantage of inheritance. Two PRG files are created, each of which contains the code for one class. The first file contains the implementation level of the form while the second one provides the utilization level. The code is split into two files and one class (utilization level) is derived from the other one (implementation level). For the example form, discussed in the previous section, two files exist in the ..\SAMPLES\BASICS\XPPFD directory: SAMPLE2.PRG and _SAMPLE2.PRG. Both contain code to create one class.

Implementation level 
-------------------- 
   File name  : _SAMPLE2.PRG 
   Declaration: CLASS _CustomerForm FROM XbpDialog 

Utilization level 
----------------- 
   File name  : SAMPLE2.PRG 
   Declaration: CLASS CustomerForm FROM _CustomerForm 

For the creation of the sample code the file name SAMPLE2.PRG and class name CustomerForm were used. The FormDesigner prefixes both names with an underscore and uses the resulting identifiers for the source code that builds the implementation level of a form. The implementation level class has instance variables for each Xbase Part contained in the form. In addition, instance variables for each accessed database plus the instance variable :editControls are declared. The source code of the sample form for the implementation level is given below:

CLASS _CustomerForm FROM XbpDialog 
   EXPORTED: 
   VAR editControls 

   * Contained control elements 
   VAR GroupBox      
   VAR Static1       
   VAR Lastname      
   VAR Static2       
   VAR Firstname     
   VAR ButtonNext    
   VAR ButtonPrevious 
   VAR ButtonOK      

   * Work area / alias 
   VAR CUSTOMER 

   METHOD init 
   METHOD create 
ENDCLASS 

In addition to instance variables, the methods :init() and :create() are declared. They initialize the form and request system resources for it. The source code for these methods is created by the FormDesigner. Both methods have the same parameter profile as other Xbase Parts. Default values for all parameters are defined in the :init() method.

METHOD _CustomerForm:init( oParent, oOwner, aPos, aSize, aPP, lVisible ) 

   DEFAULT oParent  TO AppDesktop(), ; 
           aPos     TO {148,98}, ; 
           aSize    TO {334,182}, ; 
           lVisible TO .F. , ; 
           aPP      TO {} 

   AAdd ( aPP, { XBP_PP_COMPOUNDNAME, "8.Helv.normal" } ) 
   ::XbpDialog:init( oParent, oOwner, aPos, aSize, aPP, lVisible ) 
   ::XbpDialog:border := XBPDLG_SIZEBORDER 
   ::XbpDialog:title := "New Form" 

The default value for oParent (the parent window) is given by the calling AppDesktop(), which returns the desktop window. At this point, the same assumption is made as in functional code. If the desktop window should not be used as parent window, the parent must either be explicitly specified or the code of the :init() method must be adjusted.

If the form should be used as application window, an instance of the class is to be created in AppSys() and passed to SetAppWindow().

The major task of the :init() method is to initialize the super class (XbpDialog) and all contained Xbase Parts. The code for the initialization of Xbase Parts is very similar to functional code as described in the previous section. The only difference is that object references are assigned to instance variables instead of LOCAL variables. The following lines of code show a comparison of functional and object-oriented code which create the Group box in the sample form:

Functional code 

   oXbp1 := XbpStatic():new( drawingArea, , ; 
                             {12,48}, {300,96} ) 
   oXbp1:caption := "Customer" 
   oXbp1:type := XBPSTATIC_TYPE_GROUPBOX 


Object-oriented code 

   ::GroupBox       := XbpStatic():new( ::drawingArea, , ; 
                                        {12,48}, {300,96} ) 
   ::GroupBox:caption := "Customer" 
   ::GroupBox:type := XBPSTATIC_TYPE_GROUPBOX 

In functional style the LOCAL variables oXbp1 and drawingArea are used, whereas the object-oriented code uses the instance variables ::GroupBox and ::drawingArea to store references to Xbase Parts. ::GroupBox is declared in the _CustomerForm class and ::drawingArea is an instance variable of the super class XbpDialog.

::GroupBox is an abbreviation for self:GroupBox, ::drawingArea is the abbreviation for self:drawingArea.

The code that initializes Xbase Parts, which access database fields, is also similar in functional and object-oriented style:

Functional code 

   oXbp := XbpSLE():new( oXbp1, , {96,48}, {192,24} ) 
   oXbp:bufferLength := 20 
   oXbp:tabStop := .T. 
   oXbp:dataLink := {|x| IIf( PCount()==0, ; 
                            Trim( CUSTOMER->LASTNAME ), ; 
                                  CUSTOMER->LASTNAME := x ) } 
   oXbp:create():setData() 
   AAdd( aEditControls, oXbp ) 


Object-oriented code 

   ::Lastname := XbpSLE():new( ::GroupBox, , {96,48}, {192,24} ) 
   ::Lastname:bufferLength := 20 
   ::Lastname:tabStop := .T. 
   ::Lastname:dataLink := {|x| IIf( PCount()==0, ; 
                               Trim( (::CUSTOMER)->LASTNAME ) , ; 
                                     (::CUSTOMER)->LASTNAME := x ) } 
   AAdd( ::editControls, ::Lastname ) 

In object-oriented code, the XbpSLE object is referenced in the ::Lastname instance variable. It has the same name, or symbol, as the database field that is edited by the XbpSLE object. When an Xbase Part accesses a field, the FormDesigner uses the field name as identifier for the corresponding instance variable by default.

The code for the :dataLink code block is also different in object-oriented code. Access to a field is programmed as (Alias)->Fieldname and the work area is determined by the contents of an instance variable. Functional code uses literal field names and aliases like Alias->Fieldname.

The final difference between functional and object-oriented code is the call to the :create() and :setData() methods. In functional code, both methods are called in the very same line of the program. In object-oriented code, these methods are called in two different classes: the method :create() is called in the _CustomerDialog class (Implementation level, _SAMPLE2.PRG) and :setData() is executed in the CustomerDialog class (Utilization level, SAMPLE2.PRG).

File: _SAMPLE2.PRG (Implementation level) 

   CLASS _CustomerForm FROM XbpDialog 

   METHOD _CustomerForm:create( ... ) 
      ::XbpDialog:create( ... ) 
      <...> 
      ::Lastname:create() 
      <...> 
   RETURN self 


File: SAMPLE2.PRG (Utilization level) 

   CLASS CustomerForm FROM _CustomerForm 

   METHOD CustomerForm:create( ... ) 

      * Execute method of the super class 
      ::_CustomerForm:create( ... ) 

   // Path is abbreviated only in the documentation 

      * Open databases and assign work area 
      USE ..\SAMPLES\BASICS\XPPFD\CUSTOMER.DBF NEW EXCLUSIVE 
      ::CUSTOMER := Select() 

      * Transfer values to EditControls 
      AEval ( ::EditControls, { | oXbp | oXbp:SetData() } ) 

      * Display the form 
      ::show() 

   RETURN self 

The file _SAMPLE2.PRG contains the class declaration for the sample form plus the code required to create all Xbase Parts contained in the form. The file is the implementation level of the form. To utilize a form, it is necessary to open databases and to transfer values from the current record into the form. This is the task of a derived class. It is programmed in the file SAMPLE2.PRG and provides the utilization level of the form.

The source code for the utilization level may (and should) be modified when it is integrated into an application program. For instance, the file name of the database appears as literal name after the USE command and includes drive and path. This line definitely must be changed to reflect the situation in an application program. Whether or not databases are opened in the :create() method, or are already open when this method is called, must be decided by a programmer. The FormDesigner cannot solve such questions. It only makes sure that the generated code can be compiled in order to test the form in an isolated executable file.

To test the object-oriented code of a form, it is necessary to declare a Main procedure. This is optionally being done by the FormDesigner when the corresponding switch is set in the Settings dialog (check box: Main procedure for class code). In this case, the Main procedure is written to the file that contains the utilization level of a form. In the example, this is the file SAMPLE2.PRG. The Main procedure creates a form and processes events within a DO WHILE loop:

****************************************************************** 
* Main procedure to test a form 
****************************************************************** 
PROCEDURE Main 
   LOCAL nEvent, oXbp, mp1, mp2 

   CustomerForm():New():Create() 

   DO WHILE nEvent != xbeP_Close 
      nEvent := AppEvent ( @mp1, @mp2, @oXbp ) 
      oXbp:HandleEvent ( nEvent, mp1, mp2 ) 
      IF nEvent == xbeP_Quit 
         QUIT   // AppQuit() 
      ENDIF 
   ENDDO 

RETURN 

* Include program code for the implementation-level class of the form 
#include "_SAMPLE2.PRG" 

// EOF 
////// 

The main procedure, as generated by the FormDesigner, is only intended to test a form. Prior to the event loop, the class is instantiated without even assigning the object reference to a variable. The expression CustomerForm():New():Create() creates the form as defined in the class and stores the object reference in the child list of the application window (-> SetAppWindow():childList()[1]).

To create an executable file for testing a form, two PRG files must be compiled and linked (SAMPLE2.PRG and _SAMPLE2.PRG). This is accomplished by including the file containing the implementation level (_SAMPLE2.PRG) in the file with the utilization level (SAMPLE2.PRG). When the compiler compiles the file SAMPLE2.PRG, the preprocessor already has processed the #include "_SAMPLE2.PRG" directive and only one file needs to be compiled. This file contains the declaration for both classes, CustomerForm and _CustomerForm.

Using CLASS Code

To integrate the object-oriented code into an application program, it may (and should) be modified. It is recommended to make changes only in the file that contains the source code for the utilization level of a form. Then it is possible to change the form at a later point in time and have the FormDesigner generating new source code that reflects any changes. If a form is changed after source code is generated, the Form Designer generates only code for the implementation of a form. The file that contains the utilization level, or application specific code, remains unchanged.

An example of how object-oriented code can be changed is the file SAMPLE3.PRG which is located in the ..\SAMPLES\BASICS\XPPFD directory. The CustomerForm class from SAMPLE2.PRG is modified in such a way that the form can be used in a multi-user environment. Three methods are added to the class declaration and code for these methods is implemented.

CLASS CustomerForm FROM _CustomerForm 
   EXPORTED: 
   METHOD init 
   METHOD create 

   * Application specific methods (they are user-defined) 
   METHOD skip 
   METHOD setData 
   METHOD getData 
ENDCLASS 

Before the record pointer is moved, all data changed in a form must be written to the database. In a multi-user environment this requires the record to be locked before it can be written to. This program logic is realized as application-specific code in the new methods :skip(), :setData() and :getData() and is the only difference between the files SAMPLE2.PRG and SAMPLE3.PRG. The :init() method ensures that the new methods are executed:

METHOD CustomerForm:init( oParent, oOwner, aPos, aSize, aPP, lVisible ) 

   * Execute method of the super class 
   ::_CustomerForm:init( oParent, oOwner, aPos, aSize, aPP, lVisible ) 

   * Change code blocks for pushbuttons 
   ::ButtonOK:activate       := {|| ::getData(), ; 
                                    PostAppEvent( xbeP_Close ) } 
   ::ButtonPrevious:activate := {|| ::skip(-1) } 
   ::ButtonNext:activate     := {|| ::skip( 1) } 
RETURN self 

The pushbuttons contained in the form are referenced in instance variables which are declared in the _CustomerForm class (_SAMPLE2.PRG). The :activate code blocks are changed, so the new methods will be executed when a user clicks a pushbutton. The implementation of the new methods requires only a few lines of code:

METHOD CustomerForm:skip( nSkip ) 
   IF ::getData()                      // Write record 
      (::CUSTOMER)->(DbSkip( nSkip ))  // Change record pointer 
      ::setData()                      // Read record 
   ENDIF 
RETURN self 


METHOD CustomerForm:setData 
   AEval( ::editControls, {|o| o:setData() } ) 
RETURN self 


METHOD CustomerForm:getData 
   LOCAL lOk := ( AScan( ::editControls, {|o| o:changed } ) == 0 ) 
   
   IF ! lOk                            // Data is changed 
      lOk := (::CUSTOMER)->(DbRLock()) // Lock record 
      IF lOk                           // Record is locked 
         AEval( ::editControls, {|o| o:getData() } ) 
         (::CUSTOMER)->(DbUnlock())    // Unlock record 
      ELSE 
         MsgBox( "Record is currently locked" ) 
      ENDIF 
   ENDIF 
RETURN lOk 

The :skip() method navigates the record pointer in this example only if changed data in the form is written to the database. This, in turn, depends on a successful record lock which is set in the :getData()method. If data is unchanged, no locking occurs and the record pointer is always moved. When the record pointer is changed, the :setData()method transfers data from the database into the form.

Feedback

If you see anything in the documentation that is not correct, does not match your experience with the particular feature or requires further clarification, please use this form to report a documentation issue.