Programming Guide:xppguide

Creating GUI applications Foundation

This section shows how to design and program GUI applications. It serves both as a guide for programmers new to a graphic user interface and as a source of solutions to some of the problems that may arise when programming GUI applications. Most of the example programs discussed in this section are also provided in the Xbase++ installation. Tips are included for organizing GUI applications and the answers to questions such as "How is this programmed?" and "Where is this implemented?" are discussed.

Tasks of AppSys()

The main task of the AppSys() function is to create the application window. Since AppSys() is an implicit INIT PROCEDURE, it is always called prior to the Main procedure. The application window object created in AppSys() depends on the type of application. It could be an XbpCrt window or an XbpDialog window. In order to ensure the widest possible compatibility, the default AppSys() routine included in Xbase++ creates an XbpCrt window. When developing new GUI applications using Xbase++, AppSys() should generally be changed to create an XbpDialog window instead. Additional tasks that must be performed only once at application startup can also be included in this procedure. This often includes creating the menu system, providing a help routine and initializing system wide variables or other necessary resources. These tasks can be accomplished before the application window is even visible, which allows the essential parts of the application to already be available when the Main procedure is called.

The first decision in implementing AppSys() is whether to use XbpCrt or XbpDialog windows. In the case of a GUI application, the application type must also be considered. The concept "application type" designates the kind of user interface that the application will provide. The simpler case is an SDI application (Single Document Interface) where the application consists of a single window. The alternative is an MDI application (Multiple Document Interface). An MDI application runs in multiple windows and AppSys() just creates the main window allowing the additional windows within the main window to be generated later in the program. The size of the application window generally depends on the application type. The application window of an SDI application can be smaller than that of an MDI application, since no additional windows are needed in an SDI application. Also, the window size of an SDI application can be fixed, not allowing the user to change the size. The size of the application window of an MDI application must be changeable by the user.

The following example is the AppSys() procedure from the file SDIDEMO.PRG that presents a complete example of an SDI application. The various tasks performed by AppSys() are demonstrated in the following procedure:

PROCEDURE AppSys 
   LOCAL oDlg, oXbp, aPos[2], aSize, nHeight:=400, nWidth := 615 

   // Get size of desktop window 
   // to center the application window 
   aSize    := SetAppWindow():currentSize() 
   aPos[1]  := Int( (aSize[1]-nWidth ) / 2 ) 
   aPos[2]  := Int( (aSize[2]-nHeight) / 2 ) 

   // Create application window 
   oDlg := XbpDialog():new() 
   oDlg:title := "Toys & Fun Inc. [Xbase++ - SDI Demo]" 
   oDlg:border:= XBPDLG_THINBORDER 
   oDlg:create( ,, aPos, {nWidth, nHeight},, .F. ) 

   // Set background color for drawing area 
   oDlg:drawingArea:SetColorBG( GRA_CLR_PALEGRAY ) 

   // Select font 
   oDlg:drawingArea:SetFontCompoundName( "8.Help.normal" ) 

   // Create menu system (UDF) 
   MenuCreate( oDlg:menuBar() ) 

   // Provide online help via UDF 
   oXbp := XbpHelpLabel():new():create() 
   oXbp:helpObject := ; 
        HelpObject( "SDIDEMO.HLP", "Help for SDI demo" ) 
   oDlg:helpLink := oXbp 

   // Display application window and set focus to it 
   oDlg:show() 
   SetAppWindow( oDlg ) 
   SetAppFocus ( oDlg ) 

RETURN 

In this example, a dialog window is created with the size 615 x 400 pixels. This size allows it to be completely displayed even on a low resolution screen. The window is provided for an SDI application and has a fixed size (XBPDLG_THINBORDER). The first call to SetAppWindow() provides a reference to the desktop on which the application window is displayed. The :currentSize() method of this object provides the size of the desktop window corresponding to the current screen resolution. This information is used to position the application window when the Xbase++ application is called. In the example, the application window is displayed centered on the screen.

After the background color for the drawing area of the dialog window is set, the menu system is generated in the function MenuCreate(). This user-defined function (UDF) receives the return value of the method :menuBar() as an argument. The :menuBar() method creates an XbpMenuBar object and installs it in the application window. The menu system must then be constructed in the UDF. This approach is recommended because the menu system construction can be performed before the application window is visible. The mechanics of constructing a menu system is described in the next section.

In this AppSys() example, the mechanism for the online help is implemented after the menu system is created in MenuCreate(). This includes the generation of an XbpHelpLabel object that is assigned to the instance variable :helpLink. The help label object references help information and activates the window of the online help. The online help window is in turn managed by an XbpHelp object which must be provided to the XbpHelpLabel object. This is done by assigning an XbpHelp object to the :helpObject instance variable of the XbpHelpLabel object. The XbpHelp object manages online help windows and should exist only once within an Xbase++ application. For this reason it is created in the user-defined function HelpObject() which is shown below:

******************************************************************** 
* Routine to retrieve the help object. It manages the online help 
******************************************************************** 
FUNCTION HelpObject( cHelpFile, cTitle ) 
   STATIC soHelp 

   IF soHelp == NIL 
      soHelp := XbpHelp():new() 
      soHelp:resGeneralHelp := IPFID_HELP_GENERAL 
      soHelp:resKeysHelp    := IPFID_HELP_KEYS 
      soHelp:create( SetAppWindow(), cHelpFile, cTitle ) 
   ENDIF 
RETURN soHelp 

An XbpHelp object is created and stored in a STATIC variable the first time this function is called. The XbpHelp object manages the online help window of an Xbase++ application and a reference to it can be retrieved by calling the function HelpObject() at any point in the program. This allows any number of XbpHelpLabel objects to be created that always activate the same XbpHelp object (or the same online help).

The function HelpObject() needs to receive the file name for the HLP file and the window title for the online help. Otherwise this function is generic. It also uses two #define constants which reference the two help windows available in each application. These constants can only be user-defined and must be used in the source code of the online help as numeric IDs in order to reference the specific help window.

The menu system of an application

The menu system of a GUI application plays a centrol role for program control. This menu must be created only once, generally within the function AppSys() prior to the first display of the application window. When the menu system is created within AppSys() or before the application window is displayed, the user does not see the construction of the menu system. The menu in the application window is already complete when the window is displayed for the first time.

The menu system consists of an XbpMenuBar object which manages the horizontal menu bar in the application window and several XbpMenu objects that are inserted in the menu bar as submenus. There are several ways to implement program control using menus. The simplest form is shown in the SDIMENU.PRG file (which presents an example SDI application). The most important steps are shown in the following code:

/* Call in AppSys() */ 

MenuCreate( oDlg:menuBar() ) 

******************************************************************** 
* Create menu system in the menu bar of the dialog 
******************************************************************** 
PROCEDURE MenuCreate( oMenuBar ) 
   LOCAL oMenu 

   // First sub-menu 
   // 
   oMenu := SubMenuNew( oMenuBar, "~File" ) 

   oMenu:addItem( { "Options", } ) 
   oMenu:addItem( MENUITEM_SEPARATOR ) 
   oMenu:addItem( { "~Exit" , NIL } ) 

   oMenu:itemSelected := ; 
      {|nItem,mp2,obj| MenuSelect(obj, 100+nItem) } 

   oMenuBar:addItem( {oMenu, NIL} ) 

   // Second sub-menu  -> customer data 
   // 
   oMenu := SubMenuNew( oMenuBar, "C~ustomer" ) 
   oMenu:setName( CUST_MENU ) 

   oMenu:addItem( { "~New"   , NIL } ) 
   oMenu:addItem( { "~Seek"  , NIL } ) 
   oMenu:addItem( { "~Change", NIL } ) 
   oMenu:addItem( { "~Delete",NIL , 0, ; 
                    XBPMENUBAR_MIA_DISABLED } ) 
   oMenu:addItem( { "~Print" ,NIL , 0, ; 
                    XBPMENUBAR_MIA_DISABLED } ) 

   oMenu:itemSelected := ; 
      {|nItem,mp2,obj| MenuSelect(obj, 200+nItem) } 

   oMenuBar:addItem( {oMenu, NIL} ) 

  /* And so forth... */ 

XbpMenu objects are created to contain the menu items. These submenus are created in the user-defined function SubMenuNew() (which is shown below), and menu items are attached to the submenus using the :addItem() method. A menu item is an array containing between two and four elements. In the simplest case the first element is the character string to be displayed as the menu item caption and the second element is NIL. Any character in the character string can be identified as a short-cut key by placing a tilde (~) in front of it. The second element is the code block to be executed when the menu item is selected by the user. In this example, instead of defining individual code blocks for each menu item a callback code block is defined for the entire submenu and the numeric position of the selected item and the menu object itself are passed to the selection routine MenuSelect(). In this routine a simple DO CASE...ENDCASE structure provides branching to the appropriate program module.

The second menu in the example is assigned a numeric ID (#define constant CUST_MENU) in the call to the method :setName(). This allows a specific XbpMenu object to be found later, since this value is found in the child list of the XbpMenuBar object which in turn is stored in the child list of the application window. The expression SetAppWindow():childFromName( CUST_MENU ) would provide a reference to this XbpMenu object. This can be used to make individual menu items temporarily unavailable (or available again) if this is desired in a specific program situation.

Inserting submenus into the main menu is done using the method :addItem() executed by the XbpMenuBar object. The title of a menu serves as text for the menu item. In the example program this text is set for a new submenu as follows:

******************************************************************** 
* Create sub-menu in a menu 
******************************************************************** 
FUNCTION SubMenuNew( oMenu, cTitle ) 
   LOCAL oSubMenu := XbpMenu():new( oMenu ) 
   oSubMenu:title := cTitle 
RETURN oSubMenu:create() 

In this function the main menu (or the immediately higher menu) is provided as the parent of the submenu. Assigning the title must occur prior to the call of the method :create() for correct positioning:

The default help menu

Each application should have a "Help" menu item that generally includes the same set of menu items. In the example program, this help menu is created by a separate procedure which creates the default menu items. Program control is implemented by code blocks that are passed to the menu in the method :addItem(). In this case the callback code block :itemSelected is not used.

******************************************************************** 
* Create standard help menu 
******************************************************************** 
PROCEDURE HelpMenu( oMenuBar ) 
   LOCAL oMenu := SubMenuNew( oMenuBar, "~Help" ) 
   oMenu:addItem( { "Help ~index", ; 
                    {|| HelpObject():showHelpIndex() } } ) 

   oMenu:addItem( { "~General help", ; 
                    {|| HelpObject():showGeneralHelp() } } ) 

   oMenu:addItem( { "~Using help", ; 
                    {|| HelpObject():showHelp(IPFID_HELP_HELP) } } ) 

   oMenu:addItem( { "~Keys help", ; 
                    {|| HelpObject():showKeysHelp() } } ) 

   oMenu:addItem( MENUITEM_SEPARATOR ) 

   oMenu:addItem( { "~Product information", ; 
                    {|| MsgBox("Xbase++ SDI Demo") } } ) 

   oMenuBar:addItem( {oMenu, NIL} ) 
RETURN 

The online help is managed by the XbpHelp object that is stored as a static variable in the user-defined function HelpObject(). This means it is always available when the function HelpObject() is called. Default help information can be called from the help menu by executing the XbpHelp object's methods provided for these purposes. A special method does not exist for the item "Using help". Here a #define constant is specified to the XbpHelp object that designates the numeric ID for the appropriate help window in the online help. The same ID must also be used in the IPF source code.

A dynamic menu for managing windows

In addition to the help menu that is available in both SDI and MDI applications, MDI applications have a second default menu that is used to bring different child windows of the MDI application to the front. The text in the title of each opened window appears as a menu item and selecting a menu item sets focus to the corresponding child window. This requires a dynamic approach to the menu, because the number of menu items corresponds to the number of open windows. A dynamic window menu is implemented for this purpose in the MDIMENU.PRG file (which is part of the source code for the MDIDEMO sample application). It is a good example of deriving new classes from an Xbase Part. To accomplish this, a way to easily determine the main menu of the application window (the parent window) is needed. The function AppMenu() is included in MDIDEMO.PRG for this purpose and returns the main menu of the application. There is also only one window menu per application so it can be stored in a STATIC variable. The function WinMenu() performs this task as shown in the following code:

******************************************************************** 
* Create menu to manage open windows 
******************************************************************** 
FUNCTION WinMenu() 
   STATIC soMenu 

   IF soMenu == NIL 
      soMenu := WindowMenu():new():create( AppMenu() ) 
   ENDIF 
RETURN soMenu 

The window menu is an instance of the class WindowMenu and receives the return value of AppMenu() as its parent. This means it is displayed as a submenu of the MDI application main menu. The user-defined class WindowMenu is derived from XbpMenu:

******************************************************************** 
* Menu class for management of open windows 
******************************************************************** 
CLASS WindowMenu FROM XbpMenu 
  EXPORTED: 
    CLASS VAR windowStack 
    CLASS METHOD initClass 
    METHOD init, addItem, delItem, setItem 
ENDCLASS 

******************************************************************** 
// Stack for open dialog windows as class variable 
// 
CLASS METHOD WindowMenu:initClass 
   ::windowStack := {} 
RETURN self 

The class variable :windowStack is declared to reference opened windows. The class method :initClass(), whose only task is to initialize the class variable with an empty array is also included. The four methods of the XbpMenu class are overloaded. The method :init() is executed immediately after the class method :new()terminates. The :init() method of the XbpMenu class must also be called in order to initialize the member variables implemented there:

******************************************************************** 
// Select a window via callback code block 
// 
METHOD WindowMenu:init( oParent, aPresParam, lVisible ) 
   ::xbpMenu:init( oParent, aPresParam, lVisible ) 
   ::title        := "~Window" 
   ::itemSelected := ; 
      {|nItem,mp2,obj| SetAppFocus( obj:windowStack[nItem] ) } 
RETURN self 

After the superclass is initialized, the menu title is assigned in :init(). A code block is assigned to the callback slot :itemSelected. This code block sets the focus to the window whose window title is selected from the menu. The numeric position of the selected menu item is passed to the code block as the parameter nItem and obj contains a reference to the menu object itself. Within this code block, the class variable :windowStack is accessed. :windowStack contains references to all the child windows of the MDI application. The selected window is passed to the function SetAppFocus() which sets it as the foreground window.

The last three methods of the window menu class allow menu items to be inserted, changed or deleted. These methods have the same names as methods of the XbpMenu class but the parameter passed to the methods are different. Instead of an array with between two and four elements, the passed parameter is an XbpDialog or XbpCrt object that is to receive focus if the menu item is selected.

******************************************************************** 
// Use title of the dialog window as text for menu item 
// 
METHOD WindowMenu:addItem( oDlg ) 
   LOCAL cItem := oDlg:getTitle() 

   AAdd( ::windowStack, oDlg ) 

   ::xbpMenu:addItem( {cItem, NIL} ) 
   IF ::numItems() == 1 
      ::setParent():insItem( ::setParent():numItems(), {self, NIL} ) 
   ENDIF 
RETURN self 

An opened window is passed to the :addItem() method. Within this method, the window is added to the class variable :windowStack and the window title is added as a menu item by passing it to the :addItem() method of the XbpMenu class. A special characteristic of the window menu is that it is only displayed in the main menu when at least one child window is open. Otherwise the "Window" menu item does not appear in the main menu. The window menu inserts itself as a menu item in its parent (the main menu) after the first time the method :addItem() is executed.

******************************************************************** 
// Transfer changed window title to menu item 
// 
METHOD WindowMenu:setItem( oDlg ) 
   LOCAL aItem, i := AScan( ::windowStack, oDlg ) 

   IF i == 0 
      ::addItem( oDlg ) 
   ELSE 
      aItem := ::xbpMenu:getItem(i) 
      aItem[1] := oDlg:getTitle() 
      ::xbpMenu:setItem( i, aItem ) 
   ENDIF 

RETURN self 

******************************************************************** 
// Delete dialog window from window stack and from menu 
// 
METHOD WindowMenu:delItem( oDlg ) 
   LOCAL i    := AScan( ::windowStack, oDlg ) 
   LOCAL nPos := ::setParent():numItems()-1  // window menu is always 
                                             // next to last 
   IF i > 0 
      ::xbpMenu:delItem( i ) 
      ADel( ::windowStack, i ) 
      Asize( ::windowStack, Len(::windowStack)-1) 
      IF ::numItems() == 0 
         ::setParent():delItem( nPos ) 
      ENDIF 
   ENDIF 
RETURN self 

The :setItem() method is used when the window title of an opened dialog window changes. This change must also be made in the menu item of the dynamic window menu. The:delItem() method is called when a dialog window is closed. This method removes the title of the dialog window from the window menu. If no child windows remain open, the window menu removes itself from the main menu (the parent) and the menu item "Window" is no longer visible.

Tasks of the Main procedure

After the application window including the menu system has been created in AppSys(), program execution continues in the Main procedure (assuming there is no other INIT PROCEDURE). At the start of the Main procedure all conditions required for an error free run of the GUI application should be checked. For example, this might include testing for the existence of all required files, creating index files that are not available and initialization of variables required throughout the application (PUBLIC variables). Retrieving configuration variables using the command RESTORE FROM should also generally occur within the Main procedure before the program goes into the event loop. The event loop performs the central task of the Main procedure. In this loop, events are retrieved and sent on to the addressee. The following program code is from the MDIDEMO.PRG file and shows some of what needs to be included in the Main procedure or in functions called by the Main procedure.

The example is not intended to cover all aspects that might be included in a Main procedure.

#include "Gra.ch" 
#include "Xbp.ch" 
#include "AppEvent.ch" 
#include "Mdidemo.ch" 

******************************************************************** 
* Main procedure and event loop 
******************************************************************** 
PROCEDURE Main 
   LOCAL nEvent, mp1, mp2, oXbp 
   FIELD CUSTNO, LASTNAME, FIRSTNAME, PARTNO, PARTNAME 

   // Check index files and create them if not existing 
   IF ! AllFilesExist( { "CUSTA.NTX", "CUSTB.NTX", ; 
                         "PARTA.NTX", "PARTB.NTX"  } ) 
      USE Customer EXCLUSIVE 
      INDEX ON CustNo                    TO CustA 
      INDEX ON Upper(LastName+Firstname) TO CustB 

      USE Parts EXCLUSIVE 
      INDEX ON Upper(PartNo)    TO PartA 
      INDEX ON Upper(PartName)  TO PartB 

      CLOSE DATABASE 
   ENDIF 

   SET DELETED ON 

   // Infinite loop. The program is terminated in AppQuit() 
   DO WHILE .T. 
      nEvent := AppEvent( @mp1, @mp2, @oXbp ) 
      oXbp:handleEvent( nEvent, mp1, mp2 ) 
   ENDDO 
RETURN 

******************************************************************** 
* Check if all files of the array 'aFiles' exist 
******************************************************************** 
FUNCTION AllFilesExist( aFiles ) 
   LOCAL lExist := .T., i:=0, imax := Len(aFiles) 

   DO WHILE ++i <= imax .AND. lExist 
      lExist := File( aFiles[i] ) 
   ENDDO 
RETURN lExist 

In this example, the Main procedure simply tests whether all the index files exist and recreates the index files if any are not found. The existence of the files is tested in the function AllFilesExist(). When this is complete, the Main procedure enters an infinite loop that reads events from the queue using AppEvent() and sends them on to the addressee by calling the addressee's method :handleEvent().

Looking at this implementation, the inevitable question is: Where and how is the program terminated? The infinite loop in the Main procedure cannot be terminated based on its condition DO WHILE .T.. A separate routine is used to terminate the program. The code for this routine is shown below:

******************************************************************** 
* Routine to terminate the program 
******************************************************************** 
PROCEDURE AppQuit() 
   LOCAL nButton 

   nButton := ConfirmBox( , ; 
                 "Do you really want to quit ?", ; 
                 "Quit", ; 
                  XBPMB_YESNO , ; 
                  XBPMB_QUESTION+XBPMB_APPMODAL+XBPMB_MOVEABLE ) 

   IF nButton == XBPMB_RET_YES 
      COMMIT 
      CLOSE ALL 
      QUIT 
   ENDIF 

RETURN 

In the termination routine AppQuit(), confirmation that the program should actually be terminated is received from the user via the ConfirmBox() function. If the application is to be terminated, all data buffers are written back into the files and all database are closed using CLOSE ALL. The command QUIT then terminates the program. If the user does not confirm that the program should be terminated, the infinite loop in the Main procedure is continued.

It is generally recommended that the source code for a GUI application be broken down into three sections: program start, program execution and program end. The program start is contained in AppSys() and the program code executed within the Main procedure prior to the event loop. The event loop itself is the program execution. Often within this loop the program code that was generated in MenuCreate() during program start up is called by the menu system. Program termination occurs in the user defined procedure AppQuit(), where verification by the user can be requested and any data can be saved.

There are only two places in a program where the procedure AppQuit() is called. AppQuit() is generally called from a menu item and from a callback code block or from a callback method. The next two lines illustrate this:

oMenu:addItem( {"~Quit", {|| AppQuit() } } ) 
oDialog:close := {|| AppQuit() } 

In the first line, AppQuit() is executed after a menu item is selected so there must obviously be a menu containing a menu item to terminate the application. The second line defines a callback code block for the dialog window to execute after the system menu icon of the dialog window is double clicked or the "Close" menu item is selected in the system menu of the window. Generally, the routine for terminating a GUI application should be available in the menu of the application as well as in response to the xbeP_Close event.

A DataDialog class for integrating databases

An important aspect in programming GUI applications is the connection between the elements of the dialog window and the DatabaseEngine. The link between a single dialog element and a single database field is created via the data code block contained in the instance variable :dataLink of the DataRef class that manages data. This mechanism is described in the section "DataRef() - The connection between XBP and DBE". A window generally contains several dialog elements that are linked to different database fields. Special situations can result that must be considered when programming GUI applications. The programmer must also remember that such an application is completely event driven. As soon as there is a menu system in a window, an exactly defined order of program execution is no longer assured since the user has control of the application rather than the programmer.

The two example applications SDIDEMO and MDIDEMO are provided as examples for GUI applications under Xbase++. The difficulties that arise in accessing databases are taken into account in different ways in these two programs. In SDIDEMO, a procedural approach is implemented and an object-oriented style is used in MDIDEMO. Both of these program examples solve the problem of non-modality of entry fields resulting from the event driven nature of a GUI application. The problems of non-modality are described by the questions: "When and how is data input validated?" and "When is data written to the database?". Since data entry fields can be activated with a mouse click, prevalidation (validation before data is entered) is not possible (after a mouse click an entry field has the input focus). This condition requires some consideration by programmers who have previously developed only under DOS without a mouse. Validating data in a GUI application can occur in the framework of postvalidation (validation after data is entered). The :validate() method in the DataRef class serves this purpose. If postvalidation fails, the method :undo() of the entry field (Xbase Part) should be called. In an event driven application, this is the only way to assure that no invalid data is written into the database.

However, the major task in programming GUI applications is generally not validating the data, but transferring the input data to the database. In the SDIDEMO and MDIDEMO example programs, the philosophy is used that the data needs to be written to the database when the record pointer is changed. All Xbase Parts have their own edit buffer to hold the modified data and the value to write into the database fields is stored in this edit buffer of each Xbase Part. For all of the database fields that can be changed within a dialog window, an Xbase Part must exist to store the value in its edit buffer. The following code fragment illustrates this:

oXbp := XbpSLE():new( oDlg:drawingArea,, {95,135}, {180,22} ) 
oXbp:bufferLength := 20 
oXbp:dataLink := {|x| IIf( x==NIL, LASTNAME, LASTNAME := x ) } 
oXbp:create():setData() 

In this code, an entry field is created for editing the data in the database field LASTNAME. Calling the method :setdata() in connection with :create() copies the data from the database field into the edit buffer of the XbpSLE object. Within a dialog window any number of entry fields can exist to access database fields. The edit buffer of all entry fields in the dialog window can be changed at any time (a mouse click in an entry field is sufficient to begin editing). For this reason, it must be determined when changes to the data in an entry field will be copied back into the file. There are two approaches: changes to individual data entry fields are written into the file as soon as the change occurs or all changes from all data entry fields in a window are written into the file as soon as a "Save" routine is explicitly called or the record pointer is repositioned.

The second approach is preferred in GUI applications that are designed for simultaneous access on a network. This approach allows several data entry fields to be changed in a dialog window without each change being individually copied to the database. In concurrent or network operation saving each change to the database would require a time consuming lock and release of the current record. A performance optimized GUI application only locks a record when it can write several fields to the database or when the record pointer changes.

The problems of validating and saving data into databases is present in every application. The following code shows several aspects of this problem and is based on the example application MDIDEMO. In this example application the DataDialog class is used to provide dialog windows for accessing the DatabaseEngine. A DataDialog object coordinates a DatabaseEngine with a dialog window. The source code for this class is contained in the file DATADLG.PRG. An example of an input screen based on DataDialog, is shown in the following illustration:

Input screen for customer data

The DataDialog class is derived from XbpDialog. It adds seven new instance variables and eleven additional methods for transferring data from a database to the dialog and vice versa. Three of the instance variables are for internal use only and are declared as PROTECTED:. The four methods :init(), :create(), :configure() and :destroy() perform steps in the "life cycle" of a DataDialog object:

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

******************************************************************** 
* Class declaration 
******************************************************************** 
CLASS DataDialog FROM XbpDialog 
   PROTECTED: 
     VAR appendMode           // Is it a new record? 
     VAR editControls         // List of XBPs for editing data 
     VAR appendControls       // List of XBPs enabled only 
                              // during APPEND 

   EXPORTED: 
     VAR area      READONLY   // current work area 
     VAR newTitle             // code block to change window title 
     VAR contextMenu          // context menu for data dialog 
     VAR windowMenu           // dynamic window menu in 
                              // application window 

     METHOD init              // overloaded methods 
     METHOD create 
     METHOD configure 
     METHOD destroy 
     METHOD addEditControl    // register XBP for edit 
     METHOD addAppendControl  // register XBP for append 
     METHOD notify            // process DBO message 
     METHOD readData          // read data from DBF 
     METHOD validateAll       // validate all data stored in XBPs 
     METHOD writeData         // write data from XBPs to DBF 
     METHOD isIndexUnique     // check index value for uniqueness 
ENDCLASS 

The protected instance variable :appendMode contains the logical value .T. (true) only when the phantom data record (record number LastRec()+1) is current. The other two protected instance variables:editControls and :appendControls are arrays containing lists of Xbase Parts that can modify data. In order to create a data dialog, editable XBPs are required as well as Xbase Parts that cannot edit data but display static text or boxes (XbpStatic objects). The instance variable :editControls contains a list of references to those XBPs in the child list (all XBPs that are displayed in the dialog window are contained in this list) that can be edited.

The task of the :appendControls instance variable is similar and contains a list of XBPs that are only enabled when a new record is appended. In all other cases, these XBPs are disabled. They only display data and do not allow the data in them to be edited. This is useful for editing database fields that are contained in the primary database key which should not be changed once they are entered in the database. :editControls and :appendControls are both initialized with empty arrays. This is done in the :init() method after it calls the :init() method of the XbpDialog class as shown below:

******************************************************************** 
* Initialize data dialog 
******************************************************************** 
METHOD DataDialog:init( oParent, oOwner , ; 
                        aPos   , aSize  , ; 
                        aPParam, lVisible ) 

   DEFAULT lVisible TO .F. 

   ::xbpDialog:init( oParent, oOwner, ; 
                     aPos   , aSize , ; 
                     aPParam, lVisible ) 

   ::area            := 0 
   ::border          := XBPDLG_THINBORDER 
   ::maxButton       := .F. 
   ::editControls    := {} 
   ::appendControls  := {} 
   ::appendMode      := .F. 
   ::newTitle        := {|obj| obj:getTitle() } 

RETURN self 

All instance variables are set to values with the valid data type in the :init() method. Only the instance variables :border and :maxButton change the default values assigned in the XbpDialog class. The window of a DataDialog object is fixed in size and cannot be enlarged. The method has the same parameter list as the method :new() and :init() in the XbpDialog class. This allows it to receive parameters and simply pass them on to the superclass. The DataDialog is different in that it is created as hidden by default. This is recommended when many XBPs will be displayed in the window after the window is generated. The construction of the screen with the method :show() is faster if everything can be displayed at once after the XBPs have been added to the dialog window.

The instance variable :newTitle must contain a code block that the DataDialog object is passed to. For this reason a code block is defined in the :init() method, but it must be redefined later. This code block changes the window title while the dialog window is visible. The default code block is assigned to the instance variable in the :init() method to ensure that the instance variable has the correct data type.

The next method in the "life cycle" of a DataDialog object is :create(). A database must be open in the current work area prior to this method being called. A DataDialog object continues to use the work area that is current when the :create() method is executed:

******************************************************************** 
* Load system resources 
* Register DataDialog in current work area 
******************************************************************** 
METHOD DataDialog:create( oParent, oOwner , ; 
                          aPos   , aSize  , ; 
                          aPParam, lVisible ) 

   ::xbpDialog:create( oParent, oOwner , ; 
                       aPos   , aSize  , ; 
                       aPParam, lVisible ) 

   ::drawingArea:setColorBG( GRA_CLR_PALEGRAY ) 

   ::appendMode      := Eof() 
   ::area            := Select() 

   ::close           := {|mp1,mp2,obj| obj:destroy() } 
   ::setDisplayFocus := {|mp1,mp2,obj| ; 
                          DbSelectArea( obj:area ) } 

   DbRegisterClient( self ) 

RETURN self 

The most important task of :create() is requesting system resources for the dialog window. This occurs when the method of the same name in the superclass is called and the parameters are simply passed on to it. The background color for the drawing area (:drawingArea) of the dialog window is then set. The call to :setColorBG() also defines the background color for all XBPs later displayed in the dialog window. This affects all XBPs that have a caption for displaying text. This simplifies programming because the background color of the individual XBPs with captions do not have to be set separately. Generally when the system colors defined in the system configuration are to be used :setColorBG() cannot be called.

The lines that follow are important because they link the DataDialog object and the work area. First, whether the pointer is currently at Eof() is determined, then Select() determines the number of the current work area. Two code blocks are assigned to the callback slots :close and :setDisplayFocus. The method :destroy()(described below) is called after the xbeP_Close event. As soon as the DataDialog object receives focus, the code block in :setDisplayFocus is executed. In this code block, the work area managed by the DataDialog object is selected as the current work area using DbSelectArea(). This means that if the mouse is clicked in a DataDialog window, the correct work area is automatically selected.

The call to DbRegisterClient() is critical for the program logic. This registers the DataDialog object in the work area so that it is automatically notified whenever anything in the work area changes. This includes notification of changes in the position of the record pointer. When the record pointer changes, the new data must be displayed by the XBPs that are listed in the instance variable :editControls. This is done using the method :notify() which is described later after the remaining methods in the DataDialog "life cycle" are discussed. The method :configure() is provided to handle changes in the work area managed by the DataDialog object and is shown below:

******************************************************************** 
* Configure system resources 
* Register data dialog in new work area if necessary 
******************************************************************** 
METHOD DataDialog:configure( oParent, oOwner , ; 
                             aPos   , aSize  , ; 
                             aPParam, lVisible ) 
   LOCAL lRegister := (::area <> Select()) 

   ::xbpDialog:configure( oParent, oOwner , ; 
                          aPos   , aSize  , ; 
                          aPParam, lVisible ) 
   IF lRegister 
     (::area)->( DbDeRegisterClient( self ) ) 
   ENDIF 

   ::area       := Select() 
   ::appendMode := Eof() 

   IF lRegister 
      DbRegisterClient( self ) 
   ENDIF 

RETURN self 

A DataDialog object always manipulates the current work area. Because of this, the method :configure() compares the instance variable :area to Select() to determine whether the current area has changed. If it has changed, the object is deregistered in the old work area and registered in the new area. In addition, the system resources for the dialog window are also reconfigured in the call to the :configure() method of the superclass.

The final method of the DataDialog life cycle is :destroy(). This method closes the database used by the DataDialog object and releases the system resources. The instance variables declared in the DataDialog class are reset to the values assigned in the method :init():

******************************************************************** 
* Release system resources and unregister data dialog from work area 
******************************************************************** 
METHOD DataDialog:destroy() 

   ::writeData() 
   ::hide() 

   (::area)->( DbCloseArea() ) 

   IF ! Empty( ::windowMenu ) 
      ::windowMenu:delItem( self ) // delete menu item in window menu 
      ::windowMenu := NIL 
   ENDIF 

   IF ! Empty( ::contextMenu ) 
      ::contextMenu:cargo := NIL   // Delete reference of data 
      ::contextMenu := NIL         // dialog and context menu 
   ENDIF 

   ::xbpDialog:destroy()           // release system resources 
   ::Area           := 0           // and set instance variables 
   ::appendMode     := .F.         // to values corresponding to 
   ::editControls   := {}          // :init() state 
   ::appendControls := {} 
   ::newTitle       := {|obj| obj:getTitle() } 

RETURN self 

The method :writeData() is called in :destroy() in order to write all the data changes into the database before it is closed using DbCloseArea(). After the database is closed, the DataDialog object is implicitly deregistered from the work area and a call to DbDeRegisterClient() is not necessary. If a menu object is contained in the instance variable:windowMenu, the DataDialog object is removed from the list of menu items in this menu (the WindowMenu class is described in a previous section). The instance variable:contextMenu can contain a context menu that is activated by clicking the right mouse button. This mechanism is described in a later section. It is essential that the reference to the DataDialog object in the instance variable :cargo of the context menu be deleted because the method :destroy() is expected to eliminate all references to the DataDialog object. If a DataDialog object remains referenced anywhere, whether in a variable, an array, or an instance variable, it will not be removed from memory by the garbage collector. This concludes the discussion of the methods that perform tasks in the "life cycle" of a DataDialog object.

One of the most important method of the DataDialog class is the :notify() method. This method is called whenever something is changed in the work area associated with the object. An abbreviated version of this method highlighting its essential elements is shown below:

******************************************************************** 
* Notify method: 
*   - Write data to fields prior to moving the record pointer 
*   - Read data from fields after moving the record pointer 
******************************************************************** 
METHOD DataDialog:notify( nEvent, mp1, mp2 ) 

   IF nEvent <> xbeDBO_Notify           // no notify message 
      RETURN self                       // ** return ** 
   ENDIF 

   DO CASE 
   CASE mp1 == DBO_MOVE_PROLOG          // record pointer is about 
      ::writeData()                     // to be moved 

   CASE mp1 == DBO_MOVE_DONE .OR. ;     // skip is done 
        mp1 == DBO_GOBOTTOM  .OR. ; 
        mp1 == DBO_GOTOP 
      ::readData() 

   ENDCASE 
RETURN self 

Calling the function DbRegisterClient() in the :create() method of the DataDialog object registers the object in the work area it uses. As soon as anything changes in this work area, the :notify() method is called. For record pointer movement, this method is called twice. The first time the DataDialog object receives the value represented by the constant DBO_MOVE_PROLOG (defined in the DMLB.CH file) as the mp1parameter. This is a signal that means "Warning the record pointer position is about to change." When it receives this message, the DataDialog object executes the method :writeData() which writes the data of the current record into the database. In the second call to :notify(), the object receives the value of the constant DBO_MOVE_DONE. This message tells the object "Ok, the pointer has been changed." In response to this message, the object executes the :readData() method which copies the fields of the new record into the edit buffers of the XBPs that are in the data dialog's :editControls instance variable. This allows the data in the new record to be edited.

The :notify() method provides important program logic for the DataDialog object. In this method, the DataDialog object reacts to messages sent by the work area it uses. This method is only called after the object is registered in the work area using DbRegisterClient(). Or more precisely, it is only called when the object is registered in the database object (DBO) that manages the work area (a DBO is automatically created when a database is opened). Based on the event passed, the :notify() event determines whether a record should be read into XBPs or whether the data in the XBPs should be written into the database. The DataDialog object does not directly manage the data but does manage the XBPs contained in the array :editControls. Adding XBPs to this array is done using the method :addEditControl().

******************************************************************** 
* Add an edit control to internal list 
******************************************************************** 
METHOD DataDialog:addEditControl( oXbp ) 
   IF AScan( ::editControls, oXbp ) == 0 
      AAdd(  ::editControls, oXbp ) 
   ENDIF 
RETURN self 

******************************************************************** 
* Add an append control to internal list 
******************************************************************** 
METHOD DataDialog:addAppendControl( oXbp ) 
   IF AScan( ::appendControls, oXbp ) == 0 
      AAdd(  ::appendControls, oXbp ) 
   ENDIF 
RETURN self 

The two methods :addEditControl() and :addAppendControl() are almost identical. One adds an Xbase Part to an array stored in the instance variable :editControls and the other adds an Xbase Part to :appendControls. When a DataDialog object executes the method :readData() or :writeData(), it sequentially processes the elements in the :editControls array and sends each element (each Xbase Part) the message to read or write its data. A code fragment is included below to illustrate how Xbase Parts can be added to the window of a DataDialog object and to the :editControls instance variable if appropriate. The variable oDlg references a DataDialog object.

oXbp := XbpStatic():new( oDlg:drawingArea,, {5,135}, {80,22} ) 
oXbp:caption := "Lastname:"          // static text is stored 
oXbp:options := XBPSTATIC_TEXT_RIGHT // only in the child list 
oXbp:create( ) 

oXbp := XbpSLE():new( oDlg:drawingArea,, {95,135}, {180,22} ) 
oXbp:bufferLength := 20              // entry field linked to 
oXbp:tabStop  := .T.                 // database 
oXbp:dataLink := {|x| IIf( x==NIL, LASTNAME, LASTNAME := x ) } 
oXbp:create():setData() 

oDlg:addEditControl( oXbp )          // adds new XBP to :editControls 

The Xbase Parts appear in the drawing area of a dialog window, so oDlg:drawingArea must be specified as the parent. The code fragment creates an XbpStatic object to display the text "Lastname:" and an XbpSLE object to access and edit the database field called LASTNAME. Passing the XbpSLE object to the method :addEditControl() adds this Xbase Part to the :editControlsarray. In the child list of the DataDialog object there are now two XBPs but the :editControls array contains only the XBP for data that can be edited. The methods :readData(), :validateAll()and :writeData() assume that all the Xbase Parts that can edit data are included in the :editControls array. The program code for :readData() is shown below:

******************************************************************** 
* Read current record and transfer data to edit controls 
******************************************************************** 
METHOD DataDialog:readData() 
   LOCAL i, imax  := Len( ::editControls ) 

   FOR i:=1 TO imax                       // Transfer data from file 
      ::editControls[i]:setData()         // to XBPs 
   NEXT 

   Eval( ::newTitle, self )               // Set new window title 

   IF Eof()                               // enable/disable XBPs 
      IF ! ::appendMode                   // active only during 
         imax  := Len( ::appendControls ) // APPEND 
         FOR i:=1 TO imax                 // 
            ::appendControls[i]:enable()  // Hit Eof(), so 
         NEXT                             // enable XBPs 
      ENDIF 
      ::appendMode := .T. 
   ELSEIF ::appendMode                    // Record pointer was 
      imax  := Len( ::appendControls )    // moved from Eof() to 
      FOR i:=1 TO imax                    // an existing record. 
         ::appendControls[i]:disable()    // Disable append-only 
      NEXT                                // XBPs 
      ::appendMode := .F. 
   ENDIF 

RETURN 

The :setData() method in the first FOR...NEXT loop causes all the XBPs referenced in the instance variable :editControls to re-read their edit buffers by copying the return value of the data code block contained in :dataLink into their edit buffer. The remaining code just enables and disables the XBPs in the :appendControls list. In addition to reading the data in the database fields, this method is the appropriate place to enable or disable those Xbase Parts that should only be edited when a new record is being appended.

The counterpart of :readData() is the :writeData() method. In this method, the data in the edit buffer of each Xbase Part listed in :editControls is written back to the database. This method involves relatively extensive program code, because it performs record locking and identifies whether a new record should be appended.

******************************************************************** 
* Write data from edit controls to file 
******************************************************************** 
METHOD DataDialog:writeData() 
   LOCAL i, imax 
   LOCAL lLocked   := .F. , ;       // Is record locked? 
         lAppend   := .F. , ;       // Is record new? 
         aChanged  := {}  , ;       // XBPs containing changed data 
         nOldArea  := Select()      // Current work area 

   dbSelectArea( ::area ) 

   IF Eof()                         // Append a new record 
      IF ::validateAll()            // Validate data first 
         APPEND BLANK 
         lAppend  := .T. 
         aChanged := ::editControls // Test all possible changes 
         lLocked  := ! NetErr()     // Implicit lock 
      ELSE 
         MsgBox("Invalid data")     // Do not write invalid data 
         DbSelectArea( nOldArea )   // to new record 
         RETURN .F.                 // *** RETURN *** 
      ENDIF 
   ELSE 
      imax := Len( ::editControls ) // Find all XBPs containing 
      FOR i:=1 TO imax              // changed data 
         IF ::editControls[i]:changed 
            AAdd( aChanged, ::editControls[i] ) 
         ENDIF 
      NEXT 

      IF Empty( aChanged )          // Nothing has changed, so 
         DbSelectArea( nOldArea )   // no record lock necessary 
         RETURN .T.                 // *** RETURN *** 
      ENDIF 

      lLocked := DbRLock( Recno() ) // Lock current record 
   ENDIF 

   IF ! lLocked 
      MsgBox( "Record is currently locked" ) 
      DbSelectArea( nOldArea )      // Record lock failed 
      RETURN .F.                    // *** RETURN *** 
   ENDIF 

   imax := Len( aChanged )          // Write access is necessary 
   FOR i:=1 TO imax                 // only for changed data 
      IF ! lAppend 
         IF ! aChanged[i]:validate() 
            aChanged[i]:undo()      // invalid data ! 
            LOOP                    // undo changes and validate 
         ENDIF                      // next XBP 
      ENDIF 
      aChanged[i]:getData()         // Get data from XBP and 
   NEXT                             // write to file 

   DbCommit()                       // Commit file buffers 
   DbRUnlock( Recno() )             // Release record lock 

   IF ::appendMode                  // Disable append-only XBPs 
      imax  := Len( ::appendControls ) // after APPEND 
      FOR i:=1 TO imax 
         ::appendControls[i]:disable() 
      NEXT 
      ::appendMode := .F. 

      IF ! Empty( ::contextMenu ) 
         ::contextMenu:disableBottom() 
         ::contextMenu:enableEof() 
      ENDIF 
   ENDIF 

   DbSelectArea( nOldArea ) 

RETURN .T. 

Appending a new record requires special logic in the :writeData()method of the DataDialog object. A special empty record (the phantom record) is automatically available when the pointer is positioned at Eof(). If the pointer is positioned at Eof(), the method :readData() has copied "empty" values from the database fields into the XBP edit buffers for all of the XBPs listed in the instance variable :editControls. Because of this, all XBPs contain valid data types. But there is no guarantee that valid data is also contained in the edit buffers of each XBP. This means that data validation must be performed before the record is even appended. Since there is not yet a record, all the data to be saved is found only in the edit buffer of the corresponding Xbase Parts. The method :validateAll() is called before a new record is appended which is to receive data from the edit buffers of the Xbase Parts.

Data validation is especially important when a new record is appended because no previously valid data exists to allow the changes to the individual edit buffers to be voided. For records that are being edited, the method :undo() allows changes to the values in the edit buffers to be voided. But this approach assumes there is an original field value that is valid. This is only true if the record being edited existed prior to editing. When a record is appended, the original values are "empty" values which are probably not valid.

In :writeData(), this situation is handled by calling the method :validateAll() before the new record is appended to the file using APPEND BLANK. If data validation fails on even one field, a message box containing the text "Invalid data" is displayed and a new record is not appended. The invalid data remains in the edit buffers of the corresponding Xbase Parts (:editControls) and can be corrected by the user. When an existing record is edited, data validation occurs individually for each Xbase Part. If the :validate() method of an XBP returns the value .F. (false) (indicating invalid data), the :undo() method of the XBP is executed which copies the original, valid data back into the edit buffer.

If any XBPs listed in :editControls have been changed, the record is locked using DbRLock(). After validation, data from the edit buffers is written into the database by calling the method:getData(). The function DbCommit() ensures that data in the file buffers are written into the database. Finally the record lock is released.

The :writeData() method handles the problems of data validation and appending records as they occur in an event driven environment. This process is controlled by the mouse or rather by the user who causes the mouse clicks. Even though :writeData() is called from only one place in the :notify() method, it is impossible to foresee when this method will be called. While it is clear that it is called when the record pointer moves it is not possible to predict which record will be current when the method :writeData() is called. The special case occurs when the pointer is located on the phantom record. In this case data validation cannot be reversed using the :undo() method because no previously validated data exists. For this reason, all data must be validated before a new record can be appended. The method which checks that all data is valid is called :validateAll() and is shown below:

******************************************************************** 
* Validate data of all edit controls 
* This is necessary prior to appending a new record to the database 
******************************************************************** 
METHOD DataDialog:validateAll() 
   LOCAL i := 0, imax := Len( ::editControls ) 
   LOCAL lValid := .T. 

   DO WHILE ++i <= imax .AND. lValid 
      lValid := ::editControls[i]:validate() 
   ENDDO 

RETURN lValid 

The method consists only of a DO WHILE loop that is terminated as soon as the XBP :validate method signals invalid data. The method :validate() is executed for all XBPs listed in :editControls. This method always returns the value .T. (true) unless there is a code block contained in the instance variable :validate. If a code block is contained in this instance variable, it is executed and receives the XBP as the first parameter. This code block performs data validation and returns .T. (true) if the data is valid.

Special data validation is needed for the primary key in a database. The primary key is the value in the database that uniquely identifies each record. There is always an index for the primary key. The method :isIndexUnique() (shown below) tests whether a value already exists as a primary key in an index file of the database. This method demonstrates an extremely important aspect for the use of DataDialog objects (more precisely: for the use of the function DbRegisterClient()):

******************************************************************** 
* Check whether an index value does *not* exist in an index 
******************************************************************** 
METHOD DataDialog:isIndexUnique( nOrder, xIndexValue ) 
   LOCAL nOldOrder := OrdNumber() 
   LOCAL nRecno    := Recno() 
   LOCAL lUnique   := .F. 

   DbDeRegisterClient( self )       // Suppress notification from DBO 
                                    // to self during DbSeek() !!! 
   OrdSetFocus( nOrder ) 

   lUnique := .NOT. DbSeek( xIndexValue ) 

   OrdSetFocus( nOldOrder ) 
   DbGoTo( nRecno ) 

   DbRegisterClient( self ) 

RETURN lUnique 

The functionality of the :isIndexUnique() method is very limited. All it does is search for a value in the specified index and return the value .T. (true) if the value is not found. An important point shown here is that the DataDialog object executing the method must be deregistered in the work area. It was initially registered in the work area by the method :create(), causing the method :notify() to be called every time the record pointer changes. In this case, it is a method of the DataDialog object changing the pointer by calling DbSeek(). If the DataDialog object were not deregistered, an implicit recursion would result since each change to the pointer via DbSeek() calls the method :notify(). For this reason, DbDeRegisterClient() is used to deregister the DataDialog object prior to the call to DbSeek(). It is again registered in the work area using DbRegisterClient() after DBSeek().

In summary, the DataDialog class solves many problems which must be considered when programming GUI applications that work with databases. Record pointer movements are easily identified in the method :notify() that is automatically called when the DataDialog object is registered in the current work area using the function DbRegisterClient(). Before the record pointer is moved, a DataDialog object copies the changed data in :editControls back into the database. After the record pointer is changed, a DataDialog object displays the current data. Data validation occurs prior to data being written into the database either by a new record being appended or existing data being overwritten. Whether new data is being saved or existing data modified is determined by the DataDialog object.

DataDialog and data entry screens

Objects of the DataDialog class described in the previous section are appropriate for programming data entry screens in GUI applications. Each input screen is an independent window that is displayed as a child of the application window. In each child window (input screen) Xbase Parts are added to edit the database fields. Because they are separate windows, it is recommended that each entry screen be programmed in a separate routine. The tasks of this routine include opening all databases required for the entry screen, creating the child window (DataDialog), and adding the Xbase Parts needed for editing the database fields to the entry screen. In the example application MDIDEMO, two entry screens are programmed, one for customer data and one for parts data. The process of creating the data entry screen is the same in both cases. Sections of the program code from the file MDICUST.PRG are discussed below to illustrate various aspects significant when programming data entry screens:

******************************************************************** 
* Customer Dialog 
******************************************************************** 
PROCEDURE Customer( nRecno ) 
   LOCAL oXbp, oStatic, drawingArea, oDlg 
   FIELD CUSTNO, MR_MRS, LASTNAME, FIRSTNAME, STREET, CITY, ZIP , ; 
         PHONE , FAX   , NOTES   , BLOCKED  , TOTALSALES 

   IF ! OpenCustomer( nRecno )       // open customer database 
      RETURN 
   ENDIF 

   oDlg := DataDialog():new( RootWindow():drawingArea ,, ; 
                             {100,100}, {605,315},, .F.  ) 
   oDlg:title := "Customer No: "+ LTrim( CUSTNO ) 
   oDlg:icon  := ICON_CUSTOMER 
   oDlg:create() 
   /* ... */ 

The Customer() procedure creates a new child window in the MDI application where customer data can be edited. LOCAL variables are first declared to reference the Xbase Parts created and all of the database fields are identified to the compiler as field variables. Before the child window (DataDialog) is created, the required database(s) must be open. This occurs in the function OpenCustomer() which returns the value .F. (false) only if the customer database could not be opened. Opening the database might fail because another workstation has the file exclusively open or the file is simply not found.

Testing for the existence of files should have already been done at startup in the Main procedure.

When the required file(s) can be opened, the dialog window is created. This is done using DataDialog class method :new() which generates a new instance of the DataDialog class. The parent of the new object is the drawing area (:drawingArea) of the application window created in AppSys() and returned by the user-defined function RootWindow(). As soon as the child window is created, the Resource ID for an icon must be entered into the instance variable :icon. This icon is displayed within the application window when the child window is minimized. In this example, the #define constant ICON_CUSTOMER is used. An icon is declared in a resource file and must be linked to the executable file using the resource compiler. If no icon ID is specified for a child window, the window contents in the range from point {0,0} to point {32,32} are used as the icon when the window is minimized. This means everything visible in the lower left corner of the child window up to the point {32,32} appears as the symbol for the minimized child window.

In the example program MDICUST.PRG, only the CUSTOMER.DBF database file needs to be opened. It is important for the customer database to be reopened each time the procedure Customer() is called. This is shown in the program code of the function OpenCustomer():

******************************************************************** 
* Open customer database 
******************************************************************** 
FUNCTION OpenCustomer( nRecno ) 
   LOCAL nOldArea := Select(), lDone := .F. 

   USE Customer NEW 
   IF ! NetErr() 
      SET INDEX TO CustA, CustB 
      IF nRecno <> NIL 
         DbGoto( nRecno ) 
      ENDIF 
      lDone := .T. 
   ELSE 
      DbSelectArea( nOldArea ) 
      MsgBox( "Database cannot be opened" ) 
   ENDIF 

RETURN lDone 

Each instance of the DataDialog class (each data entry screen) manages its own work area. If the Customer() procedure is executed 10 times, 10 data entry screens are created for customer data and the CUSTOMER.DBF file is opened 10 times. This rule is standard for event oriented GUI applications: Each dialog opens its own database. This requires some consideration for programmers coming from DOS procedural programming, since the same approach is not appropriate under DOS because of the 255 file handle limit. This limit does not exist under a 32bit operating system. Access to a single database file from several dialogs does require that protection mechanisms be implemented in the Xbase++ application. The mechanisms for locking records or files is sufficient under Xbase++ so that when the program allows simultaneous access on a network, it will also handle the file being opened multiple times within a single application.

Each call to OpenCustomer() opens the customer database in a new work area and the method DataDialog:create() registers the dialog window in this new work area. From this point on, the DataDialog object is notified about each change in the work area (via the method :notify()) and can be assigned the appropriate XBPs (via :editControls) so that they can automatically be handled by the DataDialog methods. (Note for Clipper programmers: the expression USE Customer NEW is allowed in Xbase++ without specifying an alias name. If a database is opened multiple times, Xbase++ provides a unique alias name formed from the file name and the number of the current work area).

When the database is open and the DataDialog (the child window) is created, the most important processes for programming a data entry screen are nearly complete. Xbase Parts must still be added to the dialog window. These include both XBPs that contribute to the visual organization of the data entry screen (borders and text) and XBPs that allow access to database fields via :dataLink. This second group is primarily made up of objects from the classes XbpSLE, XbpCheckBox and XbpMLE. XbpSLE objects provide single line data entry fields, XbpCheckBox objects manage logical values and XbpMLE objects provide multiple line data entry fields that allow memo fields to be edited. Objects of the classes XbpSLE, XbpCheckBox and XbpMLE are sufficient to program the sections of data entry screens where database fields are edited.

Boxes that are displayed by XbpStatic objects are used to provide visually organization of data entry screens. Entry fields are not only visual separated when they appear in a box, but the fields can also be grouped in the program logic. The following program section shows another example of code defining a part of a data entry screen. This example is a continuation of the Customer() procedure:

// Get drawing area from dialog 
drawingArea := oDlg:drawingArea 

oStatic := XbpStatic():new( drawingArea ,, {12,227}, {579,58} ) 
oStatic:type := XBPSTATIC_TYPE_GROUPBOX 
oStatic:create() 

In the above sample, the drawing area (:drawingArea) of the dialog window is retrieved and passed as the parent for the dialog elements to be displayed in the window. The first dialog element is an XbpStatic object responsible for displaying a group box. This box displays text (the caption) in the upper left corner. A group box is used for grouping data entry fields and acts as the parent for all the Xbase Parts which are displayed within the group box. In other words: the parent for a group box is the :drawingArea and the parent for the Xbase-Parts displayed within the group box is the XbpStatic object representing the box. For this reason the XbpStatic object is referenced in the variable oStatic and is used as the parent in the example of creating data entry fields shown below:

oXbp := XbpSLE():new( oStatic,, {95,135}, {180,22} ) 
oXbp:bufferLength := 20 
oXbp:dataLink := {|x| IIf( x==NIL, Trim(LASTNAME), LASTNAME := x ) } 
oXbp:create():setData() 

oDlg:addEditControl( oXbp )   // register Xbp as EditControl 

In the above sample, a data entry field is created for display within a group box (the parent of the data entry field is oStatic). The XbpSLE object accesses the database field NAME and the length of the edit buffer is limited to the length of the database field. The field LASTNAME has 20 characters in the example. A general incompatibility between database fields and XbpSLE objects is handled in the :dataLink code block. When data is read from the database field, the padding blank spaces are included. If the data is copied directly from the field LASTNAME into the edit buffer of the XbpSLE object, 20 characters are always included in the edit buffer even for a name such as "Smith" that is only five characters long. The blank spaces stored in the database field are copied into the edit buffer of the XbpSLE object. The result is that the edit buffer of the XBP object is already full and characters can only be added to the edit buffer in "Overwrite" mode. An XbpSLE object considers blank spaces as fully valid characters and to prevent these problems, the blank spaces at the end of the name (trailing spaces) are explicitly removed using Trim() when the data is read from the database field LASTNAME within :dataLink.

An XbpSLE object can only edit values in its edit buffer that are of "character" type. The maximum number of characters is 32KB. Values of numeric or date type must be converted to a character string when copied into the edit buffer of an XbpSLE object and converted back to the correct type before being written into the database field. This must be done in the data code block contained in :dataLink. Examples for code blocks which perform type conversions are shown below:

oXbp:dataLink := {|x| IIf(x==NIL, Transform( FIELD->NUMERIC, "@N"), ; 
                                  FIELD->NUMERIC := Val(x) ) } 
oXbp:dataLink := {|x| IIf(x==NIL, DtoC( FIELD->DATE ), ; 
                                  FIELD->DATE := CtoD(x) ) } 

When database fields are read into the edit buffer of an XbpSLE object blank spaces must be deleted and numeric and date values must be converted to character strings. When the modified data is saved to the database fields, values for date and numeric fields must again be converted to the correct data type. This task is performed by the data code block assigned to the instance variable :dataLink.

Another task of the data code block contained in :dataLink occurs when more than one file is required for the data entry screen (the DataDialog). In this case, fields from several databases are edited in a single data entry screen and the data code block must also select the correct work area for the field variable.

Program control in dialog windows

The previous discussions of GUI application concepts have focused on the basic organization of a GUI program. The key issues discussed were the program start, program execution, and program end. These correspond to AppSys() with a menu system, the Main procedure with the event loop and AppQuit(), respectively. The DataDialog class was discussed as a mechanism for linking dialog windows with DatabaseEngines. This class offers solutions to problems that can occur during simultaneous access on a network or when a database is opened multiple times in a single application. In the previous section, incorporating Xbase Parts into a dialog window was illustrated. The final remaining question for programming GUI applications is: How is the program controlled within an individual dialog window?

A distinction must be made between controlling a window and controlling an application. The overall running of the application is controlled by the application menu installed in the application window. In an SDI application, control of the application is basically the same as control of the dialog window, since the application consists of only a single dialog window. In the SDIDEMO example application, control of the application through the menu system includes selecting the data entry screens for customer data or for parts data. Control within windows occurs using pushbuttons that allow record pointer movement within the customer file or parts file, cause the current data to be saved or terminate the data input.

In the example application MDIDEMO, the application control is limited to opening the customer or parts data entry screen. A child window presents data for a customer or a part. As soon as a child window is opened, the application and the application menu no longer have control over the newly opened window. Program control within a child window is performed in an MDI application by a context menu that is an essential control element for program control. A context menu is generally activated by clicking the right mouse button. It is displayed on the screen as a Popup menu. Its menu items provide a selection of actions that are appropriate to execute within the window or in relation to the dialog element where the right mouse click occurred.

The context menu in the MDIDEMO example application includes program control of database navigation (DbSkip(), DbGoBottom(), DbGoTop()) and elementary database operations such as "Search", "Delete" and "New record". Programming a context menu requires the definition of an XbpMenu object and is otherwise similar to programming application menu objects. As an example, the program code to create the context menu for the customer database used in MDIDEMO is shown below:

******************************************************************** 
* Create context menu for customer dialog 
******************************************************************** 
STATIC FUNCTION ContextMenu() 
   STATIC soMenu 

   IF soMenu == NIL 
      soMenu       := DataDialogMenu():new() 
      soMenu:title := "Customer context menu" 
      soMenu:create() 

      soMenu:addItem( { "~New", ; 
                        {|mp1,mp2,obj| DbGoTo( LastRec()+1 ) } ; 
                    } ) 

      soMenu:addItem( { "~Seek" ,  ; 
                        {|mp1,mp2,obj| SeekCustomer( obj:cargo ) } ; 
                    } ) 

      soMenu:addItem( { "~Delete" , ; 
                        {|mp1,mp2,obj| DeleteCustomer( obj:cargo ) } ; 
                    } ) 

      soMenu:addItem( { "S~ave" , ; 
                        {|mp1,mp2,obj| obj:cargo:writeData()  } ; 
                    } ) 

      soMenu:addItem( MENUITEM_SEPARATOR ) 

      soMenu:addItem( { "~First" , ; 
                        {|mp1,mp2,obj| DbGoTop() } ; 
                    } ) 

      soMenu:addItem( { "~Last" , ; 
                        {|mp1,mp2,obj| DbGoBottom() } ; 
                    } ) 

      soMenu:addItem( MENUITEM_SEPARATOR ) 

      soMenu:addItem( { "~Previous" , ; 
                        {|mp1,mp2,obj| DbSkip(-1) } ; 
                    } ) 

      soMenu:addItem( { "~Next" , ; 
                        {|mp1,mp2,obj| DbSkip(1) } ; 
                    } ) 

      // menu items are disabled after Bof() or GoTop() 
      soMenu:disableTop    := { 6, 9 } 

      // menu items are disabled after GoBottom() 
      soMenu:disableBottom := { 7, 10 } 

      // menu items are disabled at Eof() 
      soMenu:disableEof    := { 1, 2, 3 } 

   ENDIF 

RETURN soMenu 

A code block is defined for each menu item in the context menu. This code block is executed when the user selects the menu item. Many of the code blocks control database navigation using functions such as DbSkip(), DbGoTop(), and DbGoBottom(). The DataDialog object is automatically notified of these operations (its :notify() method is called) since it is registered in the work area. The context menu itself can only be activated on the DataDialog object (child window) which currently has focus. The menu is activated with a right mouse click that must occur within the DataDialog window. The DataDialog window activates its context menu through the following callback code block (see MDICUST.PRG file, function Customer()):

drawingArea:RbDown := {|mp1,mp2,obj| ; 
     ContextMenu():cargo := obj:setParent(), ; 
     ContextMenu():popup( obj, mp1 ) } 

The :drawingArea is the drawing area of the DataDialog window. The ContextMenu() function is shown above. This function returns the contents of the STATIC variable soMenu, which is the context menu. The code block parameter obj contains a reference to the Xbase Part that is processing the event xbeM_RbDown (right mouse button is pressed). In this case, this is the drawing area of the DataDialog (:drawingArea) and the expression obj:setParent() returns the DataDialog object that is assigned to the :cargo instance variable of the context menu. This all occurs before the context menu is displayed using the method :popUp(). The current mouse coordinates (relative to obj) are contained in mp1. This allows the return value of ContextMenu() (the context menu) to be displayed at the position of the mouse pointer.

When a menu item is selected in the context menu, the DataDialog object where the context menu is activated is always contained in the :cargo instance variable. This DataDialog object has the focus (otherwise it would not react to a right mouse button click). The DataDialog object with the focus was previously selected via the callback code block :setDisplayFocus which sets the appropriate work area as the current work area. Database navigation can occur in the context menu by simply calling DbSkip() or DbGobottom() without the work area where the movement is to occur being specified. The work area is selected by the DataDialog object when it receives the focus. The context menu can only be activated on a DataDialog object that has the focus because only the DataDialog object with focus reacts to the event xbeM_RbDown and the context menu is only activated in the callback code block :RbDown.

This discussion outlines program control via a context menu as it is used in the example application MDIDEMO (it may be easier to follow by stepping through the code in the debugger). In conclusion, a context menu can be an important control element in a GUI application. Generally a context menu is not specific to a work area but calls functionality that must operate regardless of the work area. In short: a context menu controls an Xbase Part.

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.