home *** CD-ROM | disk | FTP | other *** search
/ Monster Media 1993 #2 / Image.iso / clipper / qtbrow16.zip / QT.DOC < prev    next >
Text File  |  1993-07-26  |  71KB  |  2,132 lines

  1.  
  2.                             QUIK TBROWSE
  3.  
  4.                       THE TBROWSE CODE GENERATOR
  5.  
  6.  
  7. Credits:
  8.  
  9. A lifetime of thanks goes to my friend and guru Ira Emus, President
  10. of ExtraSensory Software. Ira wrote (most notibly) keyloop() which
  11. forms the very heart of Quik TBrowse. His attention to detail kept me
  12. on track during the development phase of this project. Through his
  13. generosity I have been given permission to include the documentation
  14. from his exciting TBrowse seminars as Part Two of this manual.
  15.  
  16. Also thanks to Luiz Quintela of Computer Associates whose TBrowse class
  17. inspired this product.
  18.  
  19.  
  20. Quik TBrowse and its documentation are (c) copyright 1993 by Don Allred.
  21. All Rights Reserved.
  22.  
  23.  
  24. License Agreement
  25. -----------------
  26. The purchaser of Quik TBrowse is granted a license to use it, to make
  27. copies of the original distribution disk for backups, and to install
  28. it on one or more systems as long as there is no possibility that it
  29. will be used on more than one of those systems at a time. This does
  30. not constitute a site license; it is intended to allow a single
  31. programmer to install Quik TBrowse on all of the machines that he uses,
  32. but only to allow him to use it on one of those machines at once. In
  33. essence, Quik TBrowse is licensed to a single user, who is the only
  34. person permitted to use it.
  35.  
  36. This license applies to the development of software using Quik TBrowse,
  37. not to the redistribution of that software. Software in executable
  38. form that includes code produced by Quik TBrowse may be sold, given
  39. away, or otherwise redistributed with no additional fees to, and no
  40. special license required from, CodeSmith Software. You may not, however,
  41. redistribute Quik TBrowse in part or in whole.
  42.  
  43. CA-Clipper is a registered trademark of the Computer Associates
  44.  
  45.  
  46.  
  47. About the Manual
  48.  
  49. This manual is organized in two parts:
  50.  
  51. Part One will guide you through the operation of Quik TBrowse.
  52.  
  53. Part Two is Ira's TBrowse dissertation.
  54.  
  55.  
  56.  
  57.  
  58. PART ONE - USING QUIK TBROWSE
  59. -----------------------------
  60.  
  61.  
  62. JUMP START
  63. ==========
  64.  
  65. Start Quik TBrowse by typing QT <Enter>
  66.  
  67. Use the arrow keys to position the light bar on "ARELATE.CSB" and
  68. press <Enter>
  69.  
  70.  
  71. You should have three browses on the screen: customers, invoices
  72. and line items.    Invoices relate to customers by customer numbers
  73. and line items relate to invoices by invoice numbers. As you
  74. scroll down the customer browse invoices and line items change.
  75. Tab down to invoices and note the changes in the line items as
  76. you scroll through the invoices. Cool huh?
  77.  
  78. You'll have plenty of time to play with the Quik TBrowse so let's
  79. write some code and see if this thing really works.
  80.  
  81. Press ALT G (that's the ALT key and the letter G) for the
  82. generate menu. Select "TBrowse - Database" by pressing <Enter>
  83.  
  84. At the prompt "Output file name" press <Enter> to accept
  85. "BROWDEMO.PRG"
  86.  
  87. Press <Enter> three more times to accept the defaults.
  88.  
  89. After Quik TBrowse is finished percolating, exit to DOS by
  90. pressing ALT F for the files menu then press the letter 'X'.
  91.  
  92.  
  93. To compile BROWDEMO.PRG
  94.      CLIPPER BROWDEMO /N/W
  95.  
  96. To link BROWDEMO.OBJ
  97.      RTlink fi BROWDEMO
  98.  
  99.  
  100. Type "BROWDEMO" (yeah I know to leave out the quotes)    to examine
  101. your program.
  102.  
  103.  
  104. END JUMP START
  105. ==============
  106.  
  107.  
  108.  
  109.  
  110.                      Example Program Highlights
  111.                                          --------------------------
  112.  
  113. When designing ARELATE.CSB I made some fields editable like the
  114. customer's name, line item quantity and unit price. Just position
  115. the light bar on a cell and press enter to edit. You will get a
  116. polite message if the cell is not user editable like the customer
  117. and invoice numbers.
  118.  
  119. The Item description column is not as wide as the field so it
  120. will scroll during an edit. Note that if you change the quantity
  121. or unit price, the extended price will recalculate because it's
  122. not a field but an expression:
  123. transform(item->qty * item->unit, '999,999.99')
  124.  
  125. As you can see a column may be a field or an expression.
  126.  
  127. Pressing <Delete> will blank the contents of a record in the
  128. current browse.    The record didn't get deleted, just its contents.
  129. You may select which browse the user may delete records from.
  130.  
  131. If you press <Insert> while in the customer browse, a previously
  132. blanked record will be used (record recycling) if available otherwise
  133. a new record will be added. Note that the customer->id is incremented
  134. based on the last customer->id + '1'. An insert in the invoice browse
  135. will default to the next invoice number based on the last invoice
  136. number used in item.dbf.
  137.  
  138. When you allow the user to add records to a browse, you can set
  139. default values for each field. A support function NextNumber()
  140. is supplied which will increment the value of a key field
  141. (character or numeric) in any database which is open.
  142.  
  143.  
  144.  
  145. Using Quik TBrowse with other applications
  146. ------------------------------------------
  147.  
  148. The application we made in the "Jump Start" was a stand alone. You
  149. might remember we answered 'Y' to the Stand-alone? prompt in
  150. 'Application Options' prior to generating the code.
  151.  
  152. When integrating a Quik TBrowse program with another application you
  153. probably want a bit more contoll. In the above example, we crated a
  154. program named browdemo.prg. Browdemo uses myfuct() to create the
  155. TBrowse object which is then passed to (for multiple browses)
  156. browzeAll() which handles browse sequencing and makes calls to
  157. KeyLoop() which handles the keystroke processing. For single browses,
  158. pass the TBrowse object directly to KeyLoop().
  159.  
  160.  
  161. By answering 'N' to the Stand-alone prompt, myfunct() will return
  162. the TBrowse objects to your calling program. From there you can pass
  163. the objects to BrowzemAll() for keystroke processing when needed.
  164. This will speed things up since the objects only get created once.
  165.  
  166. Example:
  167.  
  168. oRelate := myfunct()      // create your TBrowse object
  169.  
  170.  
  171. Next you'll probably want to display all browses and their boxes with
  172. realtionship to the parent in your main application.
  173.  
  174. Example:
  175.  
  176. showRelate(oRelate, .t.)    // display TBrowse(s) and boxe(s)
  177.  
  178. ShowRelate() only displays and places the browses in sync. The second
  179. parameter is to display your boxes.
  180.  
  181.  
  182. When you want to activate the browse(s) simply pass to object
  183. to browzemAll()
  184.  
  185. Example:
  186.  
  187. BrowzemAll(oRelate)                // activate TBrowse(s)
  188.  
  189. If we wanted to create an invoicing system where the parent/customer
  190. datbase is shown in full screen mode and the children/grand children
  191. are shown in via TBrowse then we will need to delete the customer browse
  192. from myfunct() since there is probably no reason to show the customer
  193. record in both full screen and TBrowse at the same time.
  194.  
  195. How to delete the customer object:
  196.  
  197. // use customer alias customer new
  198. // dbsetindex("customer")
  199. // . . .
  200. // . . .
  201. // . . .
  202. // . . .
  203. comment out or delete down to BUT NOT INCLUDING THE LINE:
  204.  
  205. use invoice alias invoice new
  206.  
  207.  
  208.                                Summary
  209.                                                              -------
  210.  
  211. Your main application might look something like this:
  212.  
  213. local oRelate
  214. local nOldRec
  215.  
  216. oRelate := myfunct()           // create TBrowse object
  217.  
  218. ShowRelate(oRelate, .t.)             // display Tbrowses and boxes
  219.  
  220. nOldRec := customer->(recno()) // save the position of the parent database
  221.  
  222. do while .t.
  223.       .
  224.      .
  225.      .
  226.      .
  227.      do case
  228.  
  229.         case lastkey() == K_TAB
  230.                browzemAll(oRelate)   // activate TBrowse
  231.           .
  232.           .
  233.           .
  234.           .
  235.      endcase
  236.  
  237.      // If the customer record has changed
  238.      if nOldRec # customer->(recno())
  239.       // Syncronize browses, do not redisplay boxes
  240.       ShowRelate(oRelate, .f.)
  241.             nOldRec := customer->(recno()) // store the new record position
  242.      endif
  243.  
  244. enddo
  245.  
  246.  
  247. If you use CodeSmith to create the main application the only thing you
  248. need to do is delete the customer object from myfunct() as above then tell
  249. CodeSmith that your Quik TBrowse program is BROWDEMO and its main
  250. function is MYFUNCT. The rest is taken care of for you.
  251.  
  252.  
  253.  
  254.  
  255.  
  256.  
  257.                                 MENUS
  258.                                                                 -----
  259.  
  260. F10 will access the main menu. From there use the left/right
  261. arrow keys to navigate and press <Enter> to select a submenu.
  262. You can also access the submenus by pressing Alt + <hotkey>.
  263.  
  264. Example: ALT F will get you the File Menu
  265. Example: ALT O will get you the Column Menu
  266.  
  267. Help is available for each menu item, just press the familiar F1
  268. key.
  269.  
  270. The options for the TBrowse and Column menus may be accessed
  271. directly by pressing their associated hot key from within your
  272. TBrowse.
  273.  
  274.  
  275.  
  276.  
  277. FILE MENU
  278. =========
  279.  
  280.  
  281.                             ADD NEW BROWSE
  282.                             --------------
  283.  
  284. Append another TBrowse to current design. Choose an existing
  285. design (.csb) or a database (.dbf) Selecting a database will give
  286. you another TBrowse with one column. From there you may insert
  287. additional columns which may be fields or expressions.
  288. Reminder: If you want a relationship, you need to:
  289. Set Controlling Index and
  290. Set Relation
  291.  
  292.  
  293.  
  294.                         SET CONTROLLING INDEX
  295.                         ---------------------
  296.  
  297. Determines what order the TBrowse appears to be sorted on. You
  298. MUST select an index for descendant (child and grand child)
  299. browses if you want to do a relationship. A parent index is
  300. optional. The index must exist. There is no menu option for
  301. creating indexes.
  302.  
  303.  
  304.  
  305.                              SET RELATION
  306.                              ------------
  307.  
  308. This is how you position descendant (child and grand child)
  309. browses relative to their parent and to each other, kinda like a
  310. family.
  311.  
  312. Three selections must be made for this thing to work properly.
  313.  
  314. 1) Controlling index - Sets ordering for each browse and
  315. determines positioning (via seek) for the descendant browses. An
  316. index for the parent browse is optional.
  317.  
  318. You need to select a controlling index prior to 'Set Relation'.
  319. The index selection process should have probably been included
  320. within the set relation option but I thought it might be easier
  321. to find if was kept separate.
  322.  
  323. 2) Seek Condition  - Positions a descendant browse relative to
  324.                      its parent.
  325.  
  326. 3) While Condition - Top/Bottom range limit.
  327.  
  328. Example: Let's relate a customer to invoices to line items
  329.  
  330. Browse     Index     Seek             While
  331. ------     -----     ----             -----
  332. Customer   customer  (leave blank)    .t. (all records)
  333. Invoice    invoice   customer->id     invoice->id == customer->id
  334. Item       item      invoice->invnum  item->invnum == invoice->invnum
  335.  
  336.  
  337. The customer browse uses customer.ntx just to show the customers
  338. in numeric order. There is no seek so just leave it blank.
  339. The while condition is input as .t. as in DO WHILE .T..
  340. This will allow ALL customer records to be visible. If you want
  341. to have delete capability then your seek should be '7'
  342. (since our customer numbers begin with 7) which will
  343. position at the first customer number and the while condition
  344. should be !empty(customer->id). Remember that when you delete a
  345. record it does not get deleted but its contents get blanked so we
  346. want any deleted (blanked) customer record to float above the while
  347. condition so they will be out of sight.
  348.  
  349.  
  350. Invoice.ntx is used for the invoice browse. The key is
  351. invoice->ID which is the customer id number. For the seek we will
  352. use customer->id so when we move the the customer browse, the
  353. invoice will always position itself to the correct customer
  354. number. For the while condition we want to show only the invoices for
  355. one customer so the syntax is: invoice->id == customer->id
  356.  
  357.  
  358. Item.ntx is used for the line item browse. The key is
  359. item->invnum, the invoice number. When we scroll through the
  360. invoices, we want to seek the matching line items so we'll use
  361. invoice->invnum for the seek condition. Since we only want to see
  362. the all the line items for one invoice the while condition will
  363. be item->invnum == invoice->invnum.
  364.  
  365.  
  366.  
  367.  
  368.                              SAVE AS. . .
  369.                              ------------
  370.  
  371. Save your design for future use. Do not include an extension,
  372. (.csb) will be added for you.
  373.  
  374.  
  375.  
  376.                              EXIT TO DOS
  377.                              -----------
  378.  
  379. Leave this program completely. Be sure to save your work first!
  380.  
  381.  
  382.  
  383.  
  384.  
  385.  
  386. TBROWSE MENU
  387. ============
  388.  
  389.  
  390.                             MOVE (browse)
  391.                             -------------
  392.  
  393. Position current browse using arrow the keys.
  394.  
  395. May be selected outside of menu by pressing F2
  396.  
  397.  
  398.  
  399.                             SIZE (browse)
  400.                             -------------
  401.  
  402. Change the size of the current browse using arrow keys
  403.  
  404. May be selected outside of menu by pressing F3
  405.  
  406.  
  407.  
  408.                               BOX TYPE?
  409.                               ---------
  410.  
  411. Select a box for the current browse. Box types are:
  412. NONE, SINGLE, DOUBLE, SINGLE-DOUBLE and DOUBLE-SINGLE
  413.  
  414. May be selected outside of menu by pressing the letter 'B'
  415.  
  416.  
  417.                               BOX STYLE
  418.                                                             ---------
  419.  
  420.  
  421. Select a STYLE for your box. The characters used to draw each box
  422. are based on BOX TYPE selected.    BOX STYLE is a global setting and
  423. will affect the shape of all boxes.
  424.  
  425.  
  426.                               SIMPLE               COMPOUND
  427.  
  428.                           ┌───TITLE───┐         ┌───────────┐
  429.                           │           │         │   TITLE   │
  430.                           │           │         ├───────────┤
  431.                           │           │         │           │
  432.                           │           │         │           │
  433.                           │           │         │           │
  434.                           │           │         │           │
  435.                           └───────────┘         └───────────┘
  436.  
  437. May be selected outside of menu by pressing the letter 'O'
  438.  
  439.  
  440.  
  441.  
  442.                                SHADOW?
  443.                                -------
  444.  
  445. Select or deselect a shadow on right and bottom of the current browse.
  446.  
  447. May be selected outside of menu by pressing F9
  448.  
  449.  
  450.  
  451.                              HEADER TITLE
  452.                              ------------
  453.  
  454. Add some descriptive text to the current browse.
  455. Text will be automatically centered one line above the browse.
  456. Looks real goofy without a box.
  457.  
  458. May be selected outside of menu by pressing the letter 'H'
  459.  
  460.  
  461.  
  462.                             TBROWSE COLOR
  463.                             -------------
  464.  
  465. Change the default colors for the current browse.
  466. Choose two color pairs.
  467.  
  468. Example: 'w+/b. gr+/rb' will show the TBrowse in bright white on
  469. a blue background with the light bar in yellow on magenta. Note
  470. that the pairs are enclosed in quotes. Omit the quotes when using
  471. a manifest constant like C_NORM. A color chart is available to
  472. assist you.
  473.  
  474. May be selected outside of menu by pressing the letter 'C'
  475.  
  476.  
  477.  
  478.                            TAB NEXT TBROWSE
  479.                            ----------------
  480.  
  481. Switch focus to the next browse. Only useful when there is more
  482. than one browse.
  483.  
  484. May be selected outside of menu by pressing the TAB key
  485.  
  486.  
  487.  
  488.                        SH-TAB PREVIOUS TBROWSE
  489.                        -----------------------
  490.  
  491. Switch focus to the previous browse. Only useful when there is
  492. more than one browse.
  493.  
  494. May be selected outside of menu by pressing the SHIFT + TAB key
  495.  
  496.  
  497.  
  498.                            LINES PER SCREEN
  499.                            ----------------
  500.  
  501. Select 25, 43 or 50 line mode. The mode selected must be supported
  502. by your hardware.
  503.  
  504. May be selected outside of menu by pressing the letter 'L'
  505.  
  506.  
  507.  
  508.                               ZAP BROWSE
  509.                               ----------
  510.  
  511. Permanetly remove the current browse. Use with caution as there
  512. is no UNDO.    There must be at least one browse on the screen at
  513. all times. Remember to save your work frequently.
  514.  
  515. May be selected outside of menu by pressing the letter 'Z'
  516.  
  517.  
  518.  
  519.  
  520.  
  521. COLUMN MENU
  522. ===========
  523.  
  524.  
  525.                             COLUMN HEADING
  526.                             --------------
  527.  
  528. Optional title above each column.
  529. Example: For a customer->name column you might enter 'Name'
  530. (in quotes).
  531.  
  532. For multiple lines, add a semi-colon ';' Example: 'First;Name'
  533. will display:
  534.               First
  535.               Name
  536.  
  537. An expression such as dtoc(date()) is ok too, be sure to omit
  538. the quotes
  539.  
  540. May be selected outside of menu by pressing F4
  541.  
  542.  
  543.  
  544.                             COLUMN FOOTING
  545.                             --------------
  546.  
  547. Optional title below each column.
  548. Example: For a customer->name column you might enter 'Name'
  549. (in quotes).
  550.  
  551. For multiple lines, add a semi-colon ';' Example: 'First;Name'
  552. will display:
  553.              First
  554.              Name
  555.  
  556. An expression such as dtoc(date()) is ok too, be sure to
  557. omit the quotes
  558.  
  559. May be selected outside of menu by pressing F5
  560.  
  561.  
  562.  
  563.                           COLUMN SEPARATORS
  564.                           -----------------
  565.  
  566. Optional character string used for vertical separation on the
  567. left side the current column.    Not used for the first column.
  568.  
  569. An ascii chart is available by pressing F3
  570.  
  571. May be selected outside of menu by pressing F6
  572.  
  573.  
  574.  
  575.                           HEADING SEPARATOR
  576.                           -----------------
  577.  
  578. Optional character string used for horizontal separation between
  579. the top of the column data and Column Heading.
  580.  
  581. An ascii chart is available by pressing F3
  582.  
  583. May be selected outside of menu by pressing F7
  584.  
  585.  
  586.  
  587.                           FOOTING SEPARATOR
  588.                           -----------------
  589.  
  590. Optional character string used for horizontal separation between
  591. the bottom of the column data and Column Footing.    An ascii chart
  592. is available by pressing F3
  593.  
  594. May be selected outside of menu by pressing F8
  595.  
  596.  
  597.                              COPY COLUMN
  598.                                                          -----------
  599.  
  600. Make a copy of the current column. The duplicate column will be placed
  601. to the right of the current column.
  602.  
  603. May be selected outside of menu by pressing the letter 'P'
  604.  
  605.  
  606.                             DELETE COLUMN
  607.                             -------------
  608.  
  609. Permanently remove the current column. Caution, there is no UNDO
  610. feature! Save your work frequently!
  611.  
  612. May be selected outside of menu by pressing the Delete key
  613.  
  614.  
  615.  
  616.                          EDIT COLUMN CONTENTS
  617.                          --------------------
  618.  
  619. Make changes to the column contents. When you insert a field, the
  620. column data might    look like customer->name but you may want
  621. to show the    names in upper case so change to UPPER(customer->name)
  622.  
  623. You can also perform a calculation such as:
  624.  
  625. transform(item->qty * item->unit, '999,999.99')
  626.  
  627. How 'bout displaying the first 20 characters of a wide field -
  628.  
  629. SUBSTR(item->desc, 1, 20)
  630.  
  631. Other things related to the current column such as heading, footing,
  632. various separators, column width, allowing the column to be editable,
  633. edit color, picture when and valid plus column totals may also be
  634. changed at this menu.
  635.  
  636. Data columns which are numeric may be optionally totaled and placed in the
  637. column footing with the "TOTAL COLUMN" option. After entering "Y" for
  638. "COLUMN TOTAL" you then enter the "COLUMN EXPRESSION". This expression
  639. will be the same as the expression in "COLUMN DATA"    provided that the
  640. expression will evaluate as numeric.
  641.  
  642. Example: OK       invoice->namount
  643.                  OK          item->qty * item->unit
  644.                  NOT OK   transform(invoice->namount, '99,999,999.99')
  645.  
  646. Although the last example is working with NUMERIC data, the transform()
  647. makes the data CHARACTER therefore it cannot be totaled. So we'll use
  648. the first example: invoice->namount for the "COLUMN EXPRESSION".
  649.  
  650. Lastly you need to enter a "TOTAL PICTURE" such as 99,999,999.99 or
  651. 99999999.99 othewise Quik TBrowse will make a best guess which may
  652. not be what you wanted.
  653.  
  654. Your "TOTAL COLUMNS" will appear once you have compiled the generated
  655. code.
  656.  
  657. May be selected outside of menu by pressing the letter 'E'
  658.  
  659.  
  660.  
  661.                         USER EDITABLE COLUMNS
  662.                         ---------------------
  663.  
  664. Select which columns may be edited by your user. Columns which
  665. contain data (not calculations) may be editable. You MUST enter
  666. the field name and optionally you can add a picture statement,
  667. edit color, WHEN and VALID condition.
  668.  
  669. The WHEN and VALID conditions must be expressed as code blocks.
  670.  
  671. WHEN  example: {|| LetMeIn() }
  672.  
  673. If LetMeIn() returns .T. the user may enter the GET.
  674.  
  675. VALID example: {|get| YourFunc(get:varget() }
  676. Note how the var gets passed to the function. If YourFunc() returns
  677. .t. the user will be allowed to exit the GET.
  678.  
  679. The default for WHEN and VALID is {|| .t.} meaning always allow the
  680. user to enter (provided the column is editable) and exit the GET.
  681.  
  682. May be selected outside of menu by pressing the letter 'U'
  683.  
  684.  
  685.  
  686.                            FREEZE AT COLUMN
  687.                            ----------------
  688.  
  689. Keeps the column number entered and all columns to the left
  690. visible at all times.
  691.  
  692. May be selected outside of menu by pressing the letter 'F'
  693.  
  694.  
  695.  
  696.                             INSERT COLUMN
  697.                             -------------
  698.  
  699. Add a column to the right of the current column. To add a column
  700. at the left most position, place it at the far right and use the
  701. 'Rotate' or 'Move' options to reposition it.
  702.  
  703. You may select a field from a list or enter an expression (see
  704. Edit Expression). The fields list    includes only fields from the
  705. .dbf file associated with the current browse. You may add a field
  706. from another database provided that it is associated with another
  707. browse which is on the screen at the time.
  708. Fields selected from other databases may not be user editable. This
  709. will be rectified in a future version.
  710.  
  711. May be selected outside of menu by pressing the Insert key
  712.  
  713.  
  714.  
  715.                          MOVE CURRENT COLUMN
  716.                          -------------------
  717.  
  718. Reposition current column to left or right with the arrow keys.
  719.  
  720. May be selected outside of menu by pressing the letter 'M'
  721.  
  722.  
  723.  
  724.                             ROTATE COLUMNS
  725.                             --------------
  726.  
  727. Shift all columns to the left or right with the arrow keys.
  728.  
  729. May be selected outside of menu by pressing the letter 'R'
  730.  
  731.  
  732.  
  733.                              COLUMN WIDTH
  734.                              ------------
  735.  
  736. Set the data columns width.    A number too large with result in
  737. gap-osis (a hole)    between ajacent columns. A number too small
  738. will truncate the data.
  739.  
  740. May be selected outside of menu by pressing the letter 'W'
  741.  
  742.  
  743.  
  744.  
  745. USER MENU
  746. =========
  747.  
  748.  
  749.                        ALLOW RECORD DELETION
  750.                        ---------------------
  751.  
  752. Determines if your application will allow the user to delete
  753. records in the current browse. Records are not actually deleted
  754. but the contents are blanked.    These records are reused when the
  755. user adds a record provided one is available, otherwise a new record
  756. is appended.
  757.  
  758.  
  759.  
  760.                         ALLOW RECORD APPEND
  761.                         -------------------
  762.  
  763. Determines if your application will allow the user to add
  764. records to the current browse. Previously 'deleted' records
  765. are used first if available otherwise a new record is appended.
  766. When you allow records to be appended, you will be given a fields
  767. list so that default values may be assigned. For example, you
  768. may want the default item->invnum to be invoice->invnum. If need
  769. to increment a number based on the contents of a record you may
  770. use NextNumber().
  771.  
  772. Say you want to set the default customer->id based on the last
  773. customer->id + '1' enter NextNumber('customer', 'id', 1) or perhaps
  774. you need the next invoice number for invoice->invnum enter
  775. NextNumber('item', 'invnum', 1). The 1st parameter is the alias of
  776. the database to look at and the 2nd parameter is the field to
  777. increment. Parameter 3 is the index order. If you omit the 3rd
  778. parameter, or set it to zero, NextNumber() will increment the last
  779. physical record. Works with both numeric and character fields.
  780.  
  781.  
  782.  
  783.                         ALLOW RECORD SEARCH
  784.                                                 -------------------
  785.  
  786. Determine if your application will allow the user to search for
  787. records in ALL browses which have a controlling index. If a search is
  788. attempted in a browse without a controlling index you'll get a "quack".
  789.  
  790. To enable the search mechanism, the current browse must have a
  791. controlling index otherwise you will be prompted to set a controlling
  792. index prior to enabling the search. If the current browse does not have
  793. a controlling index either assign one from the FILES MENU or TAB to a
  794. browse which does have a controlling index prior to enabling the search
  795. mechanism.
  796.  
  797.  
  798.  
  799.  
  800. GENERATE MENU
  801. =============
  802.  
  803.                            TBROWSE DATABASE
  804.                            ----------------
  805.  
  806. This is the code generation option you'll probably use the most.
  807. Your TBrowse design will be running off a database (like you'd
  808. expect) as opposed to an array as with the next option.
  809.  
  810.  
  811.  
  812.                           TBROWSE HARD CODED
  813.                           ------------------
  814.  
  815. Huh? This option will populate an array with the contents of the
  816. database used in the design process. While this technique has
  817. limited usefulness it can come in handy. For example you may want
  818. to simulate a database or with a little inguinuity you can diplay a
  819. fields list or a directory, read on to find out how. You can also
  820. use this as a basis for multi-column pick lists, menus and the like.
  821.  
  822. Little attempt has been made to make this option as powerful as
  823. 'TBrowse Database'. For example, column expressions will be
  824. ignored and relations are not supported. Despite the limitations
  825. it may come in handy some day and for this reason it has been
  826. included.
  827.  
  828.  
  829. How to display a fields list:
  830.  
  831. What we are going to do is use any database that has a character
  832. field and a numeric field. Set up four columns, the first two
  833. need to be character and the last two need to be numeric. I know
  834. this sounds goofy but stick with me, it should make more sense
  835. in a minute. Quik TBrowse will only browse databases, not arrays
  836. even though it will produce code to browse an array. So what we're
  837. doing is using a dummy database as a 'template'.
  838.  
  839. Let's give this thing some titles so press F4 and enter the following:
  840. 'Name'
  841. 'Type'
  842. 'Length'
  843. 'Dec'
  844.  
  845. Next set the column widths as follows:
  846.  
  847. Column #   Width
  848. --------   -----
  849.    1        10
  850.    2         4
  851.    3         6
  852.    4         3
  853.  
  854. Ok now generate using TBrowse - Hard Coded.
  855.  
  856. Go into the file we just generated and make a few changes.
  857.  
  858. Locate the declaration: local obj
  859. at around line 34
  860.  
  861. On the next two lines add:
  862.  
  863. use somefile     // open a database
  864.  
  865. aData := dbstruct()
  866.  
  867.  
  868. Dbstruct() will load aData with an array whose length is that of the
  869. database in use. Each element of the array is a subarray containing
  870. information about one field. The subarrays have the following format.
  871.  
  872.  
  873.     Position     Metasymbol
  874.     --------     ----------
  875.     1          cName
  876.     2          cType
  877.     3          nLength
  878.     4          nDec
  879.  
  880.  
  881. Delete all occurances of:
  882.  
  883. aadd(adata, . . .
  884.  
  885.  
  886. Since Quik TBrowse will not support "Edit Expression" when generating
  887. a Hard-Coded array browse we'll need to do it manualy.
  888.  
  889. For column 3, locate the line that looks something like this
  890.      column := TBcolumnNew('', {|| trans(aData[nElement, 3], '999999.99') } )
  891.  
  892. Change to:
  893.    column := TBcolumnNew('', {|| trans(aData[nElement, 3], '99999') } )
  894.  
  895. This will display up to five numbers for the length column
  896.  
  897.  
  898.  
  899. For column 4, locate the line that looks like this
  900.    column := TBcolumnNew('', {|| trans(aData[nElement, 4], '999999.99') } )
  901.  
  902. Change to:
  903.    column := TBcolumnNew('', {|| trans(aData[nElement, 4], '99') } )
  904.  
  905. This will display up to two numbers for our decimals.
  906.  
  907.  
  908. That's it. Add one line, delete a bunch of lines then modify two lines.
  909.  
  910.  
  911. You can use the same technique to display a directory. Instead of four
  912. columns you can display a maximum of five. Our aData array will get
  913. populated using the directory() function.
  914.  
  915.      aData := directory()
  916.  
  917. Directory() will load aData with an array whose length is that of the
  918. files in the current directory. Each element of the array is a
  919. subarray containing information about one file. See your Norton Guides
  920. for further information. The subarrays have the following format.
  921.  
  922.  
  923.     Position     Metasymbol
  924.     --------     ----------
  925.     1          cName
  926.     2          cSize
  927.     3          dDate
  928.     4          cTime
  929.     5          cAttributes
  930.  
  931.  
  932.  
  933.                         VALID POP-UP FUNCTION
  934.                         ---------------------
  935.  
  936. Create an ACHOICE() valid pop-up function
  937.  
  938. Question: What do ACHOICE() and TBrowse have in common?
  939. Answer:   Very little.
  940.  
  941. Actually what they do have in common is Quik TBrowse which is used to
  942. paint the pop-up. After generation you get a program named
  943. popdemo.prg which will contain your valid pop-up function which is
  944. based on an ACHOICE() window which displays the contents of a hard
  945. coded array that was populated by the database used during the
  946. design.  Since this is ACHOICE() and NOT TBrowse there are a limited
  947. number of supported options namely box, color, shadow and header
  948. title. Unsupported options will be ignored.
  949.  
  950. To try it out fire up Quik TBrowse using state.csb    and generate a
  951. Pop-up valid. Compile and link your program, later you'll take just
  952. the generated function from the program but for now we'll use the
  953. whole thing to check out our function. When you start your newly
  954. generated program there will be a prompt to "Test your valid
  955. function", type in 'ZZ' ' (I know there's no state abreviation ZZ) to
  956. activate the function. The function will scan its array for 'ZZ'. If
  957. it can't find it, ACHOICE() will be activated showing you a list of
  958. states. Pick one.
  959.  
  960. In our example we get something like CA-California. Oops, we only
  961. want the first two characters so find the line (around line 136)
  962. that looks like this:
  963.  
  964. getactive():varput(aName[nSelection])
  965.  
  966. Change it to look like this:
  967.  
  968. getactive():varput( substr(aName[nSelection], 1, 2) )
  969.  
  970.  
  971.  
  972.  
  973. PART TWO
  974. --------
  975.  
  976. Beginning Tbrowse
  977.  
  978. In S87, dbedit() introduced us to a new way of browsing databases
  979. and, after a few attempts, most of us were amazed at the
  980. capabilities that command gave us. In some ways, Tbrowse can be
  981. considered the replacement for dbedit() since it also allows you
  982. to browse a database in a window under program control.
  983.  
  984. In fact, Tbrowse is actually a lot more powerful than dbedit()
  985. ever was-so powerful that the dbedit() included with Clipper
  986. 5.01 was written in Clipper using Tbrowse. Most of the power
  987. inherent in Tbrowse comes from its open architecture, which gives
  988. the programmer access to almost all of its "internals". (They
  989. aren't really internals, but when you read the list of all of the
  990. controls, it seems that way.)
  991.  
  992. You'll need to understand three main things before starting with
  993. Tbrowse:
  994.  
  995. (1) How to structure an object.
  996. (2) How to write a codeblock.
  997. (3) How to send messages to an object.
  998.  
  999. Neither is very difficult. First, I'll show you how to create the
  1000. codeblocks you'll need; second, how to talk to objects.
  1001.  
  1002. Objects
  1003.  
  1004. Tbrowse is object-based, so I'll be using some object-oriented
  1005. language. Before I start, I want to define a few terms so you'll
  1006. understand what I'm talking about later in this discussion.
  1007.  
  1008. An "object" is a collection of data (variables) and code
  1009. (procedures or functions).
  1010.  
  1011. An "instance variable" is a data element in an object. In a
  1012. Tbrowse object, four of the instance variables are top, bottom,
  1013. left and right. These instance variables contain the dimensions
  1014. of the indicated Tbrowse.
  1015.  
  1016. A "method" is the code of the object. When you start writing your
  1017. own objects, you'll see that a method is just another name for a
  1018. function or procedure which is bound to the object. In a Tbrowse
  1019. object, four of the methods are top(), bottom(), left() and
  1020. right(). These methods implement some of the cursor movement
  1021. capabilities in Tbrowse.
  1022.  
  1023. "Send" or "message send" is used to describe how we communicate
  1024. with objects. As we said above, an object is a collection of
  1025. code and data seemingly contained in a variable. In order to send
  1026. something, an order or request (called a "message") is sent to
  1027. the object using the send operator, better known as a colon. A
  1028. send message can cause one of three things to happen: (1) a
  1029. method within the object executing; (2) an assignment being made
  1030. to an instance variable; or (3) a value of an instance variable
  1031. being returned. Sending a message can look like any of these:
  1032.  
  1033. object:message
  1034. object:message()
  1035. object:message(<parameter list>)
  1036.  
  1037. Any of these formats may return values. As a general rule, the
  1038. first example would be expected to return a value; the second
  1039. and third options may or may not.
  1040.  
  1041.  
  1042. Codeblocks
  1043.  
  1044. A codeblock consists of a Clipper expression surrounded by curly
  1045. braces and preceded by pipes, like this:
  1046.  
  1047. {|| <expression> }
  1048.  
  1049. For use with Tbrowse, that's about all you need to know about
  1050. codeblocks. The expression will be the regular Clipper code you
  1051. always use. Following are codeblock examples which define what to
  1052. display in various columns:
  1053.  
  1054. {|| clients->lastname }
  1055. {|| upper( clients->state ) }
  1056. {|| transform( clients->phone, "@R 999/999-9999" ) }
  1057. {|| padr(trim(clients->city)+" "+clients->state+" "+clients->zip, 35 ) }
  1058. {|| if(deleted(), "Deleted", "       " }
  1059.  
  1060. Make sure that the expression always returns a constant string
  1061. length. In the above examples, the string length is the width of
  1062. the designated field. Display expressions are evaluated when the
  1063. column is defined, using the current record at the time. If, for
  1064. example, you use a trim() in your expression and the current
  1065. record is blank or has no data in the key field, the expression
  1066. would return a string length of 0-and you don't have a column.
  1067.  
  1068. It is legal to use multiple expressions in a codeblock; just
  1069. separate the expression with commas. An example where this might
  1070. be useful is shown here:
  1071.  
  1072. {|| lineitem->(dbseek(invoice->inv_no, .f.)), lineitem->qty }
  1073.  
  1074. Notice that the first half of the expression seeks for a record
  1075. in the lineitem file and the second half returns the value of
  1076. the field qty in the found record. Also note that I turned off
  1077. soft seek using dbseek()'s optional parameter .f.; if a seek()
  1078. fails, the record pointer will be positioned at end of file.
  1079.  
  1080. Sending Messages
  1081.  
  1082. Next you need to know how to send messages and assignments to
  1083. objects and how to request information from objects. This is
  1084. even simpler than codeblocks and involves only the colon
  1085. character. Here are some examples of this:
  1086.  
  1087. browse := TbrowseNew( 5, 5, 17, 65 )   // Create a Tbrowse object
  1088. browse:top := 4      // Reset the top of the Tbrowse to row 4
  1089. ? browse:left        // Print the left column of the specified Tbrowse
  1090. browse:refreshall()  // Tell the Tbrowse that all of the displayed data
  1091.                      // needs refreshing
  1092. Finally, Tbrowse
  1093.  
  1094. Now on to the real topic of the day, Tbrowse. In this class, I'll
  1095. introduce you to Tbrowse and give you enough information so you
  1096. can go home and write your own simple Tbrowses. Tbrowse is a
  1097. relatively generic browsing mechanism which is capable of
  1098. browsing anything; however, in this class we'll concentrate on
  1099. browsing databases.
  1100.  
  1101. The first step in using Tbrowse is to create a Tbrowse object.
  1102. The only information you need to supply is a set of coordinates.
  1103. The syntax looks like this:
  1104.  
  1105. browse_object := tbrowsedb( <top>,  <left>, <bottom>, <right> )
  1106.  
  1107. Tbrowse Columns
  1108.  
  1109. At this point, you have a Tbrowse object which knows how big it
  1110. is and also contains bits of code which tell it how to move
  1111. around in a database, but it doesn't yet have any information
  1112. about what to display. To tell the Tbrowse object what to
  1113. display, we must define columns for it. The command to create a
  1114. column is tbcolumnnew(), which creates a tbcolumn object. This
  1115. column object has two properties which we will consider now and
  1116. which are defined in the call to tbcolumnnew(). They are: (1)
  1117. the title, which is displayed at the top of the column and (2)
  1118. the display block, which defines the information to be displayed
  1119. in each row of the column. Here are a couple examples of creating
  1120. Tbrowse columns:
  1121.  
  1122. column := tbcolumnnew( "Name", {|| clients->name } )
  1123. column := tbcolumnnew( "Phone", ;
  1124.                      {|| transform(clients->phone, "@R 999/999-9999") } )
  1125. column := tbcolumnnew( "Record", {|| clients->( recno() ) } )
  1126. column := tbcolumnnew( "Deleted", ;
  1127.                      {|| if(clients->(deleted()), "Yes", "No " ) } )
  1128.  
  1129. Now that we've got all the necessary pieces for the Tbrowse to
  1130. work, we need to put them together. For this we'll learn about
  1131. addcolumn(), a method of Tbrowse which allows us to add columns
  1132. to the Tbrowse object. The columns will appear in the order they
  1133. are added from left to right.
  1134. Addcolumn() is used like this:
  1135.  
  1136. browse := tbrowsedb( 5, 5, 17, 65 )
  1137. column := tbcolumnnew( "First;Name", {|| clients->fname } )
  1138. browse:addcolumn( column )
  1139. column := tbcolumnnew( "Last;Name", {|| clients->fname } )
  1140. browse:addcolumn( column )
  1141.  
  1142. Or alternatively:
  1143.  
  1144. browse := tbrowsedb( 5, 5, 17, 65 )
  1145. browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
  1146. browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )
  1147.  
  1148. The advantage of the second set of examples is that it uses fewer
  1149. lines of code, slightly less memory, and makes the column object
  1150. available for modification; the disadvantage is that it might be
  1151. harder to read, especially as the display blocks get long and
  1152. complicated. You'll notice that I used semi- colons in the
  1153. headings ("First;Name"). Like dbedit(), Tbrowse has automatic
  1154. headings which will accept the semi-colon as a new line
  1155. character-so the headings for this Tbrowse will occupy 2 lines.
  1156.  
  1157. Displaying the Tbrowse
  1158.  
  1159. By now, you've learned how to create a Tbrowse which contains
  1160. everything necessary to function; but if you were to compile,
  1161. link and run the previous sample, you'd find that nothing
  1162. displayed. Tbrowse, unlike dbedit(), is completely under program
  1163. control and your program will need to tell the Tbrowse object to
  1164. display. The stabilize() method implements incremental
  1165. stabilization and display and returns a logical containing the
  1166. status of the display effort. The following code allows us to
  1167. display the Tbrowse object:
  1168.  
  1169. browse := tbrowsedb( 5, 5, 17, 65 )
  1170. browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
  1171. browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )
  1172.  
  1173. do while ! browse:stabilize()
  1174. enddo
  1175.  
  1176. Tbrowse Keystrokes
  1177.  
  1178. Not bad so far. In only 5 lines of code, we've created a simple
  1179. 2-column Tbrowse and drawn it on the screen. However, if you run
  1180. that code, you'll find that the Tbrowse will display itself as
  1181. expected and then just exit to DOS... not at all the result you
  1182. actually want. One of the strengths of Tbrowse is its ability to
  1183. do anything you want. The downside (if you want to consider it
  1184. that) is that, at least until you get some generic key handling
  1185. code written, it seems to take more lines of code for a Tbrowse
  1186. than it did for a dbedit(). The following code demonstrates a
  1187. simple key handling loop:
  1188.  
  1189. browse := tbrowsedb( 5, 5, 17, 65 )
  1190. browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
  1191. browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )
  1192.  
  1193.  
  1194. do while lastkey() <> K_ESC
  1195.     do while ! browse:stabilize()
  1196.     enddo
  1197.     key := inkey(0)
  1198.     do case
  1199.         case key == K_UP
  1200.             browse:up()
  1201.         case key == K_DOWN
  1202.             browse:down()
  1203.         case key == K_LEFT
  1204.             browse:left()
  1205.         case key == K_RIGHT
  1206.             browse:right()
  1207.         case key == K_PGDN
  1208.             browse:pagedown()
  1209.         case key == K_PGUP
  1210.             browse:pageup()
  1211.         case key == K_HOME
  1212.             browse:gotop()
  1213.         case key == K_END
  1214.             browse:gobottom()
  1215.     endcase
  1216. enddo
  1217.  
  1218. This example clearly demonstrates how simple it is to control a
  1219. Tbrowse. Most of the Tbrowse methods in this example are
  1220. self-explanatory-up(), down(), gotop() and the others should be
  1221. clearly obvious. The one thing you might want to notice is that
  1222. I've defined the <home> and <end> keys differently than you
  1223. might be used to seeing. I find these keys more user-friendly
  1224. than the old <Ctrl- PgUp> and <Ctrl-PgDown>, and as you can see
  1225. it was a trivial matter to change them.
  1226.  
  1227. Tbrowse Optimization
  1228.  
  1229. One problem with the previous example is that you have to wait
  1230. for the Tbrowse to stabilize before another keystroke will be
  1231. accepted. For keys like K_UP and K_DOWN, the stabilize is short
  1232. enough that it hardly matters; but for K_PGUP and K_PGDN, the
  1233. whole screen has to be refreshed for every keystroke. This isn't
  1234. really a problem unless your clients are on slow machines, or
  1235. you've designed a complicated or very large Tbrowse and someone
  1236. wants to move down 4 pages. The following piece of code shows
  1237. how to short-circuit the stabilization loop and speed up large
  1238. movements through the Tbrowse:
  1239.  
  1240.     do while nextkey() == 0 .and. ! browse:stabilize()
  1241.   enddo
  1242.  
  1243. At this point, we have a completely usable Tbrowse and you now
  1244. have all the knowledge you need to start using Tbrowse in your
  1245. applications. Here is a fully commented version of all we've
  1246. learned so far:
  1247.  
  1248. // First the tbrowse object, browse, is created.
  1249. browse := tbrowsedb( 5, 5, 17, 65 )
  1250.  
  1251. // Next two columns are added
  1252. browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
  1253. browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )
  1254.  
  1255. // Next we'll enter the key processing loop.  I've chosen to put the
  1256. // exit condition in the loop, but you could just as easily make the
  1257. // loop "while .t." and put // the exit condition in the case statement.
  1258.  
  1259. do while lastkey() <> K_ESC
  1260.  
  1261.     // This is the stabilization/display loop with added exit on
  1262.   // keystroke to speed up the processing of closely spaced keys
  1263.  
  1264.     do while nextkey() == 0 .and. ! browse:stabilize()
  1265.     enddo
  1266.     key := inkey(0)
  1267.  
  1268.     // This is the keystroke processing case statement. This structure
  1269.   // needs a case for every keystroke you want to process during the
  1270.   // Tbrowse. Since this uses inkey(0) to get keystrokes, you'll also
  1271.   // need to handle all of the SET KEYs and any other keystrokes you
  1272.   // take for granted.
  1273.  
  1274.     do case
  1275.         case key == K_UP
  1276.             browse:up()
  1277.         case key == K_DOWN
  1278.             browse:down()
  1279.         case key == K_LEFT
  1280.             browse:left()
  1281.         case key == K_RIGHT
  1282.             browse:right()
  1283.         case key == K_PGDN
  1284.             browse:pagedown()
  1285.         case key == K_PGUP
  1286.             browse:pageup()
  1287.         case key == K_HOME
  1288.             browse:gotop()
  1289.         case key == K_END
  1290.             browse:gobottom()
  1291.     endcase
  1292. enddo
  1293.  
  1294. Enhancements
  1295.  
  1296. Now that we've got Tbrowse up and running, you'll probably want
  1297. to make it better. What I consider "better" is probably
  1298. different than what you'd consider "better", but I'll go through
  1299. as many enhancements as seem reasonable for this session. The
  1300. first thing we'll look at is handling the special keys: help,
  1301. SET KEYs and the like. One of the things we know about SET KEYs
  1302. in Clipper 5.01 is that they all are set using setkey() and that
  1303. we can use setkey() to inquire into the status of any key. If
  1304. you've gone snooping around in getsys.prg, you've probably seen
  1305. this or a similar piece of code:
  1306.  
  1307. key := inkey( 0 )
  1308. if setkey( key ) <> NIL
  1309.     eval( key, procname(), procline(), "Var" )
  1310. endif
  1311.  
  1312. This code is what implements the evaluation of SET KEYs in
  1313. Clipper code in Clipper itself. For a Tbrowse key processing
  1314. loop, you'll probably want to make one change-if the called
  1315. function has knowledge of the Tbrowse, it should have the
  1316. ability to control what happens after it returns.
  1317.  
  1318. key := inkey( 0 )
  1319. if setkey( key ) <> NIL
  1320.     key := eval( key, procname(), procline(), "Var" )
  1321. endif
  1322.  
  1323. With the one addition of key := eval(...), the called function
  1324. suddenly has the ability to control the operation of the Tbrowse
  1325. within a very limited range. I'd guess that the most likely
  1326. return values would be 0 (to do nothing) and 27 (to exit).
  1327. Remember, so far the code doesn't check the type of the return
  1328. value. I made the assumption that most SET KEYs would be
  1329. procedures and return NIL. Since that could possibly be an
  1330. invalid assumption, I'll toss in some error checking in this next
  1331. example:
  1332.  
  1333. key := inkey( 0 )
  1334. if setkey( key ) <> NIL
  1335.     key := eval( key, procname(), procline(), "Var" )
  1336.     if valtype( key ) <> "N"
  1337.         loop
  1338.     endif
  1339. endif
  1340.  
  1341. Searching
  1342.  
  1343. The next enhancement you might want to make is the ability to
  1344. search for a particular record or range of records while
  1345. Tbrowsing. The easy way would be to use a pop-up box for the
  1346. search string, seek, and then redisplay the browse if the new
  1347. selection is found. That piece of code is pretty simple. Here
  1348. I'll show pseudo-code because I don't want to implement all of
  1349. the box stuff:
  1350.  
  1351. // Place this as a case in the key handling case
  1352. case key == K_SEARCH_KEY
  1353.     // Draw pretty box
  1354.     oldrec := recno
  1355.     @ row, col get search_string
  1356.     read
  1357.     seek search_string
  1358.     if found()
  1359.         browse:refreshall()
  1360.     else
  1361.         // Put up "search failed" message
  1362.         goto (oldrec)
  1363.     endif
  1364.     // Undraw pretty box
  1365.  
  1366. The only new thing in this code is the browse:refreshall().
  1367. Remember that Tbrowse doesn't automatically show everything you
  1368. do, so the movement of the record pointer will not show up on
  1369. screen unless we tell Tbrowse to refresh the screen. For this,
  1370. we'll use refreshall() so all the data in all the rows will be
  1371. refreshed. The reason for refreshing all of the data is that the
  1372. seek probably moved the record pointer, which means that all the
  1373. data in all the rows will have changed. Notice also that on a
  1374. failed seek, I just put the record pointer back to its original
  1375. position and return. In this case, since nothing has changed,
  1376. the Tbrowse does not need refreshing.  A more interesting way to
  1377. implement searching is called incremental searching. This is
  1378. where you type on the fly and a new search is made as each
  1379. letter is added or deleted from the string. The only difficult
  1380. part of this is storing the search string and deciding how to
  1381. implement the search. In this example, if a letter causes a
  1382. failed search, it will be disregarded and only the remaining
  1383. letters will be used. The example respects upper and lower case
  1384. letters, but the addition of upper() or lower() would make the
  1385. search case insensitive. You'll need to make it reflect the state
  1386. of the current index.
  1387.  
  1388. // The first case is where an alpha key has been hit and we want
  1389. // to search for the current search string.
  1390.  
  1391. case isalpha( chr( key ) )
  1392.  
  1393.     // First we'll save the current recno() and try seeking for the
  1394.   // new key
  1395.  
  1396.     oldrec := recno()
  1397.     if dbseek( srch_string+chr( key ), .f. )
  1398.  
  1399.         // If the search succeeds, we'll add the new letter to the
  1400.     // search string, refresh the browse and show the search
  1401.     // string in some appropriate location.
  1402.  
  1403.         srch_string += chr( key )
  1404.         browse:refreshall()
  1405.         @ row, col say padr( srch_string, 15 )
  1406.     else
  1407.  
  1408.         // else if it fails we just put the record pointer back to
  1409.     // its original position.
  1410.         goto (oldrec)
  1411.     endif
  1412.  
  1413. // The second case is where a <backspace> was hit and we need to
  1414. // shorten the search string and then seek on the new shorter
  1415. // string.
  1416.  
  1417. case key == K_BKSP
  1418.  
  1419.     // If the search string is empty, nothing needs to be done;
  1420.   // otherwise we'll nick one letter off of the end, seek on the
  1421.   // shorter string, refresh the Tbrowse, and then redisplay the
  1422.   // shorter search string.
  1423.  
  1424.     if len(srch_string) > 0
  1425.          srch_string := subs( srch_string, 1, len( srch_string )-1 )
  1426.          dbseek( srch_string+ckey, .f. )
  1427.          browse:refreshall()
  1428.          @ row, col say padr( srch_string, 15 )
  1429.     endif
  1430.  
  1431.  
  1432. The Final Example
  1433.  
  1434. The last thing we'll look at is the complete, assembled example:
  1435.  
  1436. #include "inkey.ch"
  1437. Function browser
  1438.  
  1439. local browse
  1440. local key
  1441. local srch_string := ""
  1442.  
  1443. use clients
  1444.  
  1445. browse := tbrowsedb( 5, 5, 17, 65 )
  1446.  
  1447. browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
  1448. browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )
  1449.  
  1450. do while lastkey() <> K_ESC
  1451.  
  1452.     do while nextkey() == 0 .and. ! browse:stabilize()
  1453.     enddo
  1454.  
  1455.     key := inkey(0)
  1456.  
  1457.     do case
  1458.         case key == K_UP
  1459.             browse:up()
  1460.  
  1461.         case key == K_DOWN
  1462.             browse:down()
  1463.  
  1464.         case key == K_LEFT
  1465.             browse:left()
  1466.  
  1467.         case key == K_RIGHT
  1468.             browse:right()
  1469.  
  1470.         case key == K_PGDN
  1471.             browse:pagedown()
  1472.  
  1473.         case key == K_PGUP
  1474.             browse:pageup()
  1475.  
  1476.         case key == K_HOME
  1477.             browse:gotop()
  1478.  
  1479.         case key == K_END
  1480.             browse:gobottom()
  1481.  
  1482.         case isalpha( chr( key ) )
  1483.             oldrec := recno()
  1484.             if dbseek( srch_string+chr( key ), .f. )
  1485.                 srch_string += chr( key )
  1486.                 browse:refreshall()
  1487.                 @ row, col say padr( srch_string, 15 )
  1488.             else
  1489.                 goto (oldrec)
  1490.             endif
  1491.  
  1492.         case key == K_BKSP
  1493.             if len(srch_string) > 0
  1494.                 srch_string := subs( srch_string, 1, len( srch_string )-1 )
  1495.                 dbseek( srch_string+ckey, .f. )
  1496.                 browse:refreshall()
  1497.                 @ row, col say padr( srch_string, 15 )
  1498.             endif
  1499.  
  1500.     endcase
  1501. enddo
  1502.  
  1503. return NIL
  1504.  
  1505.  
  1506.  
  1507. Table of Tbrowse methods and instance variables
  1508. -----------------------------------------------
  1509.  
  1510. Tbrowse Class Functions
  1511. -----------------------
  1512.  
  1513. TbrowseNew() Create a new Tbrowse object
  1514.  
  1515. TbrowseDB()     Create a new Tbrowse object for browsing a database file
  1516.  
  1517.  
  1518.  
  1519. Tbrowse Exported Instance Variables
  1520. -----------------------------------
  1521. autoLite          Logical value to control highlighting
  1522.  
  1523. cargo                  User-definable variable
  1524.  
  1525. colCount          Number of browse columns
  1526.  
  1527. colorSpec          Color table for the Tbrowse display
  1528.  
  1529. colPos              Current cursor column position
  1530.  
  1531. colSep              Column separator character
  1532.  
  1533. freeze              Number of columns to freeze
  1534.  
  1535. goBottomBlock    Code block executed by Tbrowse:goBottom()
  1536.  
  1537. goTopBlock        Code block executed by Tbrowse:goTop()
  1538.  
  1539. headSep                Heading separator character
  1540.  
  1541. hitBottom            Indicates the end of available data
  1542.  
  1543. hitTop                Indicates the beginning of available data
  1544.  
  1545. leftVisible        Indicates position of leftmost unfrozen column in display
  1546.  
  1547. nBottom                Bottom row number for the Tbrowse display
  1548.  
  1549. nLeft                    Left-most column for the Tbrowse display
  1550.  
  1551. nRight                Right-most column for the Tbrowse display
  1552.  
  1553. nTop                    Top row number for the Tbrowse display
  1554.  
  1555. rightVisible    Indicates position of rightmost unfrozen column in display
  1556.  
  1557. rowCount            Number of visible data rows in the Tbrowse display
  1558.  
  1559. rowPos                Current cursor row position
  1560.  
  1561. skipBlock            Code block used to reposition data source
  1562.  
  1563. stable                Indicates if the Tbrowse object is stable
  1564.  
  1565.  
  1566.  Tbrowse Exported Methods
  1567.  ------------------------
  1568.  
  1569.  Cursor Movement Methods
  1570.  -----------------------
  1571.  
  1572. down()                   Moves the cursor down one row
  1573.  
  1574. end()                       Moves the cursor to the right-most visible data column
  1575.  
  1576. goBottom()           Repositions the data source to the bottom-of-file
  1577.  
  1578. goTop()                   Repositions the data source to the top-of-file
  1579.  
  1580. home()                   Moves the cursor to the left-most visible data column
  1581.  
  1582. left()                   Moves the cursor left one column
  1583.  
  1584. pageDown()           Repositions the data source downward
  1585.  
  1586. pageUp()               Repositions the data source upward
  1587.  
  1588. panEnd()               Moves the cursor to the right-most data column
  1589.  
  1590. panHome()               Moves the cursor to the left-most visible data column
  1591.  
  1592. panLeft()               Pans left without changing the cursor position
  1593.  
  1594. panRight()           Pans right without changing the cursor position
  1595.  
  1596. right()                   Moves the cursor right one column
  1597.  
  1598. up()                       Moves the cursor up one row
  1599.  
  1600.  
  1601. Miscellaneous Methods
  1602. ---------------------
  1603.  
  1604. addColumn()      Adds a TBColumn object to the Tbrowse object
  1605.  
  1606. colorRect()           Alters the color of a rectangular group of cells
  1607.  
  1608. colWidth()           Returns the display width of a particular column
  1609.  
  1610. configure()           Reconfigures the internal settings of the Tbrowse object
  1611.  
  1612. deHilite()           De-highlights the current cell
  1613.  
  1614. delColumn()           Delete a column object from a browse
  1615.  
  1616. getColumn()           Gets a specific TBColumn object
  1617.  
  1618. hilite()               Highlights the current cell
  1619.  
  1620. insColumn()           Insert a column object in a browse
  1621.  
  1622. invalidate()       Forces redraw during next stabilization
  1623.  
  1624. refreshAll()       Causes all data to be refreshed during the next stabilize
  1625.  
  1626. refreshCurrent() Causes the current row to be refreshed on next stabilize
  1627.  
  1628. setColumn()             Replaces one TBColumn object with another
  1629.  
  1630. stabilize()             Performs incremental stabilization
  1631.  
  1632.  
  1633. Tbcolunm Class Function
  1634. -----------------------
  1635.  
  1636. TBColumnNew()         Create a new TBColumn object
  1637.  
  1638.  
  1639. Tbcolumn Exported Instance Variables
  1640. ------------------------------------
  1641.  
  1642. block                         Code block to retrieve data for the column
  1643.  
  1644. cargo                         User-definable variable
  1645.  
  1646. colorBlock             Code block that determines color of data items
  1647.  
  1648. colSep                     Column separator character
  1649.  
  1650. defColor                 Array of numeric indexes into the color table
  1651.  
  1652. footing                     Column footing
  1653.  
  1654. footSep                     Footing separator character
  1655.  
  1656. heading                     Column heading
  1657.  
  1658. headSep                     Heading separator character
  1659.  
  1660. width                         Column display width
  1661.  
  1662.  
  1663.  
  1664.  
  1665. More Advanced Tbrowse
  1666.  
  1667. Invalidate() and Configure()
  1668.  
  1669. As we get into the more advanced aspects we'll need to know about
  1670. two more Tbrowse methods, invalidate() and configure().
  1671. Invalidate() is used when you've changed the configuration of the
  1672. Tbrowse and want to redraw it with the new definitions., You'd
  1673. probably use this if you were resizing a Tbrowse. Configure() is
  1674. used when you've changed information in the Tbrowses' columns.
  1675. Tbrowse will remain unaware of column changes until you tell it
  1676. to look using configure(). This behavior comes from Clipper's
  1677. attempts to speed Tbrowse up by reading a bunch of information
  1678. into internal storage. You would use configure if you had changed
  1679. the DefColor or Block variables of a Tbrowse column.
  1680.  
  1681. Changing properties of Tbcolumn objects
  1682.  
  1683. In most of the previous examples I've added columns to a Tbrowse
  1684. like this:
  1685.  
  1686. browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
  1687.  
  1688. But in the following sections we're going to find it necessary to
  1689. modify instance variables of the columns at definition time.
  1690. This means we'll want to create our columns by assigning them to
  1691. variables, make all of the changes we want and then finally add
  1692. the columns to the Tbrowse, like this:
  1693.  
  1694. browse := Tbrowsedb( 5, 5, 17, 65 )
  1695.  
  1696. column := TbcolumnNew( "First;Name", {|| clients->fname } )
  1697. column:defColor := {2,3}
  1698. column:Width    := 9
  1699. browse:addcolumn( column )
  1700.  
  1701. At this point you have 2 choices, 1; you can use the same column
  1702. variable for every column. This means you only have one variable
  1703. which you don't really need any more after you've finished
  1704. building the Tbrowse; or 2; you can use a different variable or
  1705. array element for each column. This makes it easier to modify
  1706. columns after they've been added to the Tbrowse. Both approaches
  1707. are valid and since Clipper treats objects like arrays as far as
  1708. memory usage is concerned there is almost no memory hit from
  1709. keeping the second copies around. If you keep the second
  1710. reference around I'd suggest keeping an array of columns as then
  1711. it's easy to get at the columns by number. I don't want you to
  1712. get the idea that you need to save a copy of the column if you
  1713. intend to change it later. Tbrowse itself allows you to request
  1714. a pointer to a particular column which gives you everything you
  1715. need to change the definition of a column on the fly, like this:
  1716.  
  1717. browse := Tbrowsedb( 5, 5, 17, 65 )
  1718.  
  1719. column := TbcolumnNew( "First;Name", {|| clients->fname } )
  1720. column:defColor := {2,3}
  1721. column:Width    := 9
  1722. browse:addcolumn( column )
  1723.  
  1724. browse:GetColumn(1):width := 11
  1725.  
  1726. In this example after I was all done creating the Tbrowse I went
  1727. back in an changed the width  using the GetColumn() method of
  1728. Tbrowse to retrieve a copy of the specified column, in this case
  1729. column 1. At first that syntax looks rather odd, but it's
  1730. perfectly legit. Let's read it from left to right, first we get
  1731. the Tbrowse object, browse; next we send it the message
  1732. GetColumn(1) which tell it to give me a copy/reference pointer
  1733. to the first Tbcolumn object in the Tbrowse; and lastly we tell
  1734. the Tbcolumn object that we want to set it's width instance
  1735. variable to 11. With objects, just like with functions, you can
  1736. chain them as far as you want. At one time or another we've all
  1737. written code like this where we've nested functions 3 or 4 deep:
  1738.  
  1739. subs( upper( subs( fname, 1, 1  ) ) + ;
  1740.       lower( subs( fname, 2  )  ) + " " + ;
  1741.             trim( upper( lname )  ), 1, 35 )
  1742.  
  1743. You can do the same thing with objects or even though you probably
  1744. don't think about it that way, you do the same thing whenever you
  1745. use multi-dimensional arrays. If you specify an array like this,
  1746. array[1][2][5] what you're really saying is  give me the fifth
  1747. element of the second element of the first element of the array
  1748. and if you think about the way Clipper handles arrays that's
  1749. exactly how it seems to work internally.
  1750.  
  1751. Freezing Columns
  1752.  
  1753. Freezing columns was one of the things people always seemed to
  1754. want to do in dbedit() and now with Tbrowse not only is it
  1755. possible, it's downright simple. All you need to do to freeze
  1756. some number of the leftmost columns is to assign a number to the
  1757. Freeze instance variable like this:
  1758.  
  1759. // freeze the leftmost column
  1760. browse:Freeze := 1
  1761.  
  1762. Sometimes when you freeze columns you'd like to make sure the
  1763. user can't get into the frozen column and though there is no
  1764. built in method to keep the cursor out of a particular column the
  1765. implementation is almost trivial:
  1766.  
  1767. // freeze the leftmost column
  1768. browse:Freeze := 1
  1769.  
  1770. // After stabilization we'll need to make sure that the cursor is
  1771. // not in the first column. The browse:Right() will move the
  1772. // cursor to the right, off of the left most column
  1773.  
  1774. do while browse:ColPos < 2
  1775.     browse:Right()
  1776. enddo
  1777.  
  1778. do while nextkey() == 0 .and. ! browse:Stabilize()
  1779. enddo
  1780.  
  1781. There is also the case where you'd like to lock the cursor into a
  1782. column. In that case you can just assign a value to ColPos, like
  1783. this: "browse:ColPos := 3" and then not give them access to the
  1784. sideways movement methods.
  1785.  
  1786. Color in Tbrowse
  1787.  
  1788. Color in Tbrowse is based on pointers into the table defined it
  1789. the Tbrowse instance variable ColorSpec. The default value of
  1790. ColorSpec is defined by the setcolor() setting at the time the
  1791. Tbrowse is created. ColorSpec contains a character variable
  1792. though it's always felt to me like it should have been an array
  1793. of colors as all color references are made by specifying the
  1794. color number you want. You can assign any color string you want
  1795. to this variable like this:
  1796.  
  1797. browse:ColorSpec := "w/n, n/w, b/w, w/b, r/w, w/r, g/w, w/g"
  1798.  
  1799. As color strings this one is rather boring but it will do for
  1800. this discussion. The next color control you're likely to use is
  1801. the defColor instance variable belong to Tbcolumn. defColor is a
  1802. 2 element array containing numbers which tell Tbrowse which
  1803. colors to use for the column when it's highlighted, position 2
  1804. in the array, and when it's not, position 1 in the array. The
  1805. default value of this array is {1,2}. The defaults for ColorSpec
  1806. and defColor mean that the default Tbrowse colors will appear
  1807. just like the colors on a screen of gets. But if we look closer
  1808. and notice that defColor belongs to the column you'll realize
  1809. that different colors can contain different look-up tables. If we
  1810. assign {1, 2} to defColor in the first column and {3, 4} to
  1811. defColor in the second column of our Tbrowse we will have a 2
  1812. column Tbrowse with columns of different colors. The first column
  1813. being white on black with a black on white highlight and the
  1814. second being white on blue with a white on blue highlight.
  1815.  
  1816. This is a lot cooler than dbedit() ever was, but it's only the
  1817. beginning. The other color instance variable is ColorBlock. By
  1818. default ColorBlock is NIL and ignored, but you can assign it a
  1819. code block which returns a 2 element color array like ColorSpec
  1820. contains. If ColorBlock contains a code block this block is
  1821. evaluated every time before this row of this column is drawn and
  1822. that returned color array is used instead of the one contained
  1823. in defColor. Here is an example of a typical ColorBlock:
  1824.  
  1825. column:ColorBlock := {|| if(invoice->owed > 0, {5, 6}, {1, 2} }
  1826.  
  1827. This block will cause every negative number in this column to
  1828. display in red instead of blue. The flexibility embodied in the
  1829. ColorBlock is hard to fathom and limited basically only by your
  1830. imagination. The one thing to remember before you get to wild
  1831. with ColorBlock is that the Color Block is evaled for every row
  1832. drawn. If you have too complicated an expression or too many
  1833. columns with ColorBlocks you may start to see speed degradation
  1834. during repainting.
  1835.  
  1836. Automatic index order changing
  1837.  
  1838. Sometimes in a Tbrowse it's nice to be able to change the index
  1839. order. There are a couple of ways this is normally done. For
  1840. small fast browses with only a couple of indexes to chose from
  1841. the following works really well:
  1842.  
  1843. case key $ "oO"
  1844.     if indexord() == 3
  1845.         set order to 1
  1846.     else
  1847.         set order to (indexord()+1)
  1848.     endif
  1849.     @ row, col say {"Name ", "Zip  ", "Phone"}[indexord()]
  1850.  
  1851. In this case hitting the "O" key causes the browse to cycle
  1852. through the available indexes and paints the current index order
  1853. at some appropriate spot on the screen.
  1854.  
  1855. Another method would be to pop up an achoice() of available
  1856. indexes when the "O" key is hit and let them chose from the
  1857. available choices, In fact that could be as easy as this:
  1858.  
  1859. case key $ "oO"
  1860.     set order to achoice(5,5,10,10,{"Name", "Zip  ", "Phone"})
  1861.  
  1862.     @ row, col say {"     ", "Name ", "Zip  ", "Phone"}[indexord()+1]
  1863.  
  1864. One more method, my favorite is to have the index order depend on
  1865. the currently highlighted column. This requires some preparation
  1866. in that you'll need to have an index that corresponds with each
  1867. of the columns and you'll need to make sure that the order of
  1868. the indexes is the same as the order of the columns. The code for
  1869. this looks something like this:
  1870.  
  1871. use clients
  1872. set index to fname, lname, phone
  1873.  
  1874. browse := Tbrowsedb( 5, 5, 17, 65 )
  1875.  
  1876. browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
  1877. browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
  1878. browse:addcolumn( TbcolumnNew( "Phone:Number", {|| clients->phone } ) )
  1879.  
  1880. do while lastkey() <> K_ESC
  1881.  
  1882.     do while nextkey() == 0 .and. ! browse:Stabilize()
  1883.     enddo
  1884.  
  1885.     // this is the order changing code.
  1886.     // first we check to see if the index order is the same as the
  1887.     // current column number
  1888.  
  1889.     if indexord() <> browse:ColPos
  1890.  
  1891.         // if not we change the order, tell the Tbrowse to refresh the
  1892.         // data in all of the rows and then re-stabilize
  1893.  
  1894.         set order to (browse:ColPos)
  1895.         browse:RefreshAll()
  1896.         do while nextkey() == 0 .and. ! browse:Stabilize()
  1897.         enddo
  1898.     endif
  1899.  
  1900.     key := inkey(0)
  1901.  
  1902. Now that we know how to change the index order wouldn't it be
  1903. nice to be able to highlight the index order in a very obvious
  1904. way? The most obvious way is to just write indexkey(0) somewhere
  1905. on the screen. It's a given that we'll understand what that
  1906. means, but will our customers? With some of the keys I use there
  1907. is a good chance they would just be confused. If you happen to be
  1908. using the dBFSIX drive you could use sx_tagname() which returns
  1909. an English name for the index key which works much better and if
  1910. you're using ntx or ndx files you could just keep an array of
  1911. names hanging around and just display anames[indexord()+1], the
  1912. +1 taking care of those times when indexord is set to 0.
  1913.  
  1914. I tend to like more subtle but unmistakable signs, my favorite
  1915. being changing the color of the column which the order is
  1916. currently set too. If we implement a scheme for changing index
  1917. orders like the one above there are a couple of ways to
  1918. implement color columns. One uses a ColorBlock assigned to all
  1919. of the columns and looks like this:
  1920.  
  1921. column:ColorBlock := {|| if(browse:ColPos == indexord(), ;
  1922.                       {3, 4}, {1, 2} ) }
  1923.  
  1924. This is easy and automatic, but it's slower than necessary
  1925. because that block needs to be evaluated for ever data element
  1926. drawn by the Tbrowse. The way I'll show next might be a little
  1927. slower when you change orders because it involves re configuring
  1928. the Tbrowse, but it's faster at scrolling. On a 386 or better it
  1929. might not actually matter, but on a PC or slow 286 the
  1930. difference can be quite noticeable. The code for this method
  1931. looks like this:
  1932.  
  1933. use clients set index to fname, lname, phone
  1934.  
  1935. browse := Tbrowsedb( 5, 5, 17, 65 )
  1936.  
  1937. browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
  1938. browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
  1939. browse:addcolumn( TbcolumnNew( "Phone:Number", {|| clients->phone } ) )
  1940.  
  1941. // set the correct column to a different color
  1942. browse:GetColumn(indexord()):defColor := {3, 4}
  1943. do while lastkey() <> K_ESC
  1944.  
  1945.     do while nextkey() == 0 .and. ! browse:Stabilize()
  1946.     enddo
  1947.  
  1948.     // this is the order changing code.
  1949.     // first we check to see if the index order is the same as the
  1950.     // current column number
  1951.  
  1952.     if indexord() <> browse:ColPos
  1953.  
  1954.         // we know that the indexord() column is the one with the
  1955.     // different color setting, so we change it back to normal
  1956.  
  1957.         browse:GetColumn(indexord()):defColor := {1, 2}
  1958.  
  1959.         set order to (browse:ColPos)
  1960.  
  1961.         // Now we've changed the index order so we can now set the
  1962.     // defColor of the new column to the special color
  1963.  
  1964.         browse:GetColumn(indexord()):defColor := {3, 4}
  1965.         browse:RefreshAll()
  1966.  
  1967.         // additionally we'll need to tell the browse to configure()
  1968.     // itself so it picks up the color changes.
  1969.  
  1970.         browse:Configure()
  1971.         do while nextkey() == 0 .and. ! browse:Stabilize()
  1972.         enddo
  1973.     endif
  1974.  
  1975.     key := inkey(0)
  1976.  
  1977. The Highlight
  1978.  
  1979. Tbrowse automatically takes care of highlighting and
  1980. de-highlighting the current cell, but sometimes this can be a
  1981. problem, usually when you're trying for some sort of special
  1982. visual effect. Tbrowse provides for this with two methods,
  1983. HiLite() and DeHiLite() and an instance variable, AutoLite.
  1984. AutoLite controls the current state of the automatic highlighting
  1985. feature of Tbrowse and if AutoLite is false then HiLite() turns
  1986. the highlight on and DeHiLite() turns the highlight off,
  1987. otherwise if AutoLite is true the Tbrowse manages it
  1988. automatically. A simple example of using these methods follows.
  1989. Note that in this example I've slightly changed the stabilize
  1990. loop. This was done to make sure that the HiLite() method is not
  1991. called unless the Tbrowse is stable. Calling it when Tbrowse is
  1992. not stable will cause flickering, something it's nice to avoid.
  1993.  
  1994. browse := Tbrowsedb( 5, 5, 17, 65 )
  1995.  
  1996. browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
  1997. browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
  1998.  
  1999. // Turn off AutoLite
  2000. browse:AutoLite := .f.
  2001. do while lastkey() <> K_ESC
  2002.  
  2003.     // The loop now assigns key, avoiding the necessity to access
  2004.   // the "key := inkey(0) further down which would cause the
  2005.   // highlight to flicker.
  2006.  
  2007.     do while (key := inkey()) == 0 .and. ! browse:Stabilize()
  2008.     enddo
  2009.  
  2010.     if key == 0
  2011.         browse:HiLite()
  2012.         key := inkey(0)
  2013.         browse:DeHiLite()
  2014.     endif
  2015.  
  2016. Under some conditions where you want to manipulate the row
  2017. position by hand, like in the case where you want to maintain
  2018. the cursor in the center of the page and have the data scroll
  2019. over it. Though doable, this is one of those exercises that
  2020. demonstrates one of the Tbrowse's limitations: it does not
  2021. actually have a way to scroll the data independent of the
  2022. cursor. There is a really dirty way to accomplish this goal:
  2023.  
  2024. browse := Tbrowsedb( 5, 5, 17, 65 )
  2025.  
  2026. browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
  2027. browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
  2028.  
  2029. do while lastkey() <> K_ESC
  2030.  
  2031.     do while (key := inkey()) == 0 .and. ! browse:Stabilize()
  2032.     enddo
  2033.  
  2034.     if key == 0
  2035.         key := inkey(0)
  2036.     endif
  2037.     do case
  2038.         case key == K_UP
  2039.             skip -1
  2040.             browse:RefreshAll()
  2041.         case key == K_DOWN
  2042.             skip 1
  2043.             browse:RefreshAll()
  2044.         case key == K_PGUP
  2045.             skip -10
  2046.             browse:RefreshAll()
  2047.         case key == K_PGDN
  2048.             skip 10
  2049.             browse:RefreshAll()
  2050.  
  2051. This works just fine, but it's slow and looks funny because the
  2052. screen has to completely repaint with each movement. "Ah ha!"
  2053. you say. "What if we take out all those pesky RefreshAll()
  2054. commands? You'd    discover that even though hitting the arrow keys
  2055. apparently does nothing. . . but as soon as something which
  2056. cause the screen to    refresh itself or you try to edit the
  2057. "current" record, you'll probably find that you're not where you
  2058. thought you were. The better way to do this is a lot of work and
  2059. involves a lot of jumping through hoops. It looks something like
  2060. this:
  2061.  
  2062. browse := Tbrowsedb( 5, 5, 17, 65 )
  2063.  
  2064. browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
  2065. browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
  2066.  
  2067. browse:RowPos := 5
  2068. browse:AutoLite := .f.
  2069.  
  2070. do while lastkey() <> K_ESC
  2071.  
  2072.     do while (key := inkey()) == 0 .and. ! browse:Stabilize()
  2073.     enddo
  2074.  
  2075.     if key == 0
  2076.         brow:HiLite()
  2077.         key := inkey(0)
  2078.         brow:DeHiLite()
  2079.     endif
  2080.  
  2081.     do case
  2082.         case key == K_UP
  2083.             do while browse:RowPos > 1
  2084.                 browse:Up()
  2085.                 browse:Stabilize()
  2086.             enddo
  2087.             browse:Up()
  2088.             browse:Stabilize()
  2089.  
  2090.             do while browse:RowPos < 5
  2091.                 browse:Down()
  2092.                 browse:Stabilize()
  2093.             enddo
  2094.  
  2095.       case key == K_DOWN
  2096.  
  2097.         case key == K_UP
  2098.             do while browse:RowPos < browse:nTop-browse:nBottom
  2099.                 browse:Down()
  2100.                 browse:Stabilize()
  2101.             enddo
  2102.  
  2103.             browse:Down()
  2104.             browse:Stabilize()
  2105.  
  2106.             do while browse:RowPos > 5
  2107.                 browse:Up()
  2108.                 browse:Stabilize()
  2109.             enddo
  2110.  
  2111. This works fine and cuts down on the amount of time it takes to
  2112. move through the database. For implementing page up and page down
  2113. in this scenario it would make perfect sense to do it the same
  2114. way as the last example as the whole screen needs to be re-drawn
  2115. anyways.
  2116.  
  2117.  
  2118. Hopefully this will be enough to get you started. If you have
  2119. any questions, comments or want to know the current development
  2120. status, give us a call:
  2121.  
  2122. U.S.A.                           Europe
  2123. ───────                          ────────
  2124. Zachary Software, Inc.           SOFTSOL            Tel.:+49-40-7661290
  2125. 106 Access Road                  Neue Str.35a       Fax :+49-40-7665664
  2126. Norwood, MA 02062                21073 Hamburg      BBS :+49-40-7665527
  2127. Tel.:(800) 876-3645              Germany            CIS :100112,3401
  2128. (U.S. and Canada)
  2129.  
  2130.  
  2131. We're here to help.
  2132.