| The Farber Consulting Group, Inc. |
Tel:
(516) 796-6545 \ Fax: (516) 796-1273
E-Mail: doron@dfarber.com
Our Web Site: http://www.dfarber.com
MarkRec Object
An application I developed has many places where the user is presented with options for selecting one or more items from a list.
To meet this need, I created the MarkRec object described in this article. It makes this type of selection easy for the on both desktop and pen based hardware platforms.
The User's View of MarkRec
The basic idea is to allow selection of multiple items by pen selection with confirmation taking the form of a check mark displayed adjacent to each selected item.
Specifically, the user should be able to select items with a double click (either of mouse or pen) on a highlighted item or by clicking a SELECT button below the list. Additionally, the listbox should allow multiple fields to be displayed in columns.
Page 2
Building MarkRec
MarkRec is designed as a black box. It accepts five parameters as follows:
a) aList - is an array containing the data to display. The first column contains all the fields to display separated by CHR(179), which provides a divider. You may add additional columns for different purposes. The array is modified by MarkRec, with a check mark inserted as the first character of the selected row. The developer can scan for CHR(251) to find selected records.
b) FieldTitle - The field descriptions to appear on top of each column.
c) WinTitle - The window title to appear.
d) MultiChk - Determines if the object is single check or multi check. If .T. multiple records can be selected.
e) IsItDim - Determines if items previously selected are dimmed or marked.
To allow several fields in one row (several columns in one list box), we separate the fields with a line (CHR(179)).
Although the object is designed to function in both Dos and Windows, in Windows, non-proportional fonts must be used for this object. The use of proportional fonts results in misalignment of the line which separates fields.
The aList array can be populated using SQL-SELECT or other array functions. The array must be declared before calling MarkRec. All elements of the array must be filled with data and you must check that the array if is not empty.
How it Works
Each item in the array needs two extra spaces in front of the data. If SQL-SELECT is used to populate the array, just add SPACE(2)at the beginning of the field expression. (See the example below.) Items are marked by inserting CHR(251) as the first character. To unmark an item, the first character is changed back to a space.
When the user double clicks on an item, the function ChkMark() is called by the Valid snippet of the list box. ChkMark() figures out whether to mark or unmark the item. The Select and Undo buttons also call ChkMark(). The Select All button calls the chkAll() function, which marks every item. The Undo All
Page 3
button calls the UnChkAll function which clears every item. The Undo button unmarks the selected item.
The Cancel Button cancels the entire process by setting m.IsItEsc=.T. The CleanUp snippet checks m.IsItEsc and restores the original data if Esc was pressed or Cancel clicked.
The Setup Snippet
Several variables are initialized in the Setup code so other functions can use them later.
There is code to check if some items have been selected previously. The developer may dim those items or let them be displayed. If m.IsItDim is .T., all items previously selected are dimmed to prevent them from being reselected, which may be important in some cases.
Normally, there is no need to dim any item. The default is to leave previously selected items checked. If more than one item is initially selected, multi-check capability is automatically turned on.
Here's the Setup code
*==================================================
* Program.........: MarkRec Screen
* Author..........: Doron Farber
* Created.........: 01/10/95
* Project.........: Common
* Copyright.......: (c) The Farber Consulting Group
* Purpose.........: Enable single & multi selection
*.................: of items
* Parameters List.:
* WinTitle........: Gets the window title
* MultiChk........: Is a flag to change this object
* ................: behavior. If .T. then multi
* ................: select is on. The default is F.
* IsItDim.........: If .T. previously selected items will be
*.................: dimmed
* MemVar List.....:
* IsItEsc.........: It is a flag which Let the
*.................: object knows if it was
*.................: Canceled.
* NowChkMark......: Holds the check mark value
* ................: which is CHR(251)
* ExactOn.........: Let this object knows if there
* ................: is a need to reset back to
* ................: "SET EXACT ON"
* aTempList.......: Holds the original array
Page 4
* ................: Content
* aCols...........: Gets the number of columns
* ................: in the aList array
* aRows...........: Gets the number of rows in
* ................: the aList array
* aElements.......: Gets the number of elements
* ................: in the aList array
* SeleItems.......: Is a counter of items when
*.................: multi select is on.
*==================================================
PARAMETERS aList,m.FieldTitle,m.WindTitle,;
m.MultiChk,m.IsItDim
PRIVATE IsItEsc,m.NowChkMark,m.ExactOn,aTempList,;
m.aCols,m.aRows,m.aElements,m.SeleItems,;
m.Pos,m.CountChk
*** To prevent any FoxPro compiler warnings EXTERNAL ARRAY aList
m.IsItEsc=.F. m.ExactOn=.F. m.SeleItems=0
IF EMPTY(m.FieldTitle) m.FieldTitle="" ENDIF
IF EMPTY(m.WindTitle) m.WindTitle="" ENDIF
*** Establish the check mark
value
m.NowChkMark=CHR(251)
*** Save the SET EXACT Command
IF SET('EXACT')=='ON'
m.ExactOn=.T.
SET EXACT OFF
ENDIF
*** Number of columns in the
array
m.aCols=ALEN(aList,2)
*** Number of rows in the
array
m.aRows=ALEN(aList,1)
*** Number of elements in
the array
m.aElements=(m.aCols)*(m.aRows)
*** Copy the original array
in case of Cancel
=ACOPY(aList,aTempList)
Page 5
*** Search for the first
check mark
m.Pos=ASCAN(aList,m.NowChkMark)
IF m.IsItDim *** Dim selected items if found DO WHILE m.Pos>0 aList[m.Pos]="\ "+SUBSTR(aList[m.Pos],3) m.Pos=ASCAN(aList,m.NowChkMark,m.Pos+1) ENDDO ELSE *** Check if are there any previously selected items DO WHILE m.Pos>0 m.Pos=ASCAN(aList,m.NowChkMark,m.Pos+1) m.SeleItems=m.SeleItems+1 ENDDO IF m.Pos>1 m.MultiChk=.T. ENDIF ENDIF
The CleanUp Code
The following code goes in the Cleanup Snippet.
IF m.ExactOn SET EXACT ON ENDIF
IF m.IsItEsc =COPY aTempList TO aList RETURN .F. ELSE RETURN .T. ENDIF
In addition, there are three functions located in the CleanUp Snippet: ChkMark, ChkAll and UnChkAll.
The ChkAll function
The ChkAll function is called only when multi check is on, i.e., m.multiChk=.T. This function is called from the <Select All> button. Its purpose is to provide a check mark at the beginning of each column of the array.
Page 6
*==================================================
* Function........: ChkAll
* Author..........: Doron Farber
* Created.........: 01/10/95
* Project.........: Common
* Copyright.......: (c) The Farber Consulting Group
* Project.........: Common
* Memvar List.....:
* J...............: Counter for all elements
* SelectItems.....: Initiated in the screen setup
* aElements.......: Initiated in the screen setup
* aCols...........: Initiated in the screen setup
* aRows...........: Initiated in the screen setup
* Calling.........: ChkAll()
* Return Type.....: Modified aList array
* Notes...........: None
*==================================================
FUNCTION ChkAll
PRIVATE m.J
*** Add Check mark at the beginning of each row FOR m.J=1 TO m.aElements STEP m.aCols aList[m.J]=m.NowChkMark+" "+SUBSTR(aList[m.J],3) ENDFOR m.SeleItems=m.aRows
*** Refresh the list Box
SHOW GET m.SelectItem
SHOW GET m.NowUndoAll ENABLE
SHOW GET m.NowSeleAll DISABLE
SHOW GET m.NowUndo ENABLE
_CUROBJ=1
RETURN aList
Page 7
The UnChkAll function
This function clears all checked items, and places a space instead of the CHR(251) which was added at time of selection.
*==================================================
* Function........: UnChkAll
* Author..........: Doron Farber
* Created.........: 01/10/95
* Copyright.......: (c) The Farber Consulting Group
* Project.........: Common
* Purpose.........: Display a Check Mark for all
* Memvar List.....:
* J...............: Counter for all elements
* SelectItems.....: Initiated in the screen setup
* aElements.......: Initiated in the screen setup
* aCols...........: Initiated in the screen setup
* aRows...........: Initiated in the screen setup
* Calling.........: UnChkAll()
* Return Type.....: Modified aList array
* Notes...........: None
*==================================================
FUNCTION UnChkAll
PRIVATE m.J
*** Erase Check mark at the beginning of each row
FOR m.J=1 TO m.aElements STEP m.aCols aList[m.J]=" "+SUBSTR(aList[m.J],3) ENDFOR m.SeleItems=0
*** Refresh the list Box
SHOW GET m.SelectItem
SHOW GET m.NowSeleAll ENABLE
SHOW GET m.NowUndoAll DISABLE
SHOW GET m.NowUndo DISABLE
_CUROBJ=1
RETURN aList
The ChkMark Function
This function is the heart of the MarkRec object. It is called in different situations. Whether in multi check or single check mode, it provides the capability to check and un-check within the same function. For instance, if you double click on an un-selected item, it is selected. If you double click the same item again, it is un-selected.
Page 8
*==================================================
* Function........: ChkMark
* Author..........: Doron Farber
* Created.........: 01/10/95
* Copyright.......: (c) The Farber Consulting Group
* Project.........: Common
* Purpose.........: Display a Check Mark for the
* ................: current item
* Parameters List.:
* aList...........: Is an array which was passed
* ................: via the MarkRec object
* MultiChk........: If .T. then it is multi check
* m.IsCancel......: Indicates if the undo button
* ................: has been pressed
* m.IsSelect......: Indicates if the select button
*.................: has been pressed
*.................:
* MemVar List.....:
* LastPos.........: Indicates the last check mark
* ................: position as an element
* ................: for a single check mark
* NowPos..........: Indicates the current row as an
* ................: element
* SelectItem......: The current row number in the
* ................: list box
* NowChkMark......: Holds the CHR(251) for Dos and
* ................: windows it was initiated within
* ................: the markrec object
* NowChk..........: Holds the value of the first 2
* ................: character in the array
* ................: for multi check mark
* aCols...........: Hold the number of columns.
*.................: Initiated in the screen setup
* Calling.........: ChkMark(.T.,.F.) or
* ................: ChkMark(.F.,.T.)
* Return Type.....: Modified array with check mark
* ................: at the first element fort the
* ................: selected item.
* Notes...........: This function must be called
* ................: while MarkRec object is active
*==================================================
FUNCTION ChkMark
PARAMETERS m.IsCancel,m.IsSelect
PRIVATE m.LastPos,m.NowPos,m.NowChk
* Find current position
IF m.aCols>1 m.NowPos=AELEMENT(aList,m.SelectItem,1) ELSE m.NowPos=m.SelectItem ENDIF
Page 9
IF ! MultiChk *** Get the last position of the check mark character m.LastPos=ASCAN(aList,m.NowChkMark)
DO CASE *** To prevent any wrong cancel CASE m.LastPos<>m.NowPos AND m.IsCancel m.IsCancel=.T. m.IsSelect=.T.
*** To prevent any selection for a selected one CASE m.LastPos==m.NowPos AND m.IsSelect m.IsCancel=.T. m.IsSelect=.T.
*** In case the select button has been pressed CASE m.LastPos<>m.NowPos AND m.IsSelect m.IsSelect=.F. ENDCASE
*** Erase the previous check mark if found
IF m.LastPos>0 AND ! m.IsSelect
aList[m.LastPos]=" "+SUBSTR(aList[m.LastPos],3)
m.SeleItems=m.SeleItems-1
DO CASE
CASE ! m.IsCancel AND m.IsSelect
m.IsSelect=.F.
CASE m.NowPos<>m.LastPos
m.IsSelect=.T.
ENDCASE
m.IsCancel=.F.
ELSE
IF ! m.IsCancel
m.IsSelect=.T.
ENDIF
ENDIF
IF m.IsCancel AND m.IsSelect m.IsCancel=.F. m.IsSelect=.F. ENDIF ENDIF
Page 10
IF m.MultiChk m.NowChk=SUBSTR(aList[m.NowPos],1,2) DO CASE CASE m.NowChk==" " AND m.IsCancel m.IsCancel=.F. m.IsSelect=.F. CASE m.NowChk==m.NowChkMark+" " AND m.IsSelect m.IsCancel=.F. m.IsSelect=.F. CASE m.NowChk==" " AND ! m.IsSelect m.IsCancel=.F. m.IsSelect=.T. CASE m.NowChk==m.NowChkMark+" " AND ! m.IsSelect m.IsCancel=.T. m.IsSelect=.F. ENDCASE ENDIF
DO CASE
*** Erase the current "Check mark" CASE m.IsCancel aList[m.NowPos]=" "+SUBSTR(aList[m.NowPos],3) m.SeleItems=m.SeleItems-1
*** "Check mark" the current record CASE m.IsSelect aList[m.NowPos]=m.NowChkMark+" "+SUBSTR(aList[m.NowPos],3) m.SeleItems=m.SeleItems+1 ENDCASE
IF m.MultiChk DO CASE CASE m.SeleItems=m.aRows SHOW GET m.NowSeleAll DISABLE CASE m.SeleItems>0 SHOW GET m.NowSeleAll ENABLE SHOW GET m.NowUndoAll ENABLE CASE m.SeleItems==0 SHOW GET m.NowUndoAll DISABLE ENDCASE ENDIF
IF m.SeleItems>0 SHOW GET m.NowUndo ENABLE ELSE SHOW GET m.NowUndo DISABLE ENDIF
*** Refresh the list Box SHOW GET m.SelectItem _CUROBJ=1 RETURN aList
Page 11
An example
Let us assume we have a company with several departments. Each department has a number of employees.
Two of the tables comprising the database are Dept.Dbf and Employee.dbf. Dept.dbf contains the key field DEP_DID [Department ID] and Employee.dbf contains the key field EMP_EID [Employee ID] and a field EMP_DID. Each Dept record is thus uniquely identified by its DID value. Each Employee.dbf record is uniquely identified by its EID value and contains a DID value which determines which department an employee "belongs" to.
[Dept.Dbf]
Dep_Did
Dep_Desc
Dep_floor
Dep_City
[Employee.Dbf]
Emp_Eid
Emp_Did
Emp_Fname
Emp_Lname
Dep_Did and Emp_Did link the two tables.
Say we want the user to first select a department and then have all employees for that department displayed, so the user can select some of them. We'll use MarkRec for each part of the process - in single-check mode to choose a department, then in multi-check mode to choose employees.
Once we select a department, we can use its DEP_DID to find all employees which belong to that department.
We call the markrec object to present the department list as follows:
Use SQL-SELECT to fill the array aNowDept:
SELECT ; SPACE(2)+"|"+Dept.Dep_Desc +"|"+ ; Dept.Dep_City +"|"+ ; Dept.Dep_Floor ,; Dept.Dep_DId ; FROM ; Dept ; INTO ARRAY aNowDept
Page 12
Check if there is more then one row in the array, using the FoxPro internal memvar _TALLY, which returns the number of records processed by the most recently executed table command.
IF _TALLY==0 RETURN ENDIF
***Prepare the memvars to be passed into MarkRec object
m.FldTitle=" Department
Name Dept. I.D. City"
m.WinTitle="Select The Department You Wish To Assign
Employees From "
*** Call the MarkRec object with single check capability IF ! MarkRec(@aNowDept,m.FldTitle,m.WinTitle) RETURN ENDIF
A typical single selection screen for Windows looks like Figure 2:
Once the user makes a
choice, we can see which one was chosen, as follows:
*** Get the position of the
department in the array
m.PosRec=ASCAN(aNowDept,m.ChkMark)
IF m.PosRec==0
RETURN
ENDIF
Page 13
Given a department, use the markrec object with multi-check capability to choose some employees from that department.
*** Get the department name
from the actual array. It
*** is based on the Dep_Eid which was populated into
*** the aNowDept array SELECT Dept SET ORDER TO Dep_Did IF SEEK(aNowDept[m.PosRec+1]) m.SeleDept=Dept.Dep_Desc ENDIF
*** Populate the array for the list of employees SELECT ; SPACE(2)+"|"+Employee.Emp_Fname+"|"+ ; Employee.Emp_Lname+"|"+ ; Employee.Emp_EId ,; Employee.Emp_Did ; FROM ; Employee; WHERE Employee.Emp_Did==aNowDept[m.PosRec+1] ; INTO ARRAY aNowEmploy ; ORDER BY Employee.Emp_Fname
IF _TALLY==0 RETURN ENDIF
Note that in the aNowEmploy array there are only two columns. The first column is displayed in the list box, with three fields concatenated into one element, separated by the CHR(179) character. The second column is the Employee id. This is also loaded into the array for later use.
*** Initiate fields prompts
m.FldTitle=" Loc No. Floor Department Location Type"
*** Initiate window title
m.WinTitle="Select Locations For "+ALLTRIM(m.SeleBuild)
The following is the actual call to the Markrec object. The aNowEmploy array is passed by reference and the other two parameters are passed by value.
The only difference is the .T. right after the WinTitle. That .T. tells MarkRec object to behave as a multi check object.
IF ! MarkRec(@aNowEmploy,m.FldTitle,m.WinTitle,.T.)
RETURN ENDIF
Page 14
A typical multi check selection screen for Windows looks like Figure 3:
With the fields prompt, you
will need to experiment a little - so the same title will match
the separated
lines either in Windows or Dos.
After you have made a
selection under single or multi checks, you may need to process
those rows and
proceed further.
The following shows how you might continue when you are done with the MarkRec object:
PRIVATE
m.NowPos,aTempList,m.NowChk
m.NowChk=CHR(251)
*** Copy the selected array
into a temporary one
=ACOPY(aNowEmploy,aTempList)
*** Search for first check
mark if any
m.NowPos=ASCAN(aNowEmploy,m.NowChk)
DO WHILE m.NowPos>0 *** Erase the check mark aNowEmploy[m.NowPos]=" "+SUBSTR(aNowEmploy[m.NowPos],3)
Page 15
****************************************************
*** Process any code
SELECT Employee
*** (m.NowPos+1) has the value of Employee I.D.
IF SEEK(aNowEmploy[m.NowPos+1])
....Process your code
ENDIF
***************************************************
*** Search for new check mark if any m.NowPos=ASCAN(aNowEmploy,m.NowChk) ENDDO
*** Restore the original
array if needed
=ACOPY(aTempList,aArray)
aNowEmploy is the original array that was used in the multi check example, which was passed and modified by the MarkRec object.
This month's Companion Resource disk contains examples of using this object under Windows or Dos. The procedure names are Movex.Prg and Move.Prg. If you intend to generate the code for that object, make sure to un-check "open files" and "close files".Make sure the window is a modal so the end users may need to terminate the MarkRec screen before going further with the program Also change the extension from SPR to PRG, so that you can call it as a function.
Developers are constantly looking for opportunities to transform objects that provide application specific functionality into generic objects that can be utilized in other applications. The MarkRec function is an excellent example of such an opportunity.