The EXPRESS method



Creating and Using a Command-Based GUI Dialog system




ABSTRACT


This white-paper discusses the advantages of using a Command-Based GUI Dialog system for development of Windows applications using Xbase++. The subject matter includes the various techniques for development and usage of a robust command-layer which can also serve as a base for "data-driven" GUI applications and "active" form-designer systems. Learn how to create well-abstracted systems and how to design applications that are easier to maintain and debug. This seminar also introduces eXPress++, a third-party add-on product for Xbase++ that employs all of the techniques discussed.

AUTHOR


Roger Donnay is the president and founder of Donnay Software Designs, a Boise, Idaho, USA firm specializing in the development of Data-Driven development systems, programming tools, Calendar/Schedulers, Internet applications, and database management systems for microcomputers. Roger is the author of dCLIP / dCLIP++, an Application Development library that uses Data-Driven techniques, and eXPress++ an add-on library for Xbase++ that aids in the migration of Clipper text-based applications to Xbase++ GUI-based applications.

INTRODUCTION


When I took on the task of converting a large Clipper application to Windows, it soom became apparent that converting the thousands of lines of code and hundreds of dialogs would take much longer than the 6 months allocated to me for completion of the work. To keep this project on schedule, I needed to develop a design methodology that would allow me to leverage the 10 years and one million dollars of investment in the existing code base. Because of its Clipper compatability, Xbase++ became the obvious choice of language for the conversion. Soon after starting the work, I came to the realization that programming this application entirely in object-oriented syntax would generate 10 to 20 times more lines of code than the original text-based Clipper application. A command-based GUI language, similar to Clipper's @ SAY..GET and @..PROMPT would save thousands of lines of code and months of work. Unfortunately, no such command language existed, so I was required to develop it myself. This seminar discusses a strategy for how to develop a command-based GUI dialog system that is both well-integrated-to and well-abstracted-from the object-oriented model of the base language - Xbase++.

MIGRATION TO GUI


After spending the past 2 years discussing the conversion of Clipper applications to Xbase++ with a variety of clients, I have come to the realization that Clipper developers are not prepared to make the switch to Xbase++ until they have a clear vision of how the end product can be made "entirely GUI". Xbase++ offers so many migration possibilities that it isn't readily obvious which path to follow. GUI objects, like push-buttons, bitmaps, etc can function just as well when imbedded into areas of text on a text-screen as when imbedded into another GUI dialog screen. In some cases, it may be more advantageous to leave text code as is and embellish the screen with a few GUI controls, however, it will become soon obvious to the users that this does not look like a Windows application and the developer will be pushed to complete the migration to full GUI. This course will discuss a variety of ways to convert existing applications to full GUI while maintaining a program flow and a user interface that does not "shock" the users of the new Xbase application after being long-time users of the derived-from Clipper application.

Many Clipper applications that have been converted to Windows languages end up with source code that is often 5 to 10 times larger than the original Clipper code. After comparing the converted code to the original Clipper code it soon becomes obvious that converting hundreds of data-entry dialogs, menus, browses, etc can turn into a long, painstaking task. Object-oriented code has very little resemblance to the Clipper code. This is one of the reasons why many Clipper programmers have chosen not to even try to get acquainted with Windows development languages, because it doesn't really look at all like a step forward. They ask themselves "if Windows is so much better, then why does it take 10 times more code to accomplish the same task?" Well, in reality, there is no reason why GUI systems should require any more code than equivalent text-based systems. It's just that the developers of Windows languages have taken a different approach to RAD (rapid application design) than the developers of DOS systems. They chose to invest more in "visual" tools that allow the developer to write lots of lines of code with a few clicks of the mouse in a form-designer rather than to create new language abstractions that make it easier to migrate code from other languages. These languages were not developed with the purpose of converting applications from other languages. Xbase++, on the other hand, was designed particularly with this purpose in mind. Most Clipper applications are so large and contain so many dialog screens that it doesn't take long to realize that converting @..PROMPTs, @..SAY..GETs, Database browsers, Data-Entry screens, Picklists, etc. by writing Xbp*() class equivalents for each element of each menu, browse, get screen, etc can take an enormous amount of effort and yield an application that is less abstracted than the original Clipper application. Clipper gained a reputation for being one of the most productive application languages due to its high degree of language abstraction and for the ability of the programmer to create their own abstractions. A data-driven system takes the concept of abstraction to the highest possible level, and it's the power of multidimensional arrays, macros, code-blocks, the Database engine and the pre-processor that make much of this possible. These same features of the language are what make Clipper to Xbase++ migrations much easier than migrations to any other language. For example, the pre-processor can be used to allow you to use existing command syntax for @..PROMPTs and @..SAY..GETS while changing the "menu reader" from MenuTo() to a custom X_MenuTo() and the "get reader" from ReadModal() to a custom X_ReadGui(). By maintaining the same level of abstraction as in the Clipper language, thousands of lines of code can be migrated in weeks or months instead of years. In addition, it is much easier to fix bugs in abstracted systems. Developers can easily cause a regression to their library, in patches or new releases, which can cause a different behavior in a class or a method. Many times there are "workarounds" for these bugs. Systems that are not modularly designed or well-abstracted can often require modifying hundreds of lines of code, only to change them back again when the bug is fixed.

After we become acquainted with the concepts of creating command-based systems for menu and @..SAY.GETs it becomes readily apparent that there is no limitation at all to the amount of GUI objects that can be managed using a command-level interface; Tab Pages, Bit-maps, Browses, Memo editors, etc. can be used in GUI dialogs that can be written in a more procedural fashion rather than an object-oriented fashion. Clipper programmers who have been developing procedural and heirarchal applications for years will find it to be a much more comfortable and productive programming paradigm.

WHY COMMAND-BASED?


Clipper programmers who have attempted to convert their applications to Windows do not need to ask this question. It is fairly obvious. There are a variety of reasons why Clipper is such a "productive" language, but the most obvious, at least to me, is the pre-processor. This is the key to the most valuable level of abstraction - "the command layer". Xbase++ is a "great" language, not only because if its heritage, but because of its architecture. Commands are productive because they allow the programmer to create a "syntax" that is intuitive. This past week, I had an opportunity to meet a user of my product - eXPress++. He flew all the way from Bavaria, Germany to Boise, Idaho USA to spend a week with me. We speak two very different languages, yet when we are working with Xbase++ and eXPress++ we understand the same language, one based on an intuitive set of commands.
Advantages of command-based GUI dialogs.
  1. 1. Commands are "portable". Commands define a "behavior" whereas object-oriented code defines a "methodology". GUI dialogs in a large application that is command-based can be easily converted to any other language that supports a pre-processor. The only requirement is rewriting the GUI reader in the new language.
  2. 2. Commands are convertible. Command-based code can be easily converted to "data-driven", array-based code. Commands can be parsed into any kind of array structure, for use by a GUI reader or by a Form-Designer, without the need for a repository.
  3. 3. Commands are easy to read and maintain.
  4. 4. Commands are forgiving.
Command-line arguments can be typed in any order, whereas function-based or object oriented coding requires strict discipline and alertness to proper sequence and syntax.

@..PROMPT / MENU TO is a common method of creating menus in Clipper. It allows for menu items to be placed anywhere on the screen by using the @..PROMPT statement to locate the menu prompt and then the MENU TO command as a "reader" to allow the user to navigate the menu with the navigation keys and the select an item by pressing the ENTER key.

Let's look at the following Clipper menu system:
FUNCTION MyTextMenu()

LOCAL nChoice, promptList := {}
@ 10,10 PROMPT 'Browse Customers'
@ 12,10 PROMPT 'Edit Customers'
@ 14,10 PROMPT 'Browse Invoices'
@ 16,10 PROMPT 'Edit Invoices'

@ 10,30 PROMPT 'Pack Customers'
@ 12,30 PROMPT 'Export Customers'
@ 14,30 PROMPT 'Pack Invoices'
@ 16,30 PROMPT 'Export Invoices'

MENU TO nChoice

DO CASE
  CASE nChoice = 1
    BrowCust()
  CASE nChoice = 2
    EditCust()
.... more cases
ENDCASE

RETURN nil
Now, let's look at the same functional menu in a GUI-based system. This is a PushButton-Style menu that looks nearly identical to the text-based menu and returns the same value. A menu like this would need to be created using either XbpStatic() objects or XbpPushButton() objects on a GUI dialog. XbpStatic() objects would give the programmer the ability to create the menu items to look very much like the Clipper text-based menu prompts, but there is really no point in creating this type of menu prompt when the goal is to make the application look more like a Windows application and the code would be rather complicated to write. Probably the simplest and best approach for creating a clone of the Clipper menu is to use XbpPushButton() objects on an XbpDialog() window. In this example, a fully object-oriented technique is used to replace the Clipper code.

Here is the Xbase++ equivalent code:

This code uses a "direct" style of conversion, in which GUI elements of the Xbase language, ie "Xbase Parts" are used as direct replacements for elements of Clipper language.
FUNCTION MyGuiMenu()

    LOCAL oParent, nCol, nRow, oDlg, drawingArea, oXbp, mp1,
    mp2, nEvent, nChoice := 0, aHotKey, oButton[8], nHotKey
    oParent := AppDeskTop()
    nCol := (oParent:currentSize()[1]-280)/2
    nRow := (oParent:currentSize()[2]-280)/2
    oDlg := XbpDialog():new( oParent, , { nCol, nRow }, { 280, 300 } )
    oDlg:taskList := .T.
    oDlg:create()
    drawingArea := oDlg:drawingArea
    oButton[1]:= XbpPushButton():new( drawingArea, , { 10,200 }, { 100,30 } )
    oButton[1]:caption := 'Browse &Customers' oButton[1]:create()
    oButton[1]:activate := ;
      {|| nChoice := 1, PostAppEvent(xbeP_Close)} SetAppFocus(oButton[1])
    oButton[2] := XbpPushButton():new( drawingArea, , { 10,150 }, { 100,30 } )
    oButton[2]:caption := 'Edit C&ustomers'
    oButton[2]:create()
    oButton[2]:activate := {|| nChoice := 2, PostAppEvent(xbeP_Close)}
    oButton[3]:= XbpPushButton():new( drawingArea, , ;
       { 10,100 }, { 100,30 } )
    oButton[3]:caption := 'Browse &Invoices'
    oButton[3]:create()
    oButton[3]:activate := {|| nChoice := 3, PostAppEvent(xbeP_Close)}
    oButton[4]:= XbpPushButton():new( drawingArea, , ;
       { 10, 50 }, { 100,30 } )
    oButton[4]:caption := '&Edit Invoices'
    oButton[4]:create()
    oButton[4]:activate := {|| nChoice := 4, PostAppEvent(xbeP_Close)}
    oButton[5]:= XbpPushButton():new( drawingArea, , ;
       { 150,200 }, { 100,30 } )
    oButton[5]:caption := '&Pack Customers'
    oButton[5]:create()
    oButton[5]:activate := {|| nChoice := 5, PostAppEvent(xbeP_Close)}
    oButton[6]:= XbpPushButton():new( drawingArea, , ;
       { 150,150 }, { 100,30 } )
    oButton[6]:caption := 'E&xport Customers'
    oButton[6]:create()
    oButton[6]:activate := {|| nChoice := 6, PostAppEvent(xbeP_Close)}
    oButton[7]:= XbpPushButton():new( drawingArea, , ;
       { 150,100 }, { 100,30 } )
    oButton[7]:caption := 'Pac&k Invoices'
    oButton[7]:create()
    oButton[7]:activate := {|| nChoice := 7, PostAppEvent(xbeP_Close)}
    oButton[8]:= XbpPushButton():new( drawingArea, , ;
       { 150,50 }, { 100,30 } )
    oButton[8]:caption := 'Ex&port Invoices'
    oButton[8]:create()
    oButton[8]:activate := {|| nChoice := 8, PostAppEvent(xbeP_Close)}
    aHotKey := { 'C','U','I','E','P','X','K','P'}
    nChoice := 1
    DO WHILE nEvent <> xbeP_Close
       nEvent := AppEvent( @mp1, @mp2, @oXbp )
       oXbp:handleEvent( nEvent, mp1, mp2 )
       IF nEvent = xbeP_Keyboard .AND. oXbp == oDlg
          nHotKey := AScan(aHotKey,Upper(Chr(mp1)))
          IF nHotKey > 0 .OR. mp1 = K_ESC
             nChoice := nHotkey
             IF nChoice > 0
                SetAppFocus(oButton[nChoice])
             ENDIF
             EXIT
          ELSEIF mp1 = K_ENTER
             EXIT
          ELSEIF mp1 = xbeK_DOWN
             nChoice++
             IF nChoice > 8
                nChoice = 1
             ENDIF
          ELSEIF mp1 = xbeK_UP
             nChoice-
             IF nChoice < 1
                nChoice := 8
             ENDIF
          ENDIF
          SetAppFocus(oButton[nChoice])
       ENDIF
    ENDDO
    oDlg:Destroy()
    DO CASE
    CASE nChoice = 1
       BrowCust()
    CASE nChoice = 2
       EditCust()
       .... more cases
    ENDCASE

RETURN nil
Obviously, the code required to create a GUI equivalent of the original code doesn't look anything at all similar and it requires many more lines of code to accomplish basically the same task. This is because Clipper had already abstracted the same basic functionality into a simple set of prompts and a "reader" command. To make Xbase++ as simple to use as Clipper when creating menu prompts would require the creation of the same type of abstraction. The above code also doesn't handle the displaying of a message line or navigation with the left/right arrows. This would require more lines of code. You will also notice that there is really no correlation between the text-based coordinates in the Clipper code and the GUI-based coordinates in the Xbase++ code.

A Command-based Floating Menu system for Xbase++


If you have many menus in your application that use @..PROMPT and MENU TO, then you may want to consider writing an "abstracted" system that allows you to convert your existing @..PROMPT menus statements to an equivalent GUI menu by making only minor code changes. Here's a basic design for a Clipper equivalent. It uses the pre-processor to create equivalent commands @..XPROMPT and XMENU TO. XPROMPT adds the prompts to an array which is read by the XMENU TO command. First, we create a set of commands that are equivalent to the Clipper @..PROMPT / MENU TO commands: XMENU.CH
 #command  @ < nRow >, < nCol > XPROMPT < prompt >             ;
                                [MESSAGE < message >]          ;
                                [ID < menuid >]                ;
                                [ACTION < action >]            ;
 =>                                                            ;
       X_AtPrompt( < nRow >, < nCol >, < prompt >, promptList, ;
                     , ,  )
   #command  XMENU TO < var >                                  ;
                     [< notrim:NOTRIM >]                       ;
                     [MSGFONT < msgfont >]                     ;
                     [MSGPOS < row >,< col >]                  ;
                     [< center:MSGCENTER >]                    ;
                     [TITLE < title >]                         ;
 =>                                                      ;
    < var > := X_MenuTo(@PromptList, < var >, UPPER(#< var >), ;
                     < title >, {< row >,< col >,< .center. >, ;
                     < msgfont >}, !< .notrim. >)
Next, we create the functions. X_AtPrompt() simply adds the prompts to an array. X_MenuTo() receives the array as a parameter along with other options like font, title and message position. X_MenuTo() also has the task of converting the text-based coordinates to pixel-based coordinates for the XbpPushButton() objects. This abstracted code system can often take about 5-10 times longer to develop than the direct equivalent and will use more lines of code, but the investment of time up front will pay great dividends when the new system is used again and again in the application to convert existing menus and in new applications to create new floating menus. In the long run, the total amount of time spent on menus will be much less, the total amount of code linked into the application will be much smaller and we will have a useful set of resuable functions and commands that will exhibit consistent behavior throughout the application.
FUNCTION X_AtPrompt ( nRow, nCol, cPrompt, PromptList, cMessage, ;
      cMenuId, bBlock )

   DEFAULT nRow := 0, nCol := 0, cPrompt := ", cMenuId := "
   AADD( PromptList,{ INT(nRow), INT(nCol), cPrompt, cMessage, ;
      cMenuId, bBlock } )

RETURN nil

/* ---------------------------- */

FUNCTION X_MenuTo( aPrompt, nChoice, cVarName, cTitle, aMsgOpt, lTrim )

   LOCAL oDlg, oXbp, mp1, mp2, nEvent, nHeight, nWidth, nStartRow,;
      nEndRow, nStartCol, nEndCol, nMsgRow, nMsgCol, oParent,;
      nCol, nRow, i, drawingArea, oMsg, nMsgWidth, cMsgFont,;
      lMsgCenter, aHotkeys := {}

   IF Valtype(aPrompt) # 'A'
      RETURN nil
   ENDIF

   DEFAULT lTrim := .t., aMsgOpt := {}, cTitle := "
   ASize(aMsgOpt,4)
   DEFAULT aMsgOpt[1] := 24, aMsgOpt[2] := 0, aMsgOpt[3] := .t., ;
      aMsgOpt[4] := '12.Times.Roman'
   nMsgRow    := aMsgOpt[1]
   nMsgCol    := aMsgOpt[2]
   lMsgCenter := aMsgOpt[3]
   cMsgFont   := aMsgOpt[4]
   nStartRow := 25
   nStartCol := 80
   nEndRow   := 0
   nEndCol   := 0
   nMsgWidth := 0
   FOR i := 1 TO Len(aPrompt)
      nStartRow := Min(nStartRow,aPrompt[i,1])
      nEndRow   := Max(nEndRow,aPrompt[i,1])
      nStartCol := Min(nStartCol,aPrompt[i,2])
      nEndCol   := Max(nEndCol,aPrompt[i,2]+Len(aPrompt[i,3]))
      aPrompt[i,4] := IIF(Valtype(aPrompt[i,4])='C',aPrompt[i,4],")
      nMsgWidth := Max(nMsgWidth,Len(aPrompt[i,4]))
      nEndCol   := Max(nEndCol,nMsgCol+Len(aPrompt[i,4]))
   NEXT
   IF nMsgWidth > 0
      IF Valtype(nMsgRow)#'N'
         nMsgRow := Set(_SET_MESSAGE)
         nMsgCol := 0
      ENDIF
      nStartRow := Min(nStartRow,nMsgRow)
      nEndRow   := Max(nEndRow,nMsgRow)
      nStartCol := Min(nStartCol,nMsgCol)
      nEndCol   := Max(nEndCol,nMsgCol)
   ENDIF
   IF !lTrim
      nWidth  := 80
      nHeight := 25
   ELSE
      nWidth := nEndCol - nStartCol + 4
      nHeight := nEndRow - nStartRow + 4
   ENDIF
   nWidth  := (nWidth * 7.0)
   nHeight := (nHeight * 20 )
   oParent := AppDeskTop()
   nCol := (oParent:currentSize()[1]-nWidth)/2
   nRow := (oParent:currentSize()[2]-nHeight)/2

   oDlg := XbpDialog():new( oParent, , { nCol, nRow }, ;
      { nWidth, nHeight } ) oDlg:taskList := .T.
   oDlg:title := cTitle
   oDlg:visible := .F.
   oDlg:create()
   drawingArea := oDlg:drawingArea
   FOR i := 1 TO Len(aPrompt)
      ASize( aPrompt[i],6 ) oXbp := XbpPushButtonEx():new( drawingArea, , ;
         { _GetCol(aPrompt[i,2], nWidth, lTrim, nStartCol ), ;
         _GetRow(aPrompt[i,1], nHeight, lTrim, nStartRow ) }, ;
         { Len(aPrompt[i,3]) * 7, 20 } )
      oXbp:caption := aPrompt[i,3]
      oXbp:clipSiblings := .T.
      oXbp:create()
      oXbp:activate := aPrompt[i,5]
      oXbp:cargo    := aPrompt[i,4]
      aPrompt[i,6] := oXbp
      AADD( aHotKeys, Upper(Left(Alltrim(aPrompt[i,3]),1)) )
   NEXT
   IF nMsgWidth > 0
      oMsg := XbpStatic():new( drawingArea, , ;
         { _GetCol(nMsgCol, nWidth, lTrim, nStartCol ), ;
         _GetRow(nMsgRow, nHeight, lTrim, nStartRow ) }, ;
         { IIF(Set(_SET_MCENTER),nWidth,nMsgWidth * 7), 20 } )
      oMsg:caption := aPrompt[1,4] oMsg:clipSiblings := .T.
      oMsg:type := XBPSTATIC_TYPE_TEXT
      IF Valtype(cMsgFont)='C'
         oMsg:SetFontCompoundName(cMsgFont)
      ENDIF
      oMsg:create()
   ENDIF
   oDlg:Show()
   nEvent := xbe_None
   SetAppFocus(aPrompt[1,6])
   IF !Empty(nChoice) .AND. Valtype(nChoice)='N'
      SetAppFocus( aPrompt[nChoice,6] )
   ENDIF
   DO WHILE nEvent # xbeP_Close
      nEvent := AppEvent( @mp1, @mp2, @oXbp )
      oXbp:handleEvent( nEvent, mp1, mp2 )
      IF ( nEvent = xbeP_Keyboard .AND. mp1 = K_ESC)  .OR. ;
            ( nEvent = xbeP_Close )
         nChoice := 0
         EXIT
      ENDIF
      IF nEvent = xbeP_Activate .OR. ;
            (nEvent = xbeP_Keyboard .AND. oXbp == oDlg)
         IF mp1 = xbeK_ENTER .OR. nEvent = xbeP_Activate
            FOR i := 1 TO LEN(aPrompt)
               IF oXbp == aPrompt[i,6]
                  oXbp := _SetFocus( aPrompt, i, mp1 # xbeK_UP )
                  IF Valtype(oMsg)='C' .AND. Valtype(oXbp:cargo) = 'C'
                     oMsg:SetCaption(oXbp:cargo)
                  ENDIF
                  nChoice := i
                  nEvent := xbeP_Close
                  EXIT
               ENDIF
            NEXT
         ELSEIF ( mp1 = xbeK_UP .OR. mp1 = xbeK_DOWN )
            FOR i := 1 TO LEN(aPrompt)
               IF oXbp == aPrompt[i,6]
                  oXbp := _SetFocus( aPrompt, i, mp1 # xbeK_UP )
                  IF Valtype(oMsg)='C' .AND. Valtype(oXbp:cargo) = 'C'
                     oMsg:SetCaption(oXbp:cargo)
                  ENDIF
                  EXIT
               ENDIF
            NEXT
         ELSEIF nEvent = xbeP_Keyboard
            nChoice := AScan( aHotKeys, Upper(DC_ChrInkey(mp1)) )
            IF nChoice > 0
               SetAppFocus( aPrompt[nChoice,6] )
               EXIT
            ENDIF
         ENDIF
      ENDIF
      IF Valtype(oXbp:cargo)='C' .AND. Valtype(oMsg)='O'
        oMsg:SetCaption(oXbp:cargo)
      ENDIF
   ENDDO
   aPrompt := {}
   oDlg:Destroy()

RETURN nChoice

/* ----------------------- */

STATIC FUNCTION _GetRow ( nRow, nHeight, lTrim, nStartRow )

   IF lTrim
      RETURN nHeight - (nRow*20) + (nStartRow*20) - 60
   ENDIF

RETURN nHeight - (nRow*20) - 20

/* ----------------------- */

STATIC FUNCTION _GetCol ( nCol, nWidth, lTrim, nStartCol )

   IF lTrim
      RETURN (nCol*7) - (nStartCol*7) + 10
   ENDIF
RETURN (nCol*7)

/* ----------------------------- */

STATIC FUNCTION _SetFocus ( aPrompt, i, lDown, oMsg )

   LOCAL oXbp
   IF lDown
      DO WHILE .t.
         IF i = LEN(aPrompt)
            IF Set(_SET_WRAP)
               i := 1
            ENDIF
         ELSE
            i++
         ENDIF
         SetAppFocus( aPrompt[i,6] )
         oXbp := aPrompt[i,6]
         EXIT
      ENDDO
   ELSE
      DO WHILE .t.
         IF i = 1
            IF Set(_SET_WRAP) i := Len(aPrompt)
            ENDIF
         ELSE
            i-
         ENDIF
         SetAppFocus( aPrompt[i,6] )
         oXbp := aPrompt[i,6]
         EXIT
      ENDDO
   ENDIF
RETURN oXbp
The orginal application code can then be modified to use the new commands like so:
FUNCTION MyNewGuiMenu()

   LOCAL nChoice, promptList := {}
   @ 10,10 XPROMPT 'Browse Customers'
   @ 12,10 XPROMPT 'Edit Customers  '
   @ 14,10 XPROMPT 'Browse Invoices '
   @ 16,10 XPROMPT 'Edit Invoices   '

   @ 10,30 XPROMPT 'Pack Customers  '
   @ 12,30 XPROMPT 'Export Customers'
   @ 14,30 XPROMPT 'Pack Invoices   '
   @ 16,30 XPROMPT 'Export Invoices '

   XMENU TO nChoice
   DO CASE
   CASE nChoice = 1
      BrowCust()
   CASE nChoice = 2
      EditCust()
      .... more cases
   ENDCASE
RETURN nil

@..SAY..GETS


@..SAY..GET / READ is a common method of creating data-entry screens in Clipper. It allows for prompts to be placed anywhere on the screen by using the @..SAY..GET statement to locate the GET and then the READ command as a "reader" to allow the user to navigate the table of GETs with the navigation keys and enter data into each GET object. GETs are used to edit the value of memory variables and/or data fields of any type other than Memo and Code Block. First, Let's look at the following Clipper Data-Entry screen:
FUNCTION test

LOCAL cSaveScreen, cCustomer, cAddress, cCity, cState, cZip, ;
cPhone, cEMail, GetList := {}

USE CUSTOMER EXCLUSIVE
cCustomer := CUSTOMER->customer
cAddress  := CUSTOMER->address
           cCity     := CUSTOMER->city
           cState    := CUSTOMER->state
           cZip      := CUSTOMER->zip
           cPhone    := CUSTOMER->phone
           cEMail    := CUSTOMER->email

cSaveScreen := SaveScreen( 5,15,18,67 )
@ 5,15 CLEAR TO 18,67
@ 5,15 TO 18,67

@  7,20 SAY ' Customer Name' GET cCustomer ;
VALID TestEmpty(cCustomer,'Customer Name')
@  9,20 SAY 'Street Address' GET cAddress ;
VALID TestEmpty(cAddress,'Address')
@ 10,20 SAY '          City' GET cCity ;
VALID TestEmpty(cCity,'City')
@ 11,20 SAY '         State' GET cState PICT '!!' ;
VALID TestEmpty(cState,'State')
@ 12,20 SAY '      Zip Code' GET cZip PICT '99999-9999' ;
VALID TestEmpty(cZip,'Zip Code')
           @ 14,20 SAY '         Phone' GET cPhone PICT '999-999-9999'
           @ 15,20 SAY '        E-Mail' GET cEMail

READ
RestScreen( 5,15,18,67,cSaveScreen)
REPLACE CUSTOMER->customer WITH cCustomer, ;
CUSTOMER->address  WITH cAddress, ;
                   CUSTOMER->city     WITH cCity, ;
                   CUSTOMER->state    WITH cState, ;
                   CUSTOMER->zip      WITH cZip, ;
                   CUSTOMER->phone    WITH cPhone, ;
                   CUSTOMER->email    WITH cEMail

RETURN nil
Here is the code for a conversion of text-based @..SAY..GETs using the "direct" method of conversion. Example of GUI version of @SAY..GETs
FUNCTION test

/* -- Declare Locals -- */
LOCAL cSaveScreen, cCustomer, cAddress, cCity, cState, cZip, ;
cPhone, cEmail, oXbp, oDlg, mp1, mp2, nEvent, drawingArea, ; lOk := .f.
/* -- Gather Data -- */
USE CUSTOMER EXCLUSIVE

cCustomer := CUSTOMER->customer
cAddress  := CUSTOMER->address
           cCity     := CUSTOMER->city
           cState    := CUSTOMER->state
           cZip      := CUSTOMER->zip
           cPhone    := CUSTOMER->phone
           cEmail    := CUSTOMER->email

/* -- Create Dialog - */

oDlg := XbpDialog():new( AppDeskTop(), , { 100, 100 }, ;
{ 400, 370 } ) oDlg:taskList := .T.  oDlg:create()
drawingArea := oDlg:drawingArea
oDlg:Show()

/* -- Customer Name -- */
oXbp := XbpStatic():new( drawingArea, , { 20, 300 }, { 100, 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := 'Customer Name'
oXbp:options := XBPSTATIC_TEXT_RIGHT oXbp:create()
oXbp := XbpGet():new( drawingArea, , { 150, 300 }, { 200, 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := {|u|IIF(Pcount()=1,cCustomer:=u,cCustomer)}
oXbp:create()
oXbp:SetData()
oXbp:validate := {||TestEmpty2(cCustomer,'Customer Name')}
oXbp:killInputFocus := { |x,y,o| o:getData(),;
                          IIF(!Eval(o:validate),SetAppFocus(o),nil) }

/* -- Street Address -- */
oXbp := XbpStatic():new( drawingArea, , { 20, 275 }, { 100, 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := 'Address'
oXbp:options := XBPSTATIC_TEXT_RIGHT oXbp:create()
oXbp := XbpGet():new( drawingArea, , { 150, 275 }, { 200, 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := {|u|IIF(Pcount()=1,cAddress:=u,cAddress)}
oXbp:create()
oXbp:SetData()
oXbp:killInputFocus := { |x,y,o| o:getData(),;
                         IIF(!Eval(o:validate),SetAppFocus(o),nil) }

/* -- City -- */
oXbp := XbpStatic():new( drawingArea, , { 20, 250 }, { 100, 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := 'City'
oXbp:options := XBPSTATIC_TEXT_RIGHT
oXbp:create()
oXbp := XbpGet():new( drawingArea, , { 150, 250 }, { 200, 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := {|u|IIF(Pcount()=1,cCity:=u,cCity)}
oXbp:create()
oXbp:SetData()
oXbp:killInputFocus := { |x,y,o| o:getData(),;
                          IIF(!Eval(o:validate),SetAppFocus(o),nil) }

/* -- State -- */
oXbp := XbpStatic():new( drawingArea, , { 20, 225 }, { 100, 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := 'State'
oXbp:options := XBPSTATIC_TEXT_RIGHT
oXbp:create()
oXbp := XbpGet():new( drawingArea, , { 150, 225 }, { 50, 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := {|u|IIF(Pcount()=1,cState:=u,cState)}
oXbp:create()
oXbp:SetData()
oXbp:killInputFocus := { |x,y,o| o:getData(),;
                          IIF(!Eval(o:validate),SetAppFocus(o),nil) }

/* -- Zip -- */
oXbp := XbpStatic():new( drawingArea, , { 20, 200 }, { 100, 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := 'Zip'
oXbp:options := XBPSTATIC_TEXT_RIGHT
oXbp:create()
oXbp := XbpGet():new( drawingArea, , { 150, 200 }, { 100, 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := {|u|IIF(Pcount()=1,cZip:=u,cZip)}
oXbp:create()
oXbp:SetData()
oXbp:killInputFocus := { |x,y,o| o:getData(),;
                         IIF(!Eval(o:validate),SetAppFocus(o),nil) }

/* -- Phone -- */
oXbp := XbpStatic():new( drawingArea, , { 20, 175 }, { 100, 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := 'Phone'
oXbp:options := XBPSTATIC_TEXT_RIGHT
oXbp:create()
oXbp := XbpGet():new( drawingArea, , { 150, 175 }, { 100, 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := {|u|IIF(Pcount()=1,cPhone:=u,cPhone)}
oXbp:create()
oXbp:SetData()
oXbp:killInputFocus := { |x,y,o| o:getData() }

/* -- E-Mail -- */
oXbp := XbpStatic():new( drawingArea, , { 20, 150 }, { 100, 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := 'E-Mail'
oXbp:options := XBPSTATIC_TEXT_RIGHT
oXbp:create()
oXbp := XbpGet():new( drawingArea, , { 150, 150 }, { 100, 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := {|u|IIF(Pcount()=1,cEmail:=u,cEmail)}
oXbp:create()
oXbp:SetData()
oXbp:killInputFocus := { |x,y,o| o:getData() }

/* -- Buttons -- */
oXbp := XbpPushButton():new( drawingArea, , { 20, 50 }, { 50, 20 } )
oXbp:clipSiblings := .t.
oXbp:caption := '&Ok'
oXbp:activate := {||lOk := .t.,PostAppEvent(xbeP_Close)}
oXbp:create()

oXbp := XbpPushButton():new( drawingArea, , { 80, 50 }, { 50, 20 } )
oXbp:clipSiblings := .t.
oXbp:caption := '&Cancel'
oXbp:activate := {||lOk := .f.,PostAppEvent(xbeP_Close)}
oXbp:create()

/* -- Event Loop -- */
nEvent := 0

DO WHILE nEvent # xbeP_Close
  nEvent := AppEvent( @mp1, @mp2, @oXbp )
  oXbp:handleevent( nEvent, mp1, mp2 )
ENDDO
oDlg:Destroy()

/* -- Scatter Data -- */
IF lOk
  REPLACE CUSTOMER->customer WITH cCustomer, ;
                     CUSTOMER->address  WITH cAddress, ;
                     CUSTOMER->city     WITH cCity, ;
                     CUSTOMER->state    WITH cState, ;
                     CUSTOMER->zip      WITH cZip, ;
                     CUSTOMER->phone    WITH cPhone, ;
                     CUSTOMER->email    WITH cEmail
ENDIF
RETURN nil

/* ---------------------- */

STATIC FUNCTION TestEmpty( cValue, cText )

IF Empty( cValue )
  MsgBox( cText + ' cannot be empty.' )
  RETURN .f.
ENDIF
RETURN .t.
After comparing the above code to the original Clipper code it soon becomes obvious that converting hundreds of data-entry dialogs can turn into a daunting task. The object-oriented code above has very little resemblance to the Clipper code and has even less functionality. The code does not handle navigation through the gets with the keyboard nor does it handle "pictures". The other difficulty is the basic "syntax" of the object oriented code along with the fact that functions and assignments are required to be called in a special sequence, make it very easy for the programmer to make coding mistakes. This "direct" method is provided to demonstrate the pitfalls of this style of programming.

Now, let's look at methods to simplify this code. Notice that there is very little difference between the code that handles the "Customer" SAY/GET any all the other SAY/GETs. This makes it easy to create the objects in a sub-routine that gets its information from an array. The only portion of the code that is different from the first example is the portion that creates the XbpStatic(), (SAYs) and the XbpGet() (GETs) objects. This greatly simplifies the design of the screen because now the GETS can be defined in an array.
/* -- Declare Locals -- */
/* -- Gather Data -- */
/* -- Create Dialog -- */

/*
1 -	Say Prompt
2 -	Get-Set block
3 -	Say Column
4 -	Say Row
5 -	Say Width
6 -	Get Column
7 -	Get Row
8 -	Get Width
9 -	Picture
10 -	Valid Block
*/

/* -- Create Getlist -- */
aGetList := { ;
{ 'Customer', _GetSet(@cCustomer), ;
  20, 300, 100, 150, 300, 200, nil, ;
  {||TestEmpty(cCustomer,'Customer Name')} }, ;
{ 'Address', _GetSet(@cStreet), ;
  20, 275, 100, 150, 275, 200, nil, ;
  {||TestEmpty(cStreet,'Address')} }, ;
{ 'City', _GetSet(@cCity), ;
  20, 250, 100, 150, 250, 200, nil, ;
  {||TestEmpty(cCity,'City')} }, ;
{ 'State', _GetSet(@cState), ;
  20, 225, 100, 150, 225, 50, '!!', ;
  {||TestEmpty(cState,'State')} }, ;
{ 'Zip Code', _GetSet(@cZip), ;
  20, 200, 100, 150, 200, 100, '99999-9999', ;
  {||TestEmpty(cZip,'Zip Code')} }, ;
{ 'Phone', _GetSet(@cPhone), ;
  20, 175, 100, 150, 175, 120, '999-999-9999', ;
  {||TestEmpty(cPhone,'Phone')} }, ;
{ 'Fax', _GetSet(@cFax), ;
  20, 150, 100, 150, 150, 120, '999-999-9999', ;
  {||TestEmpty(cPhone,'Phone')} } }

FOR i := 1 TO Len(aGetList)
  _CreateGet( aGetList[i], drawingArea )
NEXT

/* -- Buttons -- */
/* -- Event Loop -- */
/* -- Scatter Data -- */

RETURN

/* -------------------------- */

STATIC PROCEDURE _CreateGet ( aGet, oParent )

LOCAL oXbp

oXbp := XbpStatic():new( oParent, , { aGet[3], aGet[4] }, { aGet[5], 20 } )
oXbp:type    := XBPSTATIC_TYPE_TEXT
oXbp:caption := aGet[1]
oXbp:options := XBPSTATIC_TEXT_RIGHT
oXbp:create()

oXbp := XbpGet():new( oParent, , { aGet[6], aGet[7] }, { aGet[8], 20 } )
oXbp:Tabstop := .t.
oXbp:clipSiblings := .t.
oXbp:datalink := aGet[2]
IF !Empty(aGet[9])
  oXbp:picture := aGet[9]
ENDIF
oXbp:create()
oXbp:SetData()
oXbp:validate := aGet[10]
oXbp:killInputFocus := { |x,y,o| o:getData(),;
                          IIF(Valtype(o:validate)#'B' .OR. !Eval(o:validate),;
SetAppFocus(o),nil) }
RETURN

/* -------------------------- */

STATIC FUNCTION _GetSet ( uVar )

RETURN {|u|IIF(PCount()=1,uVar:=u,uVar)}
When creating a small number of @..SAY..GETs, the above simplification actually produces about the same number of lines of code. Where it really starts to pay off is when using this same technique for complicated screens with 10 or more @..SAY..GETs.

Another way to simplify the coding process is to use the pre-processor for creating your own @..SAY..GET command. In this case the @..XSAY..GET command syntax is similar to the @..SAY..GET command except the Column address is first, the location is in pixels and the coordinates are based on 0,0 home at the lower left corner.
#xcommand @ < nSayCol >, < nSayRow > XSAY < cText >                ;
          [SAYWIDTH < nSayWidth >]                                 ;
          GET < uVar > [GETWIDTH < nGetWidth >]                    ;
          [PICTURE < cPict >]                                      ;
          [VALID < bValid >]                                       ;
     =>                                                            ;
  Add( aGetList, { < cText >, _GetSet(@< uVar >),                  ;
        < nSayCol >, < nSayRow >, < nSayWidth >,                   ;
         nil, < nSayRow >, < nGetWidth >, < cPict >, < {bValid} > } )

#xcommand XSHOWGETS =>                                             ;
  FOR i := 1 TO Len(aGetList)                                      ;
      ; _CreateGet( aGetList\[i], drawingArea )                    ;
 ; NEXT

/* -------------------- */

aGetlist := {}

@ 20, 300 XSAY 'Customer' GET cCustomer GETWIDTH 200 ;
  VALID TestEmpty(cCustomer,'Customer Name')
@ 20, 275 XSAY 'Address' GET cStreet GETWIDTH 200 ;
  VALID TestEmpty(cStreet,'Address')
@ 20, 250 XSAY 'City' GET cCity GETWIDTH 200 ;
  VALID TestEmpty(cCity,'City')
@ 20, 225 XSAY 'State' GET cState GETWIDTH 50 ;
  VALID TestEmpty(cState,'State') PICTURE '!!'
@ 20, 200 XSAY 'Zip Code' GET cZip GETWIDTH 100 ;
  VALID TestEmpty(cZip,'Zip Code') PICTURE '99999-9999'
@ 20, 175 XSAY 'Phone' GET cPhone GETWIDTH 120 ;
  PICTURE '999-999-9999'
@ 20, 150 XSAY 'Fax' GET cFax GETWIDTH 120 ;
  PICTURE '999-999-9999'

XSHOWGETS

The eXPress++ "GETLIST" method


Clipper data entry screens were easy to design and maintain because of the concept of the Get-List. Clipper 5.2 only supported @..SAY..GETs whereas Clipper 5.3 improved on the concept by adding Radio Buttons, Check-boxes and Pushbuttons. Unfortunately, the Clipper Get-List architecture was not originally designed for a variety of dialog objects so it evolved into a hybrid system that really had no future. The GetList concept, as it applys to GUI dialogs must be well thought out so it can deal with the endless possibilities of GUI objects that may not even exist today, including custom classes. When I first started designing a GetList model for Xbase++, I thought of creating an array structure that would deal with the existing set of Xbase Parts. It did not take long however, to be challenged with GUI dialogs that needed a custom class so I needed to design a GetList array structure and command language that would handle these extensions. An eXPress++ GUI dialog system consists of the following parts:
  1. 1. The GetList Array - GetList
  2. 2. The GetOptions Array - GetOptions
  3. 3. The Command definitions - DCDIALOG.CH
  4. The GUI Reader - DC_ReadGui()

The GetList Array


The GetList Array contains one sub-array for each dialog item. Each sub-array contains the same number of elements, with element 1 designating the type of dialog object being defined. The GetList should be designed to allow for "data-driving" a GUI dialog system, i.e. loading the array from a database, .INI file, array file, etc. This means that the GetList does not "contain" objects, but instead it only "defines" objects which will be created by the GUI reader. Because every type of object consists of different methods and instance variables, the GetList design must allow for these variations. I chose to handle unique functions by reserving 10 elements of each sub-array for optional parameters. Since each element can contain a sub-array, there is no limit to the possibilities. The remaining elements of each sub-array define attributes of the object that are common to all objects, i.e., location, caption, parent, relatives, size, colors and anchors. The most important decision that can be made is the architecture of the GetList because this will decide how robust the system will be and its future. The architecture of a GetList system must be as open as the architecture of the base language - Xbase++. It must allow for any possibility and for easy integration with existing dialog systems and application code.

I believe that the system designed in eXPress++ contains the robustness required to meet the challenges of any large database application. The GetList is usally created by adding one item for each command, just as in Clipper. It could also be created by code that reads information from a database.

Below is a description of each element of the GetList array.

NOTE: All elements that require a code block can store either a code block or an array of two elements: the first being the code block and the second being the string representation of the code block. Passing an array allows for "data-driven" applications because the reader will automatically macro-compile the string to a code-block if the code block does not exist.

nGETLIST_TYPE

This is the type of object to paint on the dialogue screen. The supported types are defined in DCDIALOG.CH and are listed below:
    GETLIST_DIALOG        - DIALOG, XbpDialog()
    GETLIST_STATIC        - STATIC, XbpStatic()
    GETLIST_GET           - GET, XbpGet()
    GETLIST_MLE           - MULTI-LINE GET, XbpMLE()
    GETLIST_3STATE        - 3-STATE, Xbp3State()
    GETLIST_CHECKBOX      - CHECKBOX, XbpCheckBox()
    GETLIST_COMBOBOX      - COMBOBOX, XbpCombobox()
    GETLIST_LISTBOX       - LIST BOX, XbpListBox()
    GETLIST_PUSHBUTTON    - PUSHBUTTON, XbpPushButton()
    GETLIST_RADIOBUTTON   - RADIO BUTTON, XbpRadioButton()
    GETLIST_SAY           - SAY, XbpStatic() text
    GETLIST_ADDBUTTON     - RADIO BUTTON, XbpStatic() on Toolbar
    GETLIST_SPINBUTTON    - SPIN BUTTON, XbpSpinButton()
    GETLIST_GROUPBOX      - GROUP BOX, XbpStatic()
    GETLIST_TABPAGE       - TAB PAGE, XbpTabPage()
    GETLIST_SCROLLBAR     - SCROLL BAR, XbpScrollBar()
    GETLIST_BITMAP        - BIT MAP, XbpBitMap(), XbpPresSpace()
    GETLIST_METAFILE      - METAFILE, XbpMetaFile()
    GETLIST_TOOLBAR       - TOOLBAR, XbpStatic()
    GETLIST_MENUBAR       - MENUBAR, XbpMenu()
    GETLIST_SUBMENU       - SUB MENU, XbpMenu()
    GETLIST_MENUITEM      - MENU ITEM, XbpMenu():additem
    GETLIST_BROWSE        - BROWSE, XbpBrowse()
    GETLIST_BROWSECOL     - BROWSE COLUMN, XbpColumn()
    GETLIST_MESSAGEBOX    - MESSAGE AREA, XbpStatic()
    GETLIST_CUSTOM        - CUSTOM OBJECT, Anything
    GETLIST_APPCRT        - Application CRT Window - XbpCrt()
    GETLIST_APPEDIT       - Application EDIT Window - APPEDIT
    GETLIST_APPBROWSE     - Application BROWSE Window - APPBROWSE
    GETLIST_APPFIELD      - Application FIELD - APPFIELD
    GETLIST_DIRTREE       - Directory Tree DIALOG - XbpTreeView()
    GETLIST_STATUS        - Status Bar
    GETLIST_PROGRESS      - Progress Bar, XbpStatic()
    GETLIST_HOTKEY        - Hot Key accelerator
    GETLIST_PICKLIST      - Special Pick-List Dialog
    GETLIST_USER          - User-Defined Commands (Start Offset)
    GETLIST_DATASTORE     - Reserved for storage of Static Data
nGETLIST_SUBTYPE

This is used only on objects that have a sub-type such as DCSTATIC objects. Sub-Types are usually defined in XBP.CH.

cGETLIST_CAPTION

This is used on objects that have a caption like DCSTATIC, DCPUSHBUTTON, DCTABPAGE, etc.

bGETLIST_VAR

This is an "anchor" code block which is usually used as the :datalink to the Xbase part. The function DC_GetAnchorCB() is used to create a code block which provides access to a local memory variable. For example, if this object were a GET, the code block may look like this:
{|x| IIf( PCount()==0 , uVar, uVar:=x) }
It is also be used to provide a link to a local array which is used for list-boxes, combo-boxes, etc.

nGETLIST_STARTROW

This is the Start Row of the object, relative to 0 (top) of the parent object, unless bGETLIST_RELATIVE contains a code-block. If lGETLIST_PIXEL is .TRUE. then this should be in pixels, otherwise it is in text-based coordinates.

nGETLIST_STARTCOL

This is the Start Column of the object, relative to 0 (left) of the parent object, unless bGETLIST_RELATIVE contains a code-block. If lGETLIST_PIXEL is .TRUE. then this should be in pixels, otherwise it is in text-based coordinates.

nGETLIST_ENDROW

This is the End Row of the object, relative to 0 (top) of the parent object. If lGETLIST_PIXEL is .TRUE. then this should be in pixels, otherwise it is in text-based coordinates.

nGETLIST_ENDCOL

This is the End Column of the object, relative to 0 (left) of the parent object. If lGETLIST_PIXEL is .TRUE. then this should be in pixels, otherwise it is in text-based coordinates.

nGETLIST_WIDTH

This is the Width of the object. If lGETLIST_PIXEL is .TRUE. then this should be in pixels, otherwise it is a text-based number.

nGETLIST_HEIGHT

This is the Height of the object. If lGETLIST_PIXEL is .TRUE. then this should be in pixels, otherwise it is a text-based number.

oGETLIST_FONT

The is the Font. If left empty, then the font is inherited from the parent. This may be a font object created by XbpFont() or the name of a font.

cGETLIST_PICTURE

This is a picture clause and is used only with GET objects to define the picture of the GET, ex: "99/99/9999".

bGETLIST_WHEN

This is a code block which is evaluated after other events which may affect memory variables or database pointers in the application. If the return value of the code block is .FALSE. then the object is disabled with the :disable() method. If the return value of the code block is .TRUE. then the object is enabled with the :enable() method.

bGETLIST_VALID

This is a code block which is evaluated when the object loses focus. If the return value is .FALSE. then the object remains in focus.

cGETLIST_TOOLTIP

This is a message to display in a "tool-tip" when the mouse is placed over the object.

xGETLIST_CARGO

This is any kind of data you wish to to add to the :cargo container for this object. The cargo may be accessed at any time via the :cargo exported variable, however it will not be stored in the same manner it is entered. The :cargo container of DCDIALOG objects is an array of at least three elements:
  1. [1] - A numeric value that is a pointer to the Getlist array for this object.
  2. [2] - A pointer to the GetList array.
  3. [3] - The value of .
  4. [4] and up - optional values added by the GUI reader.


aGETLIST_PRESENTATION

This is an array of presentation parameters that conforms to the standard presentation specification for Xbase parts. This is a two dimensional array. If specified, this contains presentation parameters for the Xbase Parts object. The first column of the array must contain #define constants from the XBP.CH file starting with the prefix XBP_PP_. These constants define the presentation parameters that can be set for the Xbase Parts object and include colors and fonts. The second column of the array contains the value for each setting. These are usually set using #define constantse from the GRA.CH file.

bGETLIST_ACTION

This is a code block that is evaluated by some Xbase Parts objects which cause an action, such as a Push-Button or a menu item.

oGETLIST_OBJECT

This is reserved. It is an "anchor" code block which is used to assign a local variable to the Xbase Part after it is created by DC_ReadGui().

xGETLIST_ORIGVALUE

This is an empty container which is used by DC_ReadGui() to store the original value of the variable which has been data-linked to the Xbase Parts object.

xGETLIST_OPTIONS

This is used to pass optional parameters to the Xbase Part. Some objects have additional features, such as the :spinFast option of a SPIN BUTTON. Features that are unique to an object are handled here.

aGETLIST_COLOR

This is a two-element array consisting of a ForeGround and a BackGround color for the object. By default, the object inherits the colors of the parent object. The values in the array my be color-strings that conform to SetColor() type colors or numerical values that conform to GRA_* type colors defined in GRA.CH.

The colors may also be set via aGETLIST_PRESENTATION.

cGETLIST_MESSAGE

This is the text of a message which is display in a DCMESSAGEBOX object when this object has focus.

cGETLIST_HELPCODE

This is the help code that is passed to DC_HelpF1() "in text mode" or to a *.HLP file "in GUI mode" if the F1 (Help) key is pressed when this object is in focus. In GUI mode, the GET option HELPFILE is required to identify the Windows .HLP file containing the context help.

cGETLIST_VARNAME

This is the character string name of the variable that has been data-linked to the object via bGETLIST_VAR

bGETLIST_GROUP

This is an "anchor" code block which is used to assign a local variable to the Xbase Part after it is created by DC_ReadGui(). The local variable is used as a pointer to this object when it it needed as a "parent" to other objects.

nGETLIST_POINTER

This is a numeric pointer to another GetList item which may be associated with this item. The number is a GetList sub-array. For example, a GETLIST_GET item is associated with a GETLIST_SAY item.

bGETLIST_PARENT

This is an "anchor" code block which is used to assign a local variable to the Xbase Part after it is created by DC_ReadGui(). The local variable is used as a pointer to the "parent" object which this object attaches itself to.

bGETLIST_REFVAR

This is an "anchor" code block which is usually used as the :datalink to the Xbase part. The function DC_GetAnchorCB() is used to create a code block which provides access to a local memory variable. This is used only when bGETLIST_VAR is used for another purpose such as a pointer to an array for a LIST BOX.

lGETLIST_PROTECT

This is used on objects that get and display information. If this is .TRUE. then the data is protected from editing.

lGETLIST_PIXEL

This is used to establish whether coordinate and size parameters are in PIXELS or text-based. If this is .FALSE., then the coordinate and size parameters are converted to pixel-based by the GUI reader.

nGETLIST_CURSOR

This is the resource id of an alternate mouse cursor to use for this object.

bGETLIST_EVAL

This code-block is evaluated as the "last step" after an object is created by the GUI reader. The object is passed to the code block. It may be used for anything that may be required during the intialization process of an object by the GUI reader. For example, let's say we have a static object that displays a color using GraBox(). We want to insure that the object is refilled with the color whenever it is repainted. We accomplish this with the following codeblock:

{|o|o:paint := {|o|RGB(o,nRed,nGreen,nBlue)} }

bGETLIST_RELATIVE

This is an "anchor" code block which is used to assign a local variable to the Xbase Part after it is created by DC_ReadGui(). The local variable is used as a pointer to a "relative" object which contains the master coordinates. If this codeblock evaluates to an object type, then the object's current position coordinates are used to locate this object and the start column / start row are relative to the start coordinates of the object. RELATIVE addressing is recommended because dialog screen objects usually have a positional relationship to each other on the screen and designing screens is much faster when a master object is moved and all other objects move in relationship to the master.

aGETLIST_OPTIONS1 - aGETLIST_OPTIONS9

These elements are provided for special use as required by each class of object.

aGETLIST_LEVEL

This is an optional character string used for "sorting" the Getlist and to establish a heirarchy tree for GetList items. It is used by the DIALOG editor to associate parent items with child items.

aGETLIST_TITLE

This is an optional character string used as a "title" for the Getlist object. It is used by the DIALOG editor when displaying a Tree-style browse of the Get List.

cGETLIST_ACCESS

This is an optional "lock code" to apply to the Getlist item. It is used by the GUI reader to provide lock and key access to individual items in the Getlist dependent on user access rights.

bGETLIST_COMPILE

This is an optional code block which is evaluated at the time the GetList is loaded by the reader. If the code block returns a .TRUE., then this item is added to the dialog window, If the code block returns a .FALSE. then this item is ignored. Use this option when it is necessary to have "conditional compiling" capability in applications.

cGETLIST_ID

This is a unique ID for the GetList item.

dGETLIST_REVDATE

This is the last revised date for this object in the Getlist. It is used by the DIALOG editor.

cGETLIST_REVTIME

This is the last revised time for this object in the Getlist. It is used by the DIALOG editor.

cGETLIST_REVUSER

This is the USER ID of the user or programmer who last revised this Getlist item via the DIALOG editor.

bGETLIST_HIDE

This is a code block which is evaluated after other events which may affect memory variables or database pointers in the application. If the return value of the code block is .FALSE. then the object is hidden with the :hide() method. If the return value of the code block is .TRUE. then the object is displayed with the :show() method.

nGETLIST_ACCELKEY

This is a numeric value equal to xbeK_* numbers that define keyboard keys in appevent.ch. Pressing the associated key will either perform the action associated with the object for objects like PushButtons, Check-Boxes, Radio-Buttons, Menu Items, etc. or will set input focus to the associated object if the object is not an action object.

bGETLIST_GOTFOCUS

This optional code block is evaluated when this object receives input focus.

bGETLIST_LOSTFOCUS

This optional code block is evaluated when this object loses input focus.

lGETLIST_TABSTOP

This logical value when .TRUE. will cause the object (Xbase Part) to be set when using the Tab key. Note: When logical groups are defined in a dialog using TABGROUP , TABSTOP is usually set to .T. (true) for the first dialog element in the group. This allows the first dialog element of a group to be activated using the Tab key.

nGETLIST_TABGROUP

This option numeric value allows objects (Xbase Parts) that are created immediately after one another in the GetList to be combined into logical groups. This allows the user to navigate within a dialog from group to group by pressing the Tab key. Within a group of dialog elements navigation is performed using the cursor keys. The numeric values conform to tab group values defined in XBP.CH.

lGETLIST_VISIBLE

This option determines whether the object is visible immediately after the call to the method :create() . By default the object follows the VISIBLE clause of the DCGETOPTIONS command. Use the INVISIBLE clause to override the VISIBLE clause of DCGETOPTIONS

cGETLIST_GETGROUP

This option is a "group" name (case-sensitive) to assign to this GetList object. Assignment to a group makes it possible to perform operations only on GetList items which are members of a specified group, using functions such as DC_GetRefresh() or DC_ReadGui().

lGETLIST_FLAG

This logical element is reserved for the form designer.

The GetOptions Array



The Dialog Options List is simply a single-dimensionsal array of 52 elements. Constants are defined in DCDIALOG.CH. The elements of the options array are defined as follows:

cGETOPT_NAME

This is the name of the Dialog screen or application. It must be all Upper Case characters no longer than characters in length. It is a unique name assigned to the dialog for saving to the dCLIP++ DIALOG dictionary database.

cGETOPT_TITLE

This is the title or description of the dialog.

nGETOPT_WNDHEIGHT

This is the dialog window height in pixels.

nGETOPT_WNDWIDTH

This is the dialog window width in pixels.

nGETOPT_ROWSPACE

This is the number of pixels between rows when using text-based coordinates in the Getlist. The default is 20.

nGETOPT_SAYWIDTH

This is the default width for DCSAY objects if the SAYSIZE parameter is not used with the DCSAY command.

cGETOPT_SAYFONT

This is the default FONT for DCSAY objects if the FONT parameter is not used with the DCSAY command. If this parameter is NIL, then the DCSAY object uses the font of the parent.

cGETOPT_GETFONT

This is the default FONT for DCGET and @..DCSAY..GET objects if the FONT parameter is not used with the DCGET or @..DCSAY..GET commands. If this parameter is NIL, then the object uses the font of the parent.

nGETOPT_GETHEIGHT

This is the default HEIGHT (in pixels) of the GET in DCGET and @..DCSAY..GET commands. If this parameter is nil, then the default height is 20.

aGETOPT_BUTTONS

This is an optional multi-dimensional array of buttons to add to the bottom of the dialog box. Each button will be added in sequence from left to right. Each sub-array is an array of 5 elements defining the buttons.
  Element      Type       Description
  -------      ----       ------------------------------------
     1          C         Button Caption
     2          N         Button Width (in pixels)
     3          N         Button Height (in pixels)
     4          B         Action Code Block
     5          X         Button Cargo
nGETOPT_WNDROW

This is the starting row of the dialog window (in pixels). If this element is NIL, then the dialog will be centered vertically in the parent.

nGETOPT_WNDCOL

This is the starting column of the dialog window (in pixels). If this element is NIL, then the dialog will be centered horizontally in the parent.

nGETOPT_ROWOFFSET

This is the row offset (in pixels) to add to each dialog object. The default is 0.

nGETOPT_COLOFFSET

This is the column offset (in pixels) to add to each dialog object. The default is 0.

lGETOPT_DESIGN

If this element is .TRUE., then the user can move and resize objects on the screen. Objects that are moved and resized are saved back in the GetList array.

aGETOPT_MENU

This is a multi-dimensional array containing an optional menu to attach to the top of the dialog window. The menu must conform to the specifications of menus returned by the dCLIP menu dictionary system. See DC_MenuLoad().

lGETOPT_PIXEL

If this element is .TRUE., then all coordinates in the Getlist are in pixels, otherwise they are text-based coordinates.

xGETOPT_SPARE

Spare.

nGETOPT_ICON

This is the resource ID of the ICON to place in the upper left corner of the dialog window.

lGETOPT_CHECKGET

If this element is .TRUE., then all DCGET or @..DCSAY..GET objects which GET a logical value will be displayed as a CheckBox rather than a normal GET.

cGETOPT_HELPFILE

This is the name of a *.HLP help file to activate when the user clicks on the HELP button on the bottom of the dialog window.

lGETOPT_VISIBLE

If this element is .TRUE., then the dialog window will be displayed before objects are created on the window, otherwise the entire dialog will not be displayed until all objects are created.

lGETOPT_TRANSLATE

If this element is .TRUE., then it is assumed that all coordinates in the GetList are based on 0,0 being the upper left home position of the display or parent object. It is also assumed that coordinates are text-based unless individually specified as pixel-based by the aGETLIST_PIXEL. A .TRUE. will also insure that RELATIVE coordinates are based on the coordinates of the relative objects specified in the Getlist. If this element is .FALSE., then NO TRANLATIONS will occur when the objects are displayed by the GUI reader and all coordinates are assumed to be based on 0,0 being the lower left home position of the display or parent object and are all pixel-based. After the GUI reader translates the coordinates, it writes the translated coordinates back to the Getlist and automatically sets this flag to .FALSE. so that successive calls to the GUI reader with the translated Getlist will not cause anomolies when displaying the objects.

lGETOPT_SAYRIGHT

If this element is .TRUE. then all GETLIST_SAY type objects in the Getlist will display their text right-justified, unless that object specificies LEFT or CENTER justification via the aGETLIST_OPTIONS element of the Getlist item.

nGETOPT_BITMAP

This is the resource ID of a Bitmap that is linked into the .EXE. This bitmap will be sized to fill the main dialog window.

aGETOPT_PRESENT

The PRESENTATION option is a two-dimensional array that contains presentation parameters for the Dialog window. The first column of the array must contain #define constants from the XBP.CH file starting with the prefix XBP_PP_. These constants define the presentation parameters that can be set for the object and include colors and fonts. The second column of the array contains the value for each setting. These are also generally set using #define constants from the GRA.CH file.

nGETOPT_BGCOLOR

This option is a numeric value defining the background color of the dialog windows. Colors start with GRA_CLR_* (defined in GRA.CH.

nGETOPT_SAYOPT

Objects created by the @DCSAY command can be given a set of options which are used to align the text within the rectangular area of the say (XbpStatic) object. Constants defined in the XBP.CH file are used for these options. These constants are listed in the table below. If combinations of alignment options are required, the sum of the appropriate constants must are assigned to the instance variable :options .
#define constants for aligning text

  Constant                  Description

  XBPSTATIC_TEXT_LEFT       Text is left aligned
  XBPSTATIC_TEXT_CENTER     Text is horizontally centered
  XBPSTATIC_TEXT_RIGHT      Text is right aligned
  XBPSTATIC_TEXT_TOP        Text is displayed at top
  XBPSTATIC_TEXT_VCENTER    Text is vertically centered
  XBPSTATIC_TEXT_BOTTOM     Text is displayed at bottom
bGETOPT_EVAL

This option is a code block that is evaluated after the main dialog window (XbpDialog) is created. This code block is evaluated before any items from the GetList are created and added to the Dialog. The Dialog object is passed to the code block.

nGETOPT_MODALSTATE

This element is used to set the Modal State of the dialog window. Options are XBP_DISP_APPMODAL or XBP_DISP_MODELESS (defined in XBP.CH).

nGETOPT_SAYHEIGHT

This option is the default HEIGHT (in pixels) of the SAY in @..DCSAY and @..DCSAY..GET commands. If this element is empty then the default height is 20.

lGETOPT_MINBUTTON

A logical .TRUE. in this element will suppress the display of the Minimize button on the dialog window.

lGETOPT_MAXBUTTON

A Logical .TRUE. in this element will suppress the display of the Maximize button on the dialog window.

lGETOPT_TABSTOP

A Logical .TRUE. in this element will cause all objects (Xbase Parts) to be set when using the Tab key. The default for TabStop is .False. or OFF. The TabStop setting for individual objects can be force to ON or OFF by using the TABSTOP or NOTABSTOP commands respectively in each command to override the default setting.

lGETOPT_ABORTQUERY

A Logical .TRUE. in this element will cause a Message Box to appear Confirming that the cancelling of operation when the Cancel button is clicked or the Dialog window is closed.

nGETOPT_ROWPIXELS

A numerical value in this element is the amount of pixels that equate to a row when using text-based coordinates. The default is 20.

nGETOPT_COLPIXELS

A numerical value in this element is the amount of pixels that equate to a column when using text-based coordinates. The default is 7.0.

lGETOPT_ESCAPEKEY

A Logical .FALSE. in this element will disable the ESCape key. Normally, the ESCape key will close the dialog window and cancel operation.

cGETOPT_SOURCECODE // feature available in Rev. 2.0

This element contains the name of the source code (*.PRG) file that contains the DC* commands which created the dialog GetList. This is needed when using the pop-up Dialog Editor in DESIGN mode. Changes made to a dialog are re-written to the source code file only if the command is identified by an ID, GETID or SAYID clause.

aGETOPT_TOOLCOLOR

This element contains an optional 2-element array defining the he foreground color and background color of tooltips when using the TOOLTIP clause on eXPress++ commands. The default colors are GRA_CLR_BLACK and GRA_CLR_WHITE.

nGETOPT_BORDER

This element defines the border type of the dialog window. Constants, starting with XBPDLG_* are defined in the XBP.CH file and can be assigned to .

lGETOPT_EXVALID

This element if .TRUE. will traverse the GetList when the user clicks on the OK button and test all validations. If any validation fails, the failed object will receive input focus and the dialog will remain active.

lGETOPT_NOTASKLIST

This element if .TRUE. will insure that the dialog window is not placed on the Windows task bar.

nGETOPT_MINSIZE

This element is used to establish the minimum size of the dialog window, in pixels, if the operator attempts to resize the window.

nGETOPT_MAXSIZE

This element is used to establish the maximum size of the dialog window, in pixels, if the operator attempts to resize the window.

lGETOPT_NORESIZE

This element if .TRUE. will prevent the operator from resizing the dialog window.

lGETOPT_NOTITLEBAR

This element if .TRUE. will prevent a titlebar from displaying on the dialog window. This will also prevent the minimize, maximize, and close buttons from appearing.

lGETOPT_NOMOVE

This element if .TRUE. will prevent the dialog window from automatically moving when it's owner is moved.

nGETOPT_ORIGIN

This element defines the reference point for the position of the dialog window. The value of must be a #define constant from the file XBP.CH.

nGETOPT_HILITEGETS

This element defines the color of a box that will be drawn around the currently selected GET. The numeric value must be a GRA_CLR_* color defined in GRA.CH. This clause highlights only the GET portion of @.DCSAY..GET commands and DCGET commands. The feature is disabled if this element is a NIL.

lGETOPT_SUPERVISE

This element if .FALSE. will cause the ENTER key to move from the last get of a group of gets to the first get of the next group of gets. Default behavior (.TRUE.) is for the movement from the last get to the first get of a group of gets that all have the same parent.

For example, if there are gets on 3 different tab pages, and the user is in the last get of TabPage 1, and ENTER will cause the first get on TabPage 2 to acquire focus.

The Command Definition File


The command file, DCDIALOG.CH contains all the constant defines and all the commands for building the GetList. Creating a robust set of commands also requires a powerful preprocessor. Xbase++ has such a preprocessor, therefore the commands are simple to design but very powerful. Let's look at the definition of a command that adds a Push-Button object to the GetList.
#command @ < nRow >, < nCol > DCPUSHBUTTON [CAPTION < cText >]              ;
                [< fancy:FANCY >]                                           ;
                [SIZE < nWidth >, < nHeight >]                              ;
                [TEXTOFFSET < nHoriz >, < nVert >]                          ;
                [TEXTHEIGHT < nTextHeight >]                                ;
                [ACTION < bAction >]                                        ;
                [WHEN < bWhen >]                                            ;
                [PARENT < oParent >]                                        ;
                [COLOR < ncFgC > [,< ncBgC >]]                              ;
                [MESSAGE < cMsg > [INTO < oMsg >]]                          ;
                [HELPCODE < cHelpCode >]                                    ;
                [FONT < cFont >]                                            ;
                [PRESENTATION < aPres >]                                    ;
                [< p: PIXEL >]                                              ;
                [OBJECT < oObject >]                                        ;
                [TOOLTIP < cToolTip >]                                      ;
                [CURSOR < nCursor >]                                        ;
                [CARGO < xCargo >]                                          ;
                [EVAL < bEval >]                                            ;
                [HIDE < bHide >]                                            ;
                [EDITPROTECT < bProtect >]                                  ;
                [TITLE < cTitle > ]                                         ;
                [RELATIVE < oRel > ]                                        ;
                [ID < cId >]                                                ;
                [ACCELKEY < nAccel >]                                       ;
                [GOTFOCUS < bGotFocus >]                                    ;
                [LOSTFOCUS < bLostFocus >]                                  ;
                [< lTabStop:TABSTOP >]                                      ;
                [< lNoTabStop:NOTABSTOP >]                                  ;
                [TABGROUP < nTabGroup >]                                    ;
                [< lVisible:VISIBLE >]                                      ;
                [< lInvisible:INVISIBLE >]                                  ;
                [GROUP < cGroup >]                                          ;
 =>                                                                         ;
  AADD( GetList,                                                            ;
    { GETLIST_PUSHBUTTON,                       /* nGETLIST_TYPE         */ ;
      nil,                                      /* nGETLIST_SUBTYPE      */ ;
      < cText >,                                /* cGETLIST_CAPTION      */ ;
      nil,                                      /* bGETLIST_VAR          */ ;
      < nRow >,                                 /* nGETLIST_STARTROW     */ ;
      < nCol >,                                 /* nGETLIST_STARTCOL     */ ;
      nil,                                      /* nGETLIST_ENDROW       */ ;
      nil,                                      /* nGETLIST_ENDCOL       */ ;
      < nWidth >,                               /* nGETLIST_WIDTH        */ ;
      < nHeight >,                              /* nGETLIST_HEIGHT       */ ;
      < cFont >,                                /* cGETLIST_FONT         */ ;
      nil,                                      /* cGETLIST_PICTURE      */ ;
      {< bWhen >,< (bWhen) >},                  /* bGETLIST_WHEN         */ ;
      nil,                                      /* bGETLIST_VALID        */ ;
      < cToolTip >,                             /* cGETLIST_TOOLTIP      */ ;
      < xCargo >,                               /* xGETLIST_CARGO        */ ;
      < aPres >,                                /* aGETLIST_PRESENTATION */ ;
      {< bAction >,< (bAction) >},              /* bGETLIST_ACTION       */ ;
      nil,                                      /* oGETLIST_OBJECT       */ ;
      nil,                                      /* xGETLIST_ORIGVALUE    */ ;
      { < nHoriz >,< nVert >,< nTextHeight >,< .fancy. > },                 ;
                                                /* xGETLIST_OPTIONS      */ ;
      [{< ncFgC >,< ncBgC >}],                  /* aGETLIST_COLOR        */ ;
      {< cMsg >,[{DC_GetAnchorCB(@< oMsg >,'O'),< oMsg >,'O'}]},            ;
                                                /* cGETLIST_MESSAGE      */ ;
      < cHelpCode >,                            /* cGETLIST_HELPCODE     */ ;
      nil,                                      /* cGETLIST_VARNAME      */ ;
      nil,                                      /* bGETLIST_READVAR      */ ;
      nil,                                      /* bGETLIST_DELIMVAR     */ ;
      [{DC_GetAnchorCB(@< oObject >,'O'),                                   ;
                        < (oObject) >,'O'}],    /* nGETLIST_GROUP        */ ;
      < nCursor >,                              /* bGETLIST_POINTER      */ ;
      [{DC_GetAnchorCB(@< oParent >,'O'),                                   ;
                        < (oParent) >,'O'}],    /* bGETLIST_PARENT       */ ;
      nil,                                      /* bGETLIST_REFVAR       */ ;
      {< bProtect >,< (bProtect) >},            /* bGETLIST_PROTECT      */ ;
      < .p. >,                                  /* lGETLIST_PIXEL        */ ;
      < nCursor >,                              /* nGETLIST_CURSOR       */ ;
      [{< bEval >,< (bEval) >}],                /* bGETLIST_EVAL         */ ;
      [{DC_GetAnchorCb(@< oRel >,'O'),                                      ;
                        < (oRel) >,'O'}],       /* bGETLIST_RELATIVE     */ ;
      nil,                                      /* xGETLIST_OPTIONS2     */ ;
      nil,                                      /* xGETLIST_OPTIONS3     */ ;
      nil,                                      /* xGETLIST_OPTIONS4     */ ;
      nil,                                      /* xGETLIST_OPTIONS5     */ ;
      nil,                                      /* xGETLIST_OPTIONS6     */ ;
      nil,                                      /* xGETLIST_OPTIONS7     */ ;
      nil,                                      /* xGETLIST_OPTIONS8     */ ;
      nil,                                      /* xGETLIST_OPTIONS9     */ ;
      nil,                                      /* cGETLIST_LEVEL        */ ;
      < cTitle >,                               /* cGETLIST_TITLE        */ ;
      nil,                                      /* cGETLIST_ACCESS       */ ;
      nil,                                      /* bGETLIST_COMPILE      */ ;
      < cId >,                                  /* cGETLIST_ID           */ ;
      nil,                                      /* dGETLIST_REVDATE      */ ;
      nil,                                      /* cGETLIST_REVTIME      */ ;
      nil,                                      /* cGETLIST_REVUSER      */ ;
      {< bHide >,< (bHide) >},                  /* bGETLIST_HIDE         */ ;
      < nAccel >,                               /* nGETLIST_ACCELKEY     */ ;
      {< bGotFocus >,< (bGotFocus) >},          /* bGETLIST_GOTFOCUS     */ ;
      {< bLostFocus >,< (bLostFocus) >},        /* bGETLIST_LOSTFOCUS    */ ;
      [< .lTabStop. >.AND.!< .lNoTabStop. >],   /* lGETLIST_TABSTOP      */ ;
      ,                              /* nGETLIST_TABGROUP     */ ;
      [< .lVisible. >.AND.!< .lInvisible. >],   /* lGETLIST_VISIBLE      */ ;
      < cGroup >,                               /* cGETLIST_GETGROUP     */ ;
      .f.,                                      /* lGETLIST_FLAG         */ ;
    } )

The GUI Reader


The Command layer creates the Getlist/GetOptions arrays which are in turn passed to the GUI reader function or class. eXPress++ uses a function named DC_ReadGui(). The GUI reader receives the GetList as a parameter. This is the only "required" parameter. The GUI Reader is the key to the power of any well-abstracted GUI dialog system. If it is properly designed, the programmer has the flexibility of fine tuning his/her programming style to anything that is suitable or productive. A well-designed GUI reader allows for not only command-based programming, but also array-based and/or data-driven programming. A GUI reader that is designed with an open-architecture also allows for intermingling custom objects and controls with standard Xbase Parts. The GUI reader should allow the programmer to use existing dialog objects as parents or owners of GetList objects, or vice-versa. The GUI reader should allow the programmer to have complete control of the event loop, if required, to handle special cases. The GUI reader should encapsulate all of its variables within its own class architecture or within the GetList to insure that it will function properly in a multi-threaded environment.

The prime directive when designing a GUI reader is this: The reader should be designed to make the task of programming simpler and more intuitive without taking away any power or flexibility of the base language.

DC_ReadGUI() was designed not only to make it easier to create complicated dialogs, but also to give the Clipper programmer a comfortable transition from text-based programming to GUI-based programming. For example, the Xbase++ coordinate system is based on the OS/2 coordinate system with 0,0 being the bottom/left corner and coordinates in pixels. The GUI reader should allow the option of standard Xbase++ coordinates or Clipper coordinates (text-based). If text-based coordinates are used, then it should handle the translation automatically. The reader should also automatically create and destroy a dialog window when it is needed (if no parent is passed) and size the dialog to the items in the GetList. This enhances programming productivity and reduces the amount of code when designing database applications that require lots of pop-up dialog windows.

Here is a description of the parameters available to DC_ReadGUI().

DC_ReadGUI ( aGetList, ;
       [cTitle], ;
       [aGetOptions], ;
       [lnButtons], ;
       [bEventHandler], ;
       [aRef], ;
       [@oDialog], ;
       [lFit], ;
       [lDesignOn], ;
       [lExit], ;
       [oAppWindow], ;
       [lModal], ;
       [bEval], ;
       [lEnterExit],;
       [oOwner], ;
       [xSetFocus], ;
       [ancGroup], ;
       [nTimeOut] ) -> lUpdated
Arguments: < aGetList > is an array containing a list of Get objects to edit. < cTitle > is the title to display on the top of the GUI dialogue box. < aGetOptions > is an array of options for the GUI dialogue box. This is ignored if painting text-based GETS. DCDIALOG.CH contains the defines for this array. < lnButtons > will add extra buttons to the bottom area of the main dialog window. < lnButtons > may be a numeric value designating the buttons to add based on the below table. DCDIALOG.CH contains DCGUI_BUTTON_* constant definitions for the available buttons. These values are summed to enable multiple buttons.
 Numeric Value       Description
 ------------------  ---------------------------------------------
 DCGUI_BUTTON_OK     Will add a button with the caption OK.
                     Selecting this button will exit the GUI reader
                     and save all changes to their associated
                     memory variables.

 DCGUI_BUTTON_CANCEL Will add a button with the caption CANCEL.
                     Selecting this button will exit the GUI reader
                     and restore all memory variables to their
                     original value.

 DCGUI_BUTTON_EXIT   Same as DCGUI_BUTTON_OK except the caption is
                     labeled as EXIT.
Optionally, < lnButtons > may be a logical .TRUE. This will paint both the OK and CANCEL buttons.

< bEventHandler > is a code block which points to a custom event handler. DC_ReadGui() uses it's own, default event handler to manage the objects during the running of the program. The default event handler makes a call to the custom event handler before processing it's default events. The custom event handler uses Appevent() to get information on the current event and passes the following parameters:
  1. nEvent   - The event type.
  2. mp1      - The first event parameter.
  3. mp2      - The second event parameter.
  4. oXbp     - A pointer to the object creating the event.
  5. oDlg     - A pointer to the main dialogue object.
  6. aGetlist - A pointer to the GetList array.
  7. aRef     - A pointer to an optional Reference array that
                was passed to DC_ReadGets().
  8. lOk      - A logical value that is .TRUE. if the OK button
                was clicked and .FALSE. if CANCEL was clicked.
The code block should look like this:
  bEventHandler := {|a,b,c,d,e,f,g,h|MyHandler(a,b,c,d,e,f,g,h)}
The Function should look like this:
  STATIC FUNCTION MyHandler ( nEvent, mp1, mp2, oXbp, oDlg, aGetlist, aRef,
                              lOk )

  /* custom code */

  RETURN DCGUI_NONE
The function must return a numeric value which controls the behavior of the event loop as follows:
  Return Value          Behavior
  ------------          --------------------------------------------
  DCGUI_NONE            Continue processing the event
  DCGUI_IGNORE          Ignore event
  DCGUI_CLEAR           Ignore event and clear all events from queue
  DCGUI_EXIT_OK         Exit the DC_ReadGui() reader event loop
                        and return a logical .TRUE.
  DCGUI_EXIT_ABORT      Exit the DC_ReadGui() reader event loop,
                        restore all referenced memvar to their
                        original value and return a logical .FALSE.

  DCGUI_MOVE_UP         Set focus to previous object in Get List
  DCGUI_MOVE_DOWN       Set focus to next object in Get List
  DCGUI_MOVE_TOP        Set focus to first object in Get List
  DCGUI_MOVE_BOTTOM     Set focus to last object in Get List
  DCGUI_MOVE_UP_PAR     Set focus to previous object in Get List that
                        has the same parent as the current object.
  DCGUI_MOVE_DOWN_PAR   Set focus to next object in Get List that
                        has the same parent as the current object.
  DCGUI_MOVE_TOP_PAR    Set focus to first object in Get List that
                        has the same parent as the current object.
  DCGUI_MOVE_BOTTOM_PAR Set focus to last object in Get List that
                        has the same parent as the current object.
< aRef > is any array. Programming dialogue systems that must manipulate many variables is much simpler if all the variables are placed into a single array and then passed to the dialogue system and its event handler.

@< oDialog > is a reference to a parent dialog or to a memory variable to store a reference to the dialog that will be created. If < oDialog > has already been created as an XbpDialog class object, it will become the parent for all the objects in the GetList. If < oDialog > is passed by reference as a NIL, the dialog object created by the reader will be stored in this memory variable.

The parent of the dialog that will be created is referenced by < oAppWindow >. If no < oAppWindow > is passed then the parent of the created dialog will be AppDeskTop().

< lFit > if .TRUE. will automatically form the dialog screen to fit comfortably around the objects within the dialog. This option allows the programmer to use coordinates on dialog objects for relative addressing only without worrying about how the objects relate to the borders of the main dialog. After all the objects are created, the main dialog borders will be sized to fit the objects. The default is .FALSE.

< lDesignOn > will add a button to the bottom of the dialog to click on for enabling DESIGN mode. In design mode, objects may be resized or moved with the mouse and the new sizes and coordinates are written back to the GetList array. This feature is useful for writing data-driven dialog systems.

< lExit > if .TRUE. will exit the reader after the objects are created and not enter the reader's event loop. It is the responsibility of the programmer to insure that events are managed and the dialog is destroyed. This feature is used to add more objects to an existing dialog, or to create a dialog that will be managed by an external event loop.

< oAppWindow > is the parent object for the main dialog. If this argument is not passed, then the AppDeskTop() will be used.

< lModal >, if .TRUE. will set this dialog modal state to MODAL - APPLICATION WIDE. If .FALSE. the dialog will be NONMODAL. The default is .FALSE.

< bEval > is an optional code block to evaluate after all the objects are created and just before entering the event loop. The main dialog object is passed to this code block as an argument.

< lEnterExit > if .TRUE. will exit the dialog if the ENTER key is pressed in the "last GET". If .FALSE. (default), then pressing the ENTER key in the last GET will cause the first GET to receive focus.

< lArrayTranslate > if .TRUE. will translate the HEIGHT, WIDTH, START COLUMN and START ROW elements in the GETLIST array to pixel coordinates and write these coordinates back to the original GETLIST array. It will also write the translated Dialog window coordinates to the GETOPTIONS array and set the TRANSLATE flag in the GETOPTIONS array to .FALSE.

< xSetFocus > is the object that receives focus when entering the event handler. < xSetFocus > may be a type "O" (object), a type "N" (numeric) or a type "C" character. If < xSetFocus > is a type "N" then the numeric value must be a number from 1 to the length of < aGetList >. This is an ordinal pointer to the GetList item. If < xSetFocus > is a type "C" then the character string value must be a value equivalent to the ID of the item in the GetList which is to receive focus.

< ancGroup > is the GetList group to create. Each item in the GetList may be assigned a group name or number via the GROUP < ancGroup > clause. If a numeric or character value is passed then only the items in the GetList which match < ancGroup > will be created. < ancGroup > may consist of an array of numeric values or character values so that more than one group may be created. If < ancGroup > is a nil then all items in the GetList will be refreshed.

The dialogue event loop will automatically be terminated and the dialog will be destroyed after < nTimeOut > seconds with no events.

Returns:

.TRUE. if any gets were updated, .FALSE. otherwise.

A robust GUI reader consists of thousands of lines of code. It is not practical to include all the code here, so much of it has been removed and only the code needed to describe the basics is shown below. The complete source code is included in the eXPress++ retail package.

/* ---- define the STATIC data needed to add to the GetList ---- */

#define aOPTIONARRAY       aGetList[nDataStore,xGETLIST_OPTIONS]
#define aEDITCONTROLS      aGetList[nDataStore,xGETLIST_OPTIONS2]
#define aBROWSECONTROLS    aGetList[nDataStore,xGETLIST_OPTIONS4]
#define aTABPAGECONTROLS   aGetList[nDataStore,xGETLIST_OPTIONS5]
#define aKEYBOARDCONTROLS  aGetList[nDataStore,xGETLIST_OPTIONS8]
#define aTOOLTIPCONTROLS   aGetList[nDataStore,xGETLIST_OPTIONS9]
...more static data...

FUNCTION DC_ReadGUI(  )

/* ---- Use only locals ---- */
LOCAL 

/* ---- Default all unpassed parameters ---- */
DEFAULT cTitle   := '', aOptions := {}, lFit := .f., ... more

/* ---- Default all elements of the GetOptions array ---- */
ASIZE(aOptions,nGETOPT_ARRAY_SIZE)
DEFAULT aOptions[cGETOPT_TITLE] := ''
DEFAULT aOptions[nGETOPT_ROWSPACE]   := 20 // (DC_Font(2):nHeight/2)+4
DEFAULT aOptions[nGETOPT_GETHEIGHT]  := 20
DEFAULT aOptions[nGETOPT_SAYHEIGHT]  := 20
DEFAULT aOptions[lGETOPT_PIXEL]      := .f.
...more defaults...

BEGIN SEQUENCE

/* ---- Create a Dialog Window if no Parent is passed ---- */
IF Empty(oParentDlg)
  nWindowHeight := aOptions[nGETOPT_WNDHEIGHT]
  nWindowWidth := aOptions[nGETOPT_WNDWIDTH]
  DEFAULT nWindowHeight := 450
  DEFAULT nWindowWidth := 600
  oParentDlg := XbpDialog():new( oAppWindow, oOwner, ;
        { nCol, nRow }, ;
        { nWindowWidth, nWindowHeight }, ;
        aOptions[aGETOPT_PRESENT], .f. )
  oParentDlg:taskList := !aOptions[lGETOPT_NOTASKLIST]
  oParentDlg:title := aOptions[cGETOPT_TITLE]
  drawingArea := oParentDlg:drawingArea
  ...more assignments (from options array) ...
ELSEIF oParentDlg:isDerivedFrom('XbpDialog')
  drawingArea := oParentDlg:drawingArea
ELSE
  drawingArea := oParentDlg
ENDIF

/* ---- Add a menu if included in options array ---- */
IF !Empty(aOptions[cGETOPT_MENU])
  DC_MenuOSys( aOptions[cGETOPT_MENU], oParentDlg, .f. )
ENDIF

/* ---- Add a hook to Help file if included in options array ---- */
IF !Empty(aOptions[cGETOPT_HELPFILE])
  oXbpHelp := XbpHelp():New( , aOptions[cGETOPT_HELPFILE] )
  oXbpHelp:Create()
ENDIF

/* ---- Add an item to the Getlist for storage of static data ---- */
nDataStore := AScan(aGetList,{|a|a[nGETLIST_TYPE]==GETLIST_DATASTORE})
IF nDataStore = 0
  ASize(aGetList,Len(aGetList)+1)
  nDataStore := Len(aGetList)
  aGetList[nDataStore] := DC_GetTemplate()
  aGetList[nDataStore,nGETLIST_TYPE] := GETLIST_DATASTORE
  aGetList[nDataStore,xGETLIST_CARGO] := ;
    { nil, nil, .t., .t., .f., nil, nil, .f., {.f., nil },  .f., .f., .t., ;
      oParentDlg, DCGUI_NONE, nil, {.f., nil }, .t. }
ENDIF
aOPTIONARRAY      := AClone(aOptions)
aEDITCONTROLS     := {}
aBROWSECONTROLS   := {}
aKEYBOARDCONTROLS := {}
aTABPAGECONTROLS  := {}
aTOOLTIPCONTROLS  := {}

/* ---- Special error handler while building objects from GetList ---- */
bErrorBlock := ErrorBlock({|e|_DCReadGuiError(e,aGetList,i,bErrorBlock)})

/* ---- Traverse the GetList and create the objects ---- */

FOR i := 1 TO Len(aGetList)

  ASIZE(aGetList[i],nGET_ARRAY_SIZE)

  nType := aGetlist[i,nGETLIST_TYPE]

  oParent := nil
  // DC_GetBlock() compiles string to block if passed as a string.
  bBlock := DC_GetBlock(aGetList[i,bGETLIST_PARENT],'O')

  IF Valtype(bBlock) = 'B'
    oParent := Eval( bBlock )
    oOptions := oParent
  ENDIF
  IF oParent == NIL
    oParent := drawingArea
    oOptions := aOptions
  ENDIF

  cToolTip  := aGetList[i,cGETLIST_TOOLTIP]
  cHelpCode := aGetList[i,cGETLIST_HELPCODE]
  nCursor   := aGetList[i,nGETLIST_CURSOR]
  xOptions  := aGetList[i,xGETLIST_OPTIONS]

  IF !Empty(cToolTip)
    AADD(aTOOLTIPCONTROLS,cToolTip)
    nToolTip := Len(aTOOLTIPCONTROLS)
  ENDIF

  IF nType = GETLIST_MENUBAR .OR. nType = GETLIST_SUBMENU .OR. ;
     nType = GETLIST_MENUITEM .OR. nType = GETLIST_HOTKEY

    IF nType = GETLIST_MENUBAR

      oXbp := XbpMenuBar():new( oParentDlg ):create()
      bBlock := DC_GetBlock(aGetList[i,bGETLIST_GROUP],'O')
      Eval( bBlock, oXbp )
      oXbp:cargo := { i, aGetlist, aGetlist[i,xGETLIST_CARGO], {}, {}, {} }

    ELSEIF nType = GETLIST_SUBMENU
      ... Submenu code ...

    ELSEIF nType = GETLIST_MENUITEM
      ... Menuitem code ...

    ELSEIF nType = GETLIST_HOTKEY

      ... Hotkey code ...
    ENDIF

    LOOP

  ENDIF

  lAutoSize := .f.

  /* --- Get pointer to any other object in GetList --- */
  nPointer := aGetList[i,nGETLIST_POINTER]

  /* --- Display object after created if Visible --- */
  lVisible := aGetList[i,lGETLIST_VISIBLE]

  /* --- Use pixel coordinates if PIXEL option --- */
  lPixel := aGetList[i,lGETLIST_PIXEL] .OR. ;
            IIF(Valtype(aOptions[lGETOPT_PIXEL])='L', ;
                        aOptions[lGETOPT_PIXEL], .f. )

  /* --- Get coordinates of Relative object if RELATIVE clause is used --- */
  bBlock := DC_GetBlock(aGetList[i,bGETLIST_RELATIVE],'O')
  IF Valtype( bBlock ) = 'B'
    oRel := Eval( bBlock )
    nRelStartCol := oRel:CurrentPos()[1]
    nRelStartRow := oRel:CurrentPos()[2] + oRel:CurrentSize()[2]
  ELSE
    nRelStartCol := 0
    nRelStartRow := 0
    oRel := nil
  ENDIF

  /* --- Store away original value of variable (if Get/Set codeblock) --- */
  IF Valtype(aGetList[i,bGETLIST_VAR])$'AB'
    bBlock := DC_GetBlock(aGetList[i,bGETLIST_VAR])
    IF Valtype(bBlock) = 'B'
      aGetList[i,xGETLIST_ORIGVALUE] := Eval( bBlock )
    ENDIF
  ENDIF

  /* --- Translate text-based to pixel-based --- */
  ...Create values for nWidth, nHeight, nStartRow, nStartCol


  /* --- Now create object for each type in GetList --- */

  IF nType == GETLIST_SAY  // Create XbpStatic() from Getlist parameters

    cCaption := aGetList[i,cGETLIST_CAPTION]
    IF Valtype(cCaption) = 'B'
      cCaption := DC_XtoC(Eval(cCaption))
    ELSEIF Valtype(cCaption) # 'C'
      cCaption := DC_XtoC(cCaption)
    ENDIF
    IF !Empty(aGetList[i,cGETLIST_PICTURE])
      cCaption := Transform(cCaption,aGetList[i,cGETLIST_PICTURE])
    ENDIF
    DEFAULT nWidth := LEN(cCaption) * nColPixels
    DEFAULT nHeight := nRowPixels
    IF !Empty(aOptions[cGETOPT_SAYFONT])
      DEFAULT aGetlist[i,cGETLIST_FONT] := aOptions[cGETOPT_SAYFONT]
    ENDIF

    oXbp := XbpStatic():new( oParent, , ;
            { nStartCol, nStartRow}, { nWidth, nHeight }, ;
            aGetList[i,aGETLIST_PRESENTATION], lVisible )

    bBlock := DC_GetBlock(aGetlist[i,bGETLIST_VAR])

    oXbp:cargo := { i, ;
                    aGetlist, ;
                    aGetlist[i,xGETLIST_CARGO], ;
                    bBlock, ;  // Variable
                    DC_GetBlock(aGetList[i,bGETLIST_WHEN]), ;
                    DC_GetBlock(aGetList[i,bGETLIST_HIDE]), ;
                    aGetList[i,cGETLIST_CAPTION], ;
                    aGetList[i,cGETLIST_PICTURE] }

    IF Valtype(aOptions[nGETOPT_SAYOPT]) = 'N'
      oXbp:options := aOptions[nGETOPT_SAYOPT]
    ENDIF
    IF !Empty(aGetlist[i,cGETLIST_FONT])
      oXbp:SetFontCompoundName( aGetlist[i,cGETLIST_FONT] )
    ENDIF
    oXbp:caption := cCaption
    oXbp:clipSiblings := .T.
    oXbp:type    := aGetList[i,nGETLIST_SUBTYPE]
    oXbp:create()
    _SetColor( aGetlist[i,aGETLIST_COLOR], oXbp, oParent, aGetlist[i], .t. )

    /* --- Store Object to LOCAL variable defined in GetList --- */
    bBlock := DC_GetBlock(aGetList[i,bGETLIST_GROUP],'O')
    IF Valtype(bBlock) = 'B'
      Eval( bBlock, oXbp )
    ENDIF

  ELSEIF nType == GETLIST_GET
    ... Create XbpGet() object from GetList parameters

  ELSEIF nType == GETLIST_DIALOG
    ... Create XbpDialog() object from GetList parameters

  ELSEIF nType == GETLIST_STATIC      // XbpStatic()
  ELSEIF nType == GETLIST_MLE         // XbpMle()
  ELSEIF nType == GETLIST_COMBOBOX    // XbpComboBox()
  ELSEIF nType == GETLIST_CHECKBOX    // XbpCheckBox()
  ELSEIF nType == GETLIST_3STATE      // Xbp3State()
  ELSEIF nType == GETLIST_PUSHBUTTON  // XbpPushButton()
  ELSEIF nType == GETLIST_TOOLBAR     // XbpStatic() - toolbar
  ELSEIF nType == GETLIST_ADDBUTTON   // XbpPushButton() - add to toolbar
  ELSEIF nType == GETLIST_RADIOBUTTON // XbpRadioButton()
  ELSEIF nType == GETLIST_TABPAGE     // XbpTabPage()
  ELSEIF nType == GETLIST_LISTBOX     // XbpListBox()
  ELSEIF nType == GETLIST_BITMAP      // XbpBitMap()
  ELSEIF nType == GETLIST_SPINBUTTON  // XbpSpinButton()
  ELSEIF nType == GETLIST_SCROLLBAR   // XbpScrollBar()
  ELSEIF nType == GETLIST_BROWSE      // XbpBrowse()
  ELSEIF nType == GETLIST_BROWSECOL   // XbpColumn()
  ELSEIF nType == GETLIST_MESSAGEBOX  // XbpStatic() - message box
  ELSEIF nType == GETLIST_PROGRESS    // XbpStatic() - progress bar
  ELSEIF nType == GETLIST_APPCRT      // XbpCrt()
  ELSEIF nType == GETLIST_CUSTOM .OR. nType >= GETLIST_USER  // Custom Class
    oXbp := Eval( DC_GetBlock(aGetList[i,bGETLIST_REFVAR], ;
                  bBlock, { aGetList, i, oParent, ;
                 { nStartCol, nStartRow }, { nWidth, nHeight }, ;
                  aGetlist[i,aGETLIST_PRESENTATION], lVisible } )
    ...more code...
  ELSEIF nType == GETLIST_DIRTREE     // XbpDirTree()
  ELSEIF nType == GETLIST_TREEARRAY   // XbpTreeArray()
  ELSEIF nType == GETLIST_PICKLIST    // XbpPickList()
  ELSEIF nType == GETLIST_APPEDIT     // App Edit
  ELSEIF nType == GETLIST_APPBROWSE   // App Browse
  ELSEIF nType == GETLIST_APPFIELD    // App Field
  ELSEIF ... more types ...
  ENDIF

  /* --- Store object pointer to current GetList element --- */
  aGetlist[i,oGETLIST_OBJECT] := oXbp

  /* --- Add Tooltip to this object --- */
  IF !Empty(cToolTip)
    oXbp:HelpLink := MagicHelpLabel():New(nToolTip)
  ELSEIF !Empty(cHelpCode)
    oXbp:HelpLink := XbpHelpLabel():New( cHelpCode )
    oXbp:HelpLink:HelpObject := oXbpHelp
  ENDIF

  /* --- Set Mouse Pointer type for this object --- */
  IF Valtype(nCursor) $ 'NA'
    IF Valtype(nCursor) = 'A'
      ASize(nCursor,3)
      DEFAULT nCursor[2] := 0
      DEFAULT nCursor[3] := XBPWINDOW_POINTERTYPE_POINTER
      oXbp:SetPointer( nCursor[1], nCursor[2], nCursor[3] )
    ELSEIF nCursor > 3
      oXbp:SetPointer(,nCursor,XBPWINDOW_POINTERTYPE_POINTER)
    ELSE
      oXbp:Setpointer(,nCursor,nCursor)
    ENDIF
  ENDIF

  /* --- Evaluate any special code block after object is created --- */
  bBlock := DC_GetBlock(aGetList[i,bGETLIST_EVAL])
  IF Valtype( bBlock ) = 'B'
    Eval( bBlock, oXbp )
  ENDIF

  /* --- Install code block to call when this object receives focus --- */
  bBlock := DC_GetBlock(aGetList[i,bGETLIST_GOTFOCUS])
  IF Valtype( bBlock ) = 'B' .AND. nType # GETLIST_BROWSECOL
    oXbp:setInputFocus := DC_MergeBlocks( bBlock, oXbp:setInputFocus, oXbp )
  ENDIF

  /* --- Install code block to call when this object loses focus --- */
  bBlock := DC_GetBlock(aGetList[i,bGETLIST_LOSTFOCUS])
  IF Valtype( bBlock ) = 'B' .AND. nType # GETLIST_BROWSECOL
    oXbp:killInputFocus := DC_MergeBlocks( bBlock, oXbp:killInputFocus, oXbp )
  ENDIF

  /* --- Set Tab-Stop on this object if TABSTOP clause is used --- */
  IF IsMemberVar(oXbp,'TABSTOP')
     oXbp:tabstop := aOptions[lGETOPT_TABSTOP]
     IF Valtype(aGetList[i,lGETLIST_TABSTOP]) = 'L'
       oXbp:tabstop := aGetList[i,lGETLIST_TABSTOP]
     ENDIF
  ENDIF
  IF Valtype(aGetlist[i,nGETLIST_TABGROUP]) = 'N' .AND. ;
      IsMemberVar(oXbp,'GROUP')
     oXbp:group := aGetlist[i,nGETLIST_TABGROUP]
  ENDIF

  /* --- Assign Accelerator Key for this object --- */
  IF Valtype( aGetlist[i,nGETLIST_ACCELKEY] ) = 'N'
    _AddHotKey( aGetlist[i,nGETLIST_ACCELKEY], ;
                DC_GetBlock(aGetlist[i,bGETLIST_ACTION]), ;
                DC_GetBlock(aGetlist[i,bGETLIST_WHEN]), ;
                DC_GetBlock(aGetlist[i,bGETLIST_HIDE]), ;
                IIF( oParent=nil,nil,oXbp), aKEYBOARDCONTROLS )
  ENDIF

NEXT

/* --- FIT clause was used --- */
IF lFit
  _AutoSize( aGetList, oParentDlg, lMenu, nButtons, aOptions )
ENDIF

/* --- Restore old Error Handler --- */
ErrorBlock(bErrorBlock)

IF nButtons > 0
  ... Add optional buttons ...
ENDIF


/* --- Show all browse windows --- */
FOR i := 1 TO Len(aBROWSECONTROLS)
  aArray := aBROWSECONTROLS[i]:cargo
  xData := aArray[5]
  IF Valtype(aArray[11]) = 'A' .AND. Empty(aBROWSECONTROLS[i]:setLeftFrozen())
    aBROWSECONTROLS[i]:setLeftFrozen( aArray[11] )
  ENDIF
  IF Valtype(aArray[12]) = 'A' .AND. Empty(aBROWSECONTROLS[i]:setRightFrozen())
    aBROWSECONTROLS[i]:setRightFrozen( aArray[12] )
  ENDIF
  aBROWSECONTROLS[i]:show()
NEXT

/* --- Set Focus to first tabpage in each group --- */
FOR i := Len(aTABPAGECONTROLS) TO 1 STEP -1
  oXbp := aTABPAGECONTROLS[i]
  IF i = 1 .OR. !(oXbp:setParent()==aTABPAGECONTROLS[i-1]:setParent())
     _TabActivate( aTABPAGECONTROLS, oXbp, .t. )
     _Message( oXbp, aGetList, oMESSAGEBOX, .f., .f. )
  ENDIF
NEXT

oParentDlg:show()

IF !Empty(aEDITCONTROLS)
  SetAppFocus(aEDITCONTROLS[1])
ENDIF

/* --- Set Focus to designated object --- */
IF !Empty(xSetFocus)
   IF Valtype(xSetFocus) = 'O'
      oXbp := xSetFocus
   ELSE
      oXbp := DC_GetObject( aGetList, xSetFocus )
   ENDIF
   IF Valtype(oXbp) = 'O'
     SetAppFocus(oXbp)
   ENDIF
ENDIF

/* --- EXIT clause was used --- */
IF lSubDialog
  BREAK
ENDIF

/* --- Start up Event Loop --- */
DC_ReadGuiEventLoop( aGetList, bEventHandler, nil, oParentDlg, @aRef, nTimeOut )

IF lNewDlg
  oParentDlg:Destroy()
ENDIF

END SEQUENCE

IF nLASTEVENT # DCGUI_NONE .OR. lGETLISTUNDO
  lOKSTATUS := nLASTEVENT # DCGUI_EXIT_ABORT .AND. !lGETLISTUNDO
  nLASTEVENT := DCGUI_NONE
  IF !lOKSTATUS
    _Undo(aGetList)
  ENDIF
ENDIF

RETURN lOKSTATUS

Using the eXPress++ system to create an Application


The eXPress++ system can be used to create a new application or to add sub-dialogs and pop-up dialogs to an existing application.

XCLIPDOC is an application that captures either Text or BitMap images that were saved to the Clipboard by any other application and stores them into a database name CLIPDOC.DBF. The documents can later be browsed, previewed and/or printed. Pressing the CLIP button grabs the ClipBoard and saves the document. Pressing the CAPTURE button enables "automatic capture" mode and minimizes the application on the taskbar. The application constantly monitors the Clipboard and automatically pops up when something has been captured by another program. Use CLIPDOC for scanning documents and storing them to a database. Press CAPTURE, then scan the document with your scanning software and save it to the Clipboard. Use CLIPDOC for capturing screen images for slide-shows and storing them to a database. Press CAPTURE, then highlight the desired window and press Alt-Print-Scrn to capture to the clipboard.

This application is a good example of how to combine commands and object-oriented code and how to write a dialog system that will work both in SDI mode and MDI mode.
#xclipdoc.prg

#INCLUDE "dcdialog.ch"
#INCLUDE "dcprint.ch"
#INCLUDE "appevent.ch"
#INCLUDE "dcbitmap.ch"
#include 'dmlb.ch'
#include "fileio.ch"

#DEFINE cDocName             aApp[1]
#DEFINE dDocDate             aApp[2]
#DEFINE cDesc                aApp[3]
#DEFINE cComments            aApp[5]
#DEFINE oToolBar             aApp[6]
#DEFINE oBrowse              aApp[7]
#DEFINE lAddMode             aApp[8]
#DEFINE oDocName             aApp[9]
#DEFINE aGetList             aApp[10]
#DEFINE aPres                aApp[11]
#DEFINE lCaptureMode         aApp[12]
#DEFINE xBuffer              aApp[13]
#DEFINE aTimerEvent          aApp[14]
#DEFINE oDlg                 aApp[15]
#DEFINE oStatic1             aApp[16]
#DEFINE oBitMap1             aApp[17]
#DEFINE oStatic2             aApp[18]
#DEFINE oText2               aApp[19]
#DEFINE xBitMap              aApp[20]
#DEFINE cDocType             aApp[21]
#DEFINE cSavedDocType        aApp[22]
#DEFINE oAddButton           aApp[23]
#DEFINE oStatic3             aApp[24]
#DEFINE oDocSay              aApp[25]
#DEFINE oDlgWindow           aApp[26]
#DEFINE lMDIApp              aApp[27]
#DEFINE cTitle               aApp[28]
#DEFINE oDialogWindow        aApp[29]

FIELD DOC_NAME, DOC_DATE, DESC

FUNCTION XClipDoc( oDialog, lMDI )

LOCAL GetList := {}, aStru, aApp[30], GetOptions

DEFAULT lMDI := .f.
lMDIApp := lMDI

aGetList := GetList
IF !File('CLIPDOC.DBF')
  aStru := { ;
   { 'DOC_NAME'  , 'C',  30, 0 }, ;
   { 'DOC_DATE'  , 'D',   8, 0 }, ;
   { 'DESC'      , 'C',  50, 0 }, ;
   { 'COMMENTS'  , 'C', 100, 0 }, ;
   { 'DOC_TYPE'  , 'C',   1, 0 }, ;
   { 'BITMAP'    , 'V',   4, 0 } }

  dbCreate('CLIPDOC.DBF',aStru,'FOXCDX')
ENDIF

SET DELETED ON
USE CLIPDOC VIA "FOXCDX" SHARED

IF !File('CLIPDOC.CDX')
  INDEX ON DOC_NAME TAG 'DOC_NAME' TO CLIPDOC.CDX
  INDEX ON DOC_DATE TAG 'DOC_DATE' TO CLIPDOC.CDX
  INDEX ON DESC TAG 'DESC' TO CLIPDOC.CDX
ENDIF

SET INDEX TO CLIPDOC.CDX
OrdSetFocus('DOC_NAME')

SET DATE FORMAT TO 'mm/dd/yyyy'
LoadFields(aApp,.f.)
cDocType := ' '
lAddMode := .f.
lCaptureMode := .f.
aTimerEvent := SetTimerEvent()
cTitle := 'ClipBoard Document Capture Utility'

aPres := ;
  { { XBP_PP_COL_HA_FGCLR, GRA_CLR_WHITE },    /*  Header FG Color  */     ;
    { XBP_PP_COL_HA_BGCLR, GRA_CLR_DARKGRAY }, /*  Header BG Color  */     ;
    { XBP_PP_COL_DA_ROWSEPARATOR, XBPCOL_SEP_DOTTED }, /* Row Sep  */      ;
    { XBP_PP_COL_DA_COLSEPARATOR, XBPCOL_SEP_DOTTED }, /* Col Sep  */      ;
    { XBP_PP_COL_DA_FGCLR, GRA_CLR_BLACK },       /* Row FG Color  */      ;
    { XBP_PP_COL_DA_BGCLR, GRA_CLR_WHITE }        /* Row BG Color  */      ;
  }

IF lMDIApp
  @ 0,0 DCDIALOG oDlgWindow DRAWINGAREA oDlg SIZE 85,22 ;
    CAPTION cTitle
ELSE
  @ .5,3 DCSTATIC TYPE XBPSTATIC_TYPE_RAISEDBOX ;
    OBJECT oDlg SIZE 85,21
  IF Valtype(oDialog)='O'
    oDlg := oDialog:drawingArea
    oDialog:SetTitle( oDialog:title + ' - ' + cTitle )
    oDialogWindow := oDialog
  ENDIF
ENDIF

@ 1,2 DCBROWSE oBrowse ALIAS 'CLIPDOC' SIZE 44,10 ;
  EVAL {|o|o:itemmarked := {||LoadFields(aApp)}} ;
  PRESENTATION aPres ;
  PARENT oDlg ;
  WHEN {||!lAddMode}

DCBROWSECOL FIELD CLIPDOC->DOC_NAME PARENT oBrowse ;
   HEADER 'Doc Name' WIDTH 9 ;
   SORT {||OrdSetFocus('DOC_NAME')}

DCBROWSECOL FIELD CLIPDOC->DOC_DATE PARENT oBrowse ;
   HEADER 'Date' WIDTH 6 ;
   SORT {||OrdSetFocus('DOC_DATE')}

DCBROWSECOL FIELD CLIPDOC->DESC PARENT oBrowse ;
   HEADER 'Description' WIDTH 20 ;
   SORT {||OrdSetFocus('DESC')}

@ 1, 47 DCSTATIC TYPE XBPSTATIC_TYPE_RAISEDBOX ;
   OBJECT oStatic1  SIZE 25,10 ;
   PARENT oDlg ;
   HIDE {||!(cDocType='B' .OR. cSavedDocType='B')} ;
   EVAL {|o|o:lbdown := {||PrintDocument(aApp,2)}}

DCBITMAP CLIPDOC->bitmap PARENT oStatic1 AUTOSCALE

@ 1, 47 DCSTATIC TYPE XBPSTATIC_TYPE_RAISEDBOX ;
   OBJECT oStatic2  SIZE 25,10 ;
   PARENT oDlg ;
   HIDE {||!(cDocType='T' .OR. cSavedDocType='T')}

@ 0,0 DCMULTILINE xBitMap PARENT oStatic2 ;
  SIZE 25,10 ;
  NOWORDWRAP ;
  NOVERTSCROLL ;
  NOHORIZSCROLL ;
  EVAL {|o|o:lbdown := {||PrintDocument(aApp,2)}}

@ 11.5,2 DCSTATIC TYPE XBPSTATIC_TYPE_RAISEDBOX ;
  SIZE 70, 9 OBJECT oStatic3 PARENT oDlg

@ .5,2 DCSAY 'Document Name' GET cDocName ;
  PICT '@!' ;
  PARENT oStatic3 ;
  SAYSIZE 14 SAYRIGHT ;
  SAYOBJECT oDocSay ;
  GETOBJECT oDocName ;
  VALID {||Validate(aApp,1)}

@ 1,0 DCSAY 'Document Date' GET dDocDate ;
  SAYSIZE 14 SAYRIGHT ;
  RELATIVE oDocSay ;
  PARENT oStatic3 ;
  POPUP {|d|DC_PopDate(d)} ;
  VAlID {||!Empty(dDocDate)}

@ 2,0 DCSAY 'Description' GET cDesc ;
  PARENT oStatic3 ;
  RELATIVE oDocSay ;
  SAYSIZE 14 SAYRIGHT GETSIZE 40

@ 3,0 DCSAY 'Comments:' PARENT oStatic3 ;
  RELATIVE oDocSay

@ 4,0 DCMULTILINE cComments SIZE 66,4 PARENT oStatic3 ;
  RELATIVE oDocSay

/* ----- Toolbar ----- */

@ 1, 74 DCTOOLBAR oToolBar SIZE 9,18.3 ;
  BUTTONSIZE 9,1.2 ;
  PARENT oDlg ;
  TYPE XBPSTATIC_TYPE_TEXT

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION '&Top' ;
   PARENT oToolBar ;
   ACCELKEY xbeK_ALT_T ;
   ACTION {||dbGoTop(),LoadFields(aApp)} ;
   WHEN {||!lAddMode}

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION '&Previous' ;
   PARENT oToolBar ;
   ACCELKEY xbeK_ALT_P ;
   ACTION {||dbSkip(-1),LoadFields(aApp)} ;
   WHEN {||!lAddMode}

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION '&Next' ;
   PARENT oToolBar ;
   ACCELKEY xbeK_ALT_N ;
   ACTION {||dbSkip(1),LoadFields(aApp)} ;
   WHEN {||!lAddMode}

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION '&Bottom' ;
   PARENT oToolBar ;
   ACCELKEY xbeK_ALT_B ;
   ACTION {||dbGoBottom(),LoadFields(aApp)} ;
   WHEN {||!lAddMode}

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION '&Save' ;
   TOOLTIP 'Save Changes ; to file' ;
   PARENT oToolBar ;
   ACCELKEY xbeK_ALT_S ;
   ACTION {||SaveFields(aApp)} ;
   WHEN {||TestChanged(aApp)}

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION '&Add' ;
   TOOLTIP 'Add a new Docment Record' ;
   OBJECT oAddButton ;
   PARENT oToolBar ;
   ACCELKEY xbeK_ALT_A ;
   ACTION {||AddMode(aApp)}

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION '&Clip' ;
   PARENT oToolBar ;
   TOOLTIP 'Capture Clipboard bitmap to Selected record' ;
   ACTION {||MonitorClipboard(aApp)} ;
   WHEN {||!lAddMode} ;
   ACCELKEY xbeK_ALT_C

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION 'Pr&int' ;
   PARENT oToolBar ;
   TOOLTIP 'Print the document' ;
   ACTION {||PrintDocument(aApp,1)} ;
   WHEN {||!lAddMode} ;
   ACCELKEY xbeK_ALT_I

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION 'Pre&view' ;
   PARENT oToolBar ;
   ACTION {||PrintDocument(aApp,2)} ;
   TOOLTIP 'Preview the document on the screen' ;
   WHEN {||!lAddMode} ;
   ACCELKEY xbeK_ALT_V

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION 'Captu&re' ;
   PARENT oToolBar ;
   TOOLTIP {||IIF(lCaptureMode,'Turn off Capture',;
                  'Enable Capture Mode (grab images from Clipboard)')} ;
   ACTION {||SetCaptureMode(aApp)} ;
   WHEN {||!lAddMode} ;
   ACCELKEY xbeK_ALT_R

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCADDBUTTON CAPTION 'E&xit' ;
   PARENT oToolBar ;
   ACCELKEY xbeK_ALT_X ;
   ACTION {||DC_ReadGuiEvent(DCGUI_EXIT_OK,GetList)}

DCADDBUTTON TYPE XBPSTATIC_TYPE_RAISEDRECT ;
            SIZE 9,.1 PARENT oToolBar

DCGETOPTIONS ;
   BITMAP BITMAP_PINSTRIPE

DCREAD GUI ;
   _FIT oDialog==NIL ;
   PARENT oDialog ;
   TITLE cTitle ;
   HANDLER DlgHandler REFERENCE aApp ;
   OPTIONS GetOptions ;
   EVAL {|o|oDialogWindow:=o} ;
   SAVE

CLOSE DATABASES

IF Valtype(oDlgWindow)='O'
  oDlgWindow:destroy()
ELSE
  DC_GetDestroy(GetList)
ENDIF

RETURN nil

/* ------------------------- */

STATIC FUNCTION ;
  DlgHandler ( nEvent, mp1, mp2, oXbp, oDialog, GetList, aApp )

IF nEvent = xbeP_Close .AND. lMDIApp
  RETURN DCGUI_EXIT_OK
ELSEIF Valtype(oDialog:cargo)='L' .AND. !oDialog:cargo .AND. !lMDIApp
  PostAppEvent( xbeP_Close )
ELSEIF !Empty(xBuffer) .AND. !lAddMode .AND. lCaptureMode
  AddMode(aApp)
ENDIF

RETURN DCGUI_NONE

/* -------------------- */

STATIC FUNCTION AddMode( aApp )

LOCAL nSaveRec, oDialog

IF !lAddMode
  lAddMode := .t.
  nSaveRec := RecNo()
  dbGoBottom()
  dbSkip()
  LoadFields(aApp,.t.)
  dbGoto(nSaveRec)
  oAddButton:setCaption('C&ancel')
  DC_GetRefresh(aGetList)
  SetAppFocus(oDocName)
  DC_ClearEvents()
  IF cSavedDocType = 'B'
    DC_BitMapDraw(oStatic1,xBuffer)
  ENDIF
ELSE
  xBuffer := nil
  lAddMode := .f.
  oAddButton:setCaption('&Add')
  LoadFields(aApp)
  IF lCaptureMode
    oDialog := DialogWindow(aApp)
    oDialog:setFrameState(XBPDLG_FRAMESTAT_MINIMIZED)
  ENDIF
ENDIF

RETURN nil

/* --------------------- */

STATIC FUNCTION Validate ( aApp, nMode )

LOCAL nEvent, oNextXbp

nEvent := NextAppEvent(nil,nil,@oNextXbp)
IF nEvent = xbeP_SetInputFocus .AND. Valtype(oNextXbp) = 'O' .AND. ;
   oNextXbp == oAddButton
   RETURN .t.
ENDIF

IF nMode = 1  // validate doc name

  IF Empty(cDocName)
     DC_WinAlert('Name cannot be empty!')
     RETURN .f.
  ENDIF
  RETURN .t.

ENDIF

RETURN .t.

/* -------------------- */

STATIC FUNCTION LoadFields( aApp, lRefresh )

DEFAULT lRefresh := .t.
cDocName      := CLIPDOC->doc_name
dDocDate      := CLIPDOC->doc_date
cDesc         := CLIPDOC->desc
cComments     := CLIPDOC->comments
xBitMap       := CLIPDOC->bitmap
cDocType      := CLIPDOC->doc_type

IF Empty(dDocDate)
   dDocDate := Date()
ENDIF
IF lRefresh
  DC_GetRefresh(aGetList)
  IF cDocType = 'B'
    DC_BitMapDraw(oStatic1,CLIPDOC->bitmap)
  ENDIF
ENDIF

RETURN nil

/* -------------------- */

STATIC FUNCTION SaveFields( aApp )

LOCAL oDialog

IF lAddMode
  IF !AddRec()
    RETURN .f.
  ENDIF
ELSE
  IF !RecLock()
    RETURN .f.
  ENDIF
ENDIF

REPL CLIPDOC->doc_name WITH cDocName
REPL CLIPDOC->doc_date WITH dDocDate
REPL CLIPDOC->desc     WITH cDesc
REPL CLIPDOC->comments WITH cComments
IF !Empty(xBuffer)
  REPL CLIPDOC->bitmap   WITH xBuffer
  REPL CLIPDOC->doc_type WITH cSavedDocType
ENDIF

cSavedDocType := ' '
xBuffer := nil
dbRUnlock()

lAddMode := .f.
DC_GetRefresh(aGetList)

IF lCaptureMode
  oDialog := DialogWindow(aApp)
  oDialog:setFrameState(XBPDLG_FRAMESTAT_MINIMIZED)
ENDIF

RETURN nil

/* -------------------- */

STATIC FUNCTION TestChanged( aApp )

RETURN ;
cDocName      # CLIPDOC->doc_name   .OR. ;
dDocDate      # CLIPDOC->doc_date   .OR. ;
cDesc         # CLIPDOC->desc       .OR. ;
cComments     # CLIPDOC->comments

/* ----------------- */

FUNCTION addrec ()

APPEND BLANK
IF !NETERR()
   RETURN (.T.)
ENDIF
DC_Winalert('File is in use. Try again later!')
RETURN .F.

/* ----------------- */

FUNCTION reclock ()

IF dbRLock()
   RETURN .t.
ENDIF
DC_Winalert('Record is in use. Try again later!')
RETURN .F.

/* --------------- */

STATIC PROCEDURE SetCaptureMode( aApp )

LOCAL bTimerEvent, oDialog

bTimerEvent := {|| CaptureClipBoard( aApp )}
lCaptureMode := !lCaptureMode
DC_WinAlert('Capture Mode is ' + IIF(lCaptureMode,'ON','OFF'))
IF lCaptureMode
  SetTimerEvent( 100, bTimerEvent )
  oDialog := DialogWindow(aApp)
  oDialog:setFrameState(XBPDLG_FRAMESTAT_MINIMIZED)
ELSE
  SetTimerEvent( aTimerEvent[1], aTimerEvent[2] )
ENDIF

RETURN

/* ---------------*/

STATIC PROCEDURE CaptureClipBoard( aApp )

LOCAL oBitMap, aFormats, oClipBoard, oDialog

// Open clipboard and determine the data formats contained in it
IF !Empty(xBuffer)
   RETURN
ENDIF

oClipBoard := XbpClipBoard():new():Create()
oClipBoard:open()
aFormats := oClipBoard:queryFormats()
oDialog := DialogWindow(aApp)
IF AScan( aFormats, XBPCLPBRD_BITMAP ) > 0
  oBitMap := oClipBoard:getBuffer( XBPCLPBRD_BITMAP )
  xBuffer := oBitMap:setBuffer()
  cSavedDocType := 'B'
  DC_BitmapDraw(oStatic1,xBuffer)
  oClipBoard:clear()
  oDialog:setFrameState(XBPDLG_FRAMESTAT_NORMALIZED)
ELSEIF AScan( aFormats, XBPCLPBRD_TEXT ) > 0
  xBuffer := oClipBoard:getBuffer( XBPCLPBRD_TEXT )
  cSavedDocType := 'T'
  oClipBoard:clear()
  oDialog:setFrameState(XBPDLG_FRAMESTAT_NORMALIZED)
  xBitMap := xBuffer
ENDIF
oClipBoard:close()
oClipBoard:Destroy()

RETURN

/* --------------- */

STATIC PROCEDURE MonitorClipBoard( aApp )

LOCAL aFormats, oClipBoard, oBitMap

// Open clipboard and determine the data formats contained in it

IF !RecLock()
  RETURN
ENDIF
oClipBoard := XbpClipBoard():new():Create()
oClipBoard:open()
aFormats := oClipBoard:queryFormats()
IF AScan( aFormats, XBPCLPBRD_BITMAP ) > 0
  oBitMap := oClipBoard:getBuffer( XBPCLPBRD_BITMAP )
  oClipBoard:clear()
  REPL CLIPDOC->doc_type WITH 'B'
  REPL CLIPDOC->bitmap WITH oBitMap:setBuffer()
  oBitMap:SetBuffer(oBitMap:SetBuffer())
  DC_WinAlert('BitMap has been saved!')
  DC_BitmapDraw(oStatic1,CLIPDOC->bitmap)
ELSEIF AScan( aFormats, XBPCLPBRD_TEXT ) > 0
  xBuffer := oClipBoard:getBuffer( XBPCLPBRD_TEXT )
  oClipBoard:clear()
  REPL CLIPDOC->doc_type WITH 'T'
  REPL CLIPDOC->bitmap WITH xBuffer
  DC_WinAlert('Text has been saved!')
ELSEIF AScan( aFormats, XBPCLPBRD_METAFILE ) > 0
  xBuffer := oClipBoard:getBuffer( XBPCLPBRD_METAFILE )
  oClipBoard:clear()
  DC_WinAlert('Metafile was retrieved!')
ELSE
  DC_WinAlert('Nothing in Clipboard to Save!')
ENDIF
oClipBoard:close()
oClipBoard:Destroy()
xBuffer := nil
UNLOCK
DC_GetRefresh(aGetList)

RETURN

/* ------------------------- */

STATIC FUNCTION PrintDocument( aApp, nPrintMode )

LOCAL oPrinter, cScrn, i, cMemo, nLineCount, cMemoLine, nRow

DC_ClearEvents()
IF nPrintMode = 1  // Standard Print
  DCPRINT ON SIZE 60,100 TO oPrinter FONT '14.Arial'
ELSEIF nPrintMode = 2 // Preview
  DCPRINT ON SIZE 60,100 TO oPrinter PREVIEW FONT '14.Arial'
ENDIF

BEGIN SEQUENCE

IF Valtype(oPrinter) # 'O' .OR. !oPrinter:lActive
  BREAK
ENDIF

IF nPrintMode # 2
  cScrn := DC_WaitOn()
ENDIF

@ 3,1 DCPRINT SAY 'Document Name: ' FONT '12.Courier'
@ DC_PrinterRow(oPrinter), DC_PrinterCol(oPrinter) ;
   DCPRINT SAY CLIPDOC->doc_name PRINTER oPrinter

@ 4.5,1 DCPRINT SAY 'Document Date: ' FONT '12.Courier'
@ DC_PrinterRow(oPrinter), DC_PrinterCol(oPrinter) ;
   DCPRINT SAY CLIPDOC->doc_date PRINTER oPrinter

@ 6,1 DCPRINT SAY 'Document Desc: ' FONT '12.Courier'
@ DC_PrinterRow(oPrinter), DC_PrinterCol(oPrinter) ;
   DCPRINT SAY CLIPDOC->desc PRINTER oPrinter

IF cDocType = 'B'
  @ 8,1,59,98 DCPRINT BITMAP CLIPDOC->bitmap ;
   PRINTER oPrinter NOAUTOSCALE
ELSEIF cDocType = 'T'
  nRow := 9
  DCPRINT FONT '12.Terminal' PRINTER oPrinter
  cMemo := CLIPDOC->bitmap
  nLineCount := MLCount(cMemo)
  FOR i := 1 TO nLineCount
    cMemoLine := MemoLine( cMemo, nil, i )
    @ nRow,1 DCPRINT SAY cMemoLine PRINTER oPrinter
    nRow += 1.5
    IF nRow >= 59
      nRow := 1
      DCPRINT EJECT PRINTER oPrinter
    ENDIF
  NEXT
  DCPRINT EJECT PRINTER oPrinter
ENDIF

IF nPrintMode # 2
  DC_Impl(cScrn)
ENDIF

END SEQUENCE

DCPRINT OFF PRINTER oPrinter

RETURN nil

/* ------------------- */

STATIC FUNCTION DialogWindow( aApp )

LOCAL oDialog

IF Valtype(oDialogWindow) = 'O' .AND. oDialogWindow:isderivedFrom('XbpDialog')
  oDialog := oDialogWindow
ELSEIF Valtype(oDlgWindow) = 'O' .AND. oDlgWindow:isderivedFrom('XbpDialog')
  oDialog := oDlgWindow
ENDIF
RETURN oDialog

CONCLUSION


We have seen that Windows, GUI and Event-driven programming can be simpler than Clipper programming, while yielding a much more robust and user-friendly application. Command-based abstractions to object-oriented languages provide the best of both worlds and make programming faster and more maintainable.