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. 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. 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. Commands are easy to read and maintain.
- 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. The GetList Array - GetList
- 2. The GetOptions Array - GetOptions
- 3. The Command definitions - DCDIALOG.CH
- 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] - A numeric value that is a pointer to the Getlist array
for this object.
- [2] - A pointer to the GetList array.
- [3] - The value of .
- [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.