Dec 102017
 
Quick TBrowse allows you to design TBrowse on the screen and then generate the code. Relational databases supported.
File QTBROW.ZIP from The Programmer’s Corner in
Category Dbase Source Code
Quick TBrowse allows you to design TBrowse on the screen and then generate the code. Relational databases supported.
File Name File Size Zip Size Zip Type
ARELATE.CSB 20667 1219 deflated
CUSTOMER.DBF 273 119 deflated
CUSTOMER.NTX 2048 203 deflated
INVOICE.DBF 512 187 deflated
INVOICE.NTX 2048 205 deflated
ITEM.DBF 1701 465 deflated
ITEM.NTX 2048 279 deflated
QT.DOC 68578 20767 deflated
QT.EXE 491306 190650 deflated
README.QT 482 288 deflated
STATE.CSB 1899 349 deflated
STATE.DBF 2106 607 deflated

Download File QTBROW.ZIP Here

Contents of the QT.DOC file



QUIK TBROWSE

THE TBROWSE CODE GENERATOR


Credits:

A lifetime of thanks goes to my friend and guru Ira Emus, President
of ExtraSensory Software. Ira wrote (most notibly) keyloop() which
forms the very heart of Quik TBrowse. His attention to detail kept me
on track during the development phase of this project. Through his
generosity I have been given permission to include the documentation
from his exciting TBrowse seminars as Part Two of this manual.

Also thanks to Luiz Quintela of Computer Associates whose TBrowse class
inspired this product.


Quik TBrowse and its documentation are copyright 1992,1993 by:

CodeSmith Software
P.O. Box 56885
Sherman Oaks, CA 91413

All rights are reserved.


License Agreement
-----------------
The purchaser of Quik TBrowse is granted a license to use it, to make
copies of the original distribution disk for backups, and to install
it on one or more systems as long as there is no possibility that it
will be used on more than one of those systems at a time. This does
not constitute a site license; it is intended to allow a single
programmer to install Quik TBrowse on all of the machines that he uses,
but only to allow him to use it on one of those machines at once. In
essence, Quik TBrowse is licensed to a single user, who is the only
person permitted to use it.

This license applies to the development of software using Quik TBrowse,
not to the redistribution of that software. Software in executable
form that includes code produced by Quik TBrowse may be sold, given
away, or otherwise redistributed with no additional fees to, and no
special license required from, CodeSmith Software. You may not, however,
redistribute Quik TBrowse in part or in whole.

Clipper is a registered trademark of the Computer Associates



About the Manual

This manual is organized in two parts:

Part One will guide you through the operation of Quik TBrowse.

Part Two is Ira's TBrowse dissertation.




PART ONE - USING QUIK TBROWSE
-----------------------------


JUMP START
==========

Start Quik TBrowse by typing QT

Use the arrow keys to position the light bar on "ARELATE.CSB" and
press


You should have three browses on the screen: customers, invoices
and line items.Invoices relate to customers by customer numbers
and line items relate to invoices by invoice numbers. As you
scroll down the customer browse invoices and line items change.
Tab down to invoices and note the changes in the line items as
you scroll through the invoices. Cool huh?

You'll have plenty of time to play with the Quik TBrowse so let's
write some code and see if this thing really works.

Press ALT G (that's the ALT key and the letter G) for the
generate menu. Select "TBrowse - Database" by pressing

At the prompt "Output file name" press to accept
"BROWDEMO.PRG"

Press three more times to accept the defaults.

After Quik TBrowse is finished percolating, exit to DOS by
pressing ALT F for the files menu then press the letter 'X'.


To compile BROWDEMO.PRG
CLIPPER BROWDEMO /N/W

To link BROWDEMO.OBJ
RTlink fi BROWDEMO


Type "BROWDEMO" (yeah I know to leave out the quotes)to examine
your program.


END JUMP START
==============




Example Program Highlights
--------------------------

When designing ARELATE.CSB I made some fields editable like the
customer's name, line item quantity and unit price. Just position
the light bar on a cell and press enter to edit. You will get a
polite message if the cell is not user editable like the customer
and invoice numbers.

The Item description column is not as wide as the field so it
will scroll during an edit. Note that if you change the quantity
or unit price, the extended price will recalculate because it's
not a field but an expression:
transform(item->qty * item->unit, '999,999.99')

As you can see a column may be a field or an expression.

Pressing will blank the contents of a record in the
current browse.The record didn't get deleted, just its contents.
You may select which browse the user may delete records from.

If you press while in the customer browse, a previously
blanked record will be used (record recycling) if available otherwise
a new record will be added. Note that the customer->id is incremented
based on the last customer->id + '1'. An insert in the invoice browse
will default to the next invoice number based on the last invoice
number used in item.dbf.

When you allow the user to add records to a browse, you can set
default values for each field. A support function NextNumber()
is supplied which will increment the value of a key field
(character or numeric) in any database which is open.



Using Quik TBrowse with other applications
------------------------------------------

The application we made in the "Jump Start" was a stand alone. You
might remember we answered 'Y' to the Stand-alone? prompt in
'Application Options' prior to generating the code.

When integrating a Quik TBrowse program with another application you
probably want a bit more contoll. In the above example, we crated a
program named browdemo.prg. Browdemo uses myfuct() to create the
TBrowse object which is then passed to browzeAll() which handles
browse sequencing and makes calls to KeyLoop() which handles the
keystroke processing.

By answering 'N' to the Stand-alone prompt, myfunct() will return
the TBrowse objects to your calling program. From there you can pass
the objects to BrowzemAll() for keystroke processing when needed.
This will speed things up since the objects only get created once.

Example:

oRelate := myfunct() // create your TBrowse object


Next you'll probably want to display all browses and their boxes with
realtionship to the parent in your main application.

Example:

showRelate(oRelate, .t.)// display TBrowse(s) and boxe(s)

ShowRelate() only displays and places the browses in sync. The second
parameter is to display your boxes.


When you want to activate the browse(s) simply pass to object
to browzemAll()

Example:

BrowzemAll(oRelate)// activate TBrowse(s)

If we wanted to create an invoicing system where the parent/customer
datbase is shown in full screen mode and the children/grand children
are shown in via TBrowse then we will need to delete the customer browse
from myfunct() since there is probably no reason to show the customer
record in both full screen and TBrowse at the same time.

How to delete the customer object:

// use customer alias customer new
// dbsetindex("customer")
// . . .
// . . .
// . . .
// . . .
comment out or delete down to BUT NOT INCLUDING:

use invoice alias invoice new


Summary
-------

Your main application might look something like this:

local oRelate
local nOldRec

oRelate := myfunct() // create TBrowse object

ShowRelate(oRelate, .t.) // display Tbrowses and boxes

nOldRec := customer->(recno()) // save the positon of the parent database

do while .t.
.
.
.
.
do case

case lastkey() == K_TAB
browzemAll(oRelate) // activate TBrowse
.
.
.
.
endcase

// If the customer record has changed
if nOldRec # customer->(recno())
// Syncronize browses, do not redisplay boxes
ShowRelate(oRelate, .f.)
nOldRec := customer->(recno()) // store the new record position
endif

enddo


If you use CodeSmith to create the main application the only thing you
need to do is delete the customer object from myfunct() then tell
CodeSmith that your Quik TBrowse program is BROWDEMO and its main
function is myfunct. The rest is taken care of for you.






MENUS
-----

F10 will access the main menu. From there use the left/right
arrow keys to navigate and press to select a submenu.
You can also access the submenus by pressing Alt + .

Example: ALT F will get you the File Menu
Example: ALT O will get you the Column Menu

Help is available for each menu item, just press the familiar F1
key.

The options for the TBrowse and Column menus may be accessed
directly by pressing their associated hot key from within your
TBrowse.




FILE MENU
=========


ADD NEW BROWSE
--------------

Append another TBrowse to current design. Choose an existing
design (.csb) or a database (.dbf) Selecting a database will give
you another TBrowse with one column. From there you may insert
additional columns which may be fields or expressions.
Reminder: If you want a relationship, you need to:
Set Controlling Index and
Set Relation



SET CONTROLLING INDEX
---------------------

Determines what order the TBrowse appears to be sorted on. You
MUST select an index for descendant (child and grand child)
browses if you want to do a relationship. A parent index is
optional. The index must exist. There is no menu option for
creating indexes.



SET RELATION
------------

This is how you position descendant (child and grand child)
browses relative to their parent and to each other, kinda like a
family.

Three selections must be made for this thing to work properly.

1) Controlling index - Sets ordering for each browse and
determines positioning (via seek) for the descendant browses. An
index for the parent browse is optional.

You need to select a controlling index prior to 'Set Relation'.
The index selection process should have probably been included
within the set relation option but I thought it might be easier
to find if was kept separate.

2) Seek Condition - Positions a descendant browse relative to
its parent.

3) While Condition - Top/Bottom range limit.

Example: Let's relate a customer to invoices to line items

Browse Index Seek While
------ ----- ---- -----
Customer customer (leave blank) .t. (all records)
Invoice invoice customer->id invoice->id == customer->id
Item item invoice->invnum item->invnum == invoice->invnum


The customer browse uses customer.ntx just to show the customers
in numeric order. There is no seek so just leave it blank.
The while condition is input as .t. as in DO WHILE .T..
This will allow ALL customer records to be visible. If you want
to have delete capability then your seek should be '7'
(since our customer numbers begin with 7) which will
position at the first customer number and the while condition
should be !empty(customer->id). Remember that when you delete a
record it does not get deleted but its contents get blanked so we
want any deleted (blanked) customer record to float above the while
condition so they will be out of sight.


Invoice.ntx is used for the invoice browse. The key is
invoice->ID which is the customer id number. For the seek we will
use customer->id so when we move the the customer browse, the
invoice will always position itself to the correct customer
number. For the while condition we want to show only the invoices for
one customer so the syntax is: invoice->id == customer->id


Item.ntx is used for the line item browse. The key is
item->invnum, the invoice number. When we scroll through the
invoices, we want to seek the matching line items so we'll use
invoice->invnum for the seek condition. Since we only want to see
the all the line items for one invoice the while condition will
be item->invnum == invoice->invnum.






SAVE AS. . .
------------

Save your design for future use. Do not include an extension,
(.csb) will be added for you.



EXIT TO DOS
-----------

Leave this program completely. Be sure to save your work first!






TBROWSE MENU
============


MOVE (browse)
-------------

Position current browse using arrow the keys.

May be selected outside of menu by pressing F2



SIZE (browse)
-------------

Change the size of the current browse using arrow keys

May be selected outside of menu by pressing F3



BOX?
----

Select a box for the current browse. Box types are:
NONE, SINGLE, DOUBLE, SINGLE-DOUBLE and DOUBLE-SINGLE

May be selected outside of menu by pressing the letter 'B'



SHADOW?
-------

Select or deselect a shadow on right and bottom of the current browse.

May be selected outside of menu by pressing F9



HEADER TITLE
------------

Add some descriptive text to the current browse.
Text will be automatically centered one line above the browse.
Looks real goofy without a box.

May be selected outside of menu by pressing the letter 'H'



TBROWSE COLOR
-------------

Change the default colors for the current browse.
Choose two color pairs.

Example: 'w+/b. gr+/rb' will show the TBrowse in bright white on
a blue background with the light bar in yellow on magenta. Note
that the pairs are enclosed in quotes. Omit the quotes when using
a manifest constant like C_NORM. A color chart is available to
assist you.

May be selected outside of menu by pressing the letter 'C'



TAB NEXT TBROWSE
----------------

Switch focus to the next browse. Only useful when there is more
than one browse.

May be selected outside of menu by pressing the TAB key



SH-TAB PREVIOUS TBROWSE
-----------------------

Switch focus to the previous browse. Only useful when there is
more than one browse.

May be selected outside of menu by pressing the SHIFT + TAB key



LINES PER SCREEN
----------------

Select 25, 43 or 50 line mode. The mode selected must be supported
by your hardware.

May be selected outside of menu by pressing the letter 'L'



ZAP BROWSE
----------

Permanetly remove the current browse. Use with caution as there
is no UNDO.There must be at least one browse on the screen at
all times. Remember to save your work frequently.

May be selected outside of menu by pressing the letter 'Z'





COLUMN MENU
===========


COLUMN HEADING
--------------

Optional title above each column.
Example: For a customer->name column you might enter 'Name'
(in quotes).

For multiple lines, add a semi-colon ';' Example: 'First;Name'
will display:
First
Name

An expression such as dtoc(date()) is ok too, be sure to omit
the quotes

May be selected outside of menu by pressing F4



COLUMN FOOTING
--------------

Optional title below each column.
Example: For a customer->name column you might enter 'Name'
(in quotes).

For multiple lines, add a semi-colon ';' Example: 'First;Name'
will display:
First
Name

An expression such as dtoc(date()) is ok too, be sure to
omit the quotes

May be selected outside of menu by pressing F5



COLUMN SEPARATORS
-----------------

Optional character string used for vertical separation on the
left side the current column.Not used for the first column.

An ascii chart is available by pressing F3

May be selected outside of menu by pressing F6



HEADING SEPARATOR
-----------------

Optional character string used for horizontal separation between
the top of the column data and Column Heading.

An ascii chart is available by pressing F3

May be selected outside of menu by pressing F7



FOOTING SEPARATOR
-----------------

Optional character string used for horizontal separation between
the bottom of the column data and Column Footing.An ascii chart
is available by pressing F3

May be selected outside of menu by pressing F8


COPY COLUMN
-----------

Make a copy of the current column. The duplicate column will be placed
to the right of the current column.

May be selected outside of menu by pressing the letter 'O'


DELETE COLUMN
-------------

Permanently remove the current column. Caution, there is no UNDO
feature! Save your work frequently!

May be selected outside of menu by pressing the Delete key



EDIT COLUMN CONTENTS
--------------------

Make changes to the column contents. When you insert a field, the
column data mightlook like customer->name but you may want
to show thenames in upper case so change to UPPER(customer->name)

You can also perform a calculation such as:

transform(item->qty * item->unit, '999,999.99')

How 'bout displaying the first 20 characters of a wide field -

SUBSTR(item->desc, 1, 20)

Other things related to the current column such as heading, footing,
separators and width may also be changed at this menu.

May be selected outside of menu by pressing the letter 'E'



USER EDITABLE COLUMNS
---------------------

Select which columns may be edited by your user. Columns which
contain data (not calculations) may be editable. You MUST enter
the field name and optionally you can add a picture statement and
an edit color.

May be selected outside of menu by pressing the letter 'U'



FREEZE AT COLUMN
----------------

Keeps the column number entered and all columns to the left
visible at all times.

May be selected outside of menu by pressing the letter 'F'



INSERT COLUMN
-------------

Add a column to the right of the current column. To add a column
at the left most position, place it at the far right and use the
'Rotate' or 'Move' options to reposition it.

You may select a field from a list or enter an expression (see
Edit Expression). The fields listincludes only fields from the
.dbf file associated with the current browse. You may add a field
from another database provided that it is associated with another
browse which is on the screen at the time.
Fields selected from other databases may not be user editable. This
will be rectified in a future version.

May be selected outside of menu by pressing the Insert key



MOVE CURRENT COLUMN
-------------------

Reposition current column to left or right with the arrow keys.

May be selected outside of menu by pressing the letter 'M'



ROTATE COLUMNS
--------------

Shift all columns to the left or right with the arrow keys.

May be selected outside of menu by pressing the letter 'R'



COLUMN WIDTH
------------

Set the data columns width.A number too large with result in
gap-osis (a hole)between ajacent columns. A number too small
will truncate the data.

May be selected outside of menu by pressing the letter 'W'




USER MENU
=========


ALLOW RECORD DELETION
---------------------

Determines if your application will allow the user to delete
records in the current browse. Records are not actually deleted
but the contents are blanked.These records are reused when the
user adds a record provided one is available, otherwise a new record
is appended.



ALLOW RECORD APPEND
-------------------

Determines if your application will allow the user to add
records to the current browse. Previously 'deleted' records
are used first if available otherwise a new record is appended.
When you allow records to be appended, you will be given a fields
list so that default values may be assigned. For example, you
may want the default item->invnum to be invoice->invnum. If need
to increment a number based on the contents of a record you may
use NextNumber().

Say you want to set the default customer->id based on the last
customer->id + '1' enter NextNumber('customer', 'id', 1) or perhaps
you need the next invoice number for invoice->invnum enter
NextNumber('item', 'invnum', 1). The 1st parameter is the alias of
the database to look at and the 2nd parameter is the field to
increment. Parameter 3 is the index order. If you omit the 3rd
parameter, or set it to zero, NextNumber() will increment the last
physical record. Works with both numeric and character fields.



ALLOW RECORD SEARCH
-------------------

Determine if your application will allow the user to search for
records in ALL browses which have a controlling index. If a search is
attempted in a browse without a controlling index you'll get a "quack".

To enable the search mechanism, the current browse must have a
controlling index otherwise you will be prompted to set a controlling
index prior to enabling the search. If the current browse does not have
a controlling index either assign one from the FILES MENU or TAB to a
browse which does have a controlling index prior to enabling the search
mechanism.



GENERATE MENU
=============

TBROWSE DATABASE
----------------

This is the code generation option you'll probably use the most.
Your TBrowse design will be running off a database (like you'd
expect) as opposed to an array as with the next option.



TBROWSE HARD CODED
------------------

Huh? This option will populate an array with the contents of the
database used in the design process. While this technique has
limited usefulness it can come in handy. For example you may want
to simulate a database or with a little inguinuity you can diplay a
fields list or a directory, read on to find out how. You can also
use this as a basis for multi-column pick lists, menus and the like.

Little attempt has been made to make this option as powerful as
'TBrowse Database'. For example, column expressions will be
ignored and relations are not supported. Despite the limitations
it may come in handy some day and for this reason it has been
included.


How to display a fields list:

What we are going to do is use any database that has a character
field and a numeric field. Set up four columns, the first two
need to be character and the last two need to be numeric. I know
this sounds goofy but stick with me, it should make more sense
in a minute. Quik TBrowse will only browse databases, not arrays
even though it will produce code to browse an array. So what we're
doing is using a dummy database as a 'template'.

Let's give this thing some titles so press F4 and enter the following:
'Name'
'Type'
'Length'
'Dec'

Next set the column widths as follows:

Column # Width
-------- -----
1 10
2 4
3 6
4 3

Ok now generate using TBrowse - Hard Coded.

Go into the file we just generated and make a few changes.

Locate the declaration: local obj
at around line 34

On the next two lines add:

use somefile // open a database

aData := dbstruct()


Dbstruct() will load aData with an array whose length is that of the
database in use. Each element of the array is a subarray containing
information about one field. The subarrays have the following format.


Position Metasymbol
-------- ----------
1 cName
2 cType
3 nLength
4 nDec


Delete all occurances of:

aadd(adata, . . .


Since Quik TBrowse will not support "Edit Expression" when generating
a Hard-Coded array browse we'll need to do it manualy.

For column 3, locate the line that looks something like this
column := TBcolumnNew('', {|| trans(aData[nElement, 3], '999999.99') } )

Change to:
column := TBcolumnNew('', {|| trans(aData[nElement, 3], '99999') } )

This will display up to five numbers for the length column



For column 4, locate the line that looks like this
column := TBcolumnNew('', {|| trans(aData[nElement, 4], '999999.99') } )

Change to:
column := TBcolumnNew('', {|| trans(aData[nElement, 4], '99') } )

This will display up to two numbers for our decimals.


That's it. Add one line, delete a bunch of lines then modify two lines.


You can use the same technique to display a directory. Instead of four
columns you can display a maximum of five. Our aData array will get
populated using the directory() function.

aData := directory()

Directory() will load aData with an array whose length is that of the
files in the current directory. Each element of the array is a
subarray containing information about one file. See your Norton Guides
for further information. The subarrays have the following format.


Position Metasymbol
-------- ----------
1 cName
2 cSize
3 dDate
4 cTime
5 cAttributes



VALID POP-UP FUNCTION
---------------------

Create an ACHOICE() valid pop-up function

Question: What do ACHOICE() and TBrowse have in common?
Answer: Very little.

Actually what they do have in common is Quik TBrowse
which is used to paint the pop-up. After generation
you get a program named popdemo.prg which will contain
your valid pop-up function which is based on an ACHOICE()
window which displays the contents of a hard coded array
that was populated by the database used during the design.
Since this is ACHOICE() and NOT TBrowse there are a limited
number of supported options namely box, color, shadow and
header title. Unsupported options will be ignored.

To try it out fire up Quik TBrowse using state.csband generate a
Pop-up valid. Compile and link your program, later you'll take just
the generated function from the program but for now we'll use the
whole thing to check out our function. When you start your newly
generated program there will be a prompt to "Test your valid
function", type in 'ZZ' ' (I know there's no state abreviation ZZ) to
activate the function. The function will scan its array for 'ZZ'. If it
can't find it, ACHOICE() will be activated showing you a list of
states. Pick one.

In our example we get something like CA-California. Oops, we only
want the first two characters so find the line (around line 136)
that looks like this:
getactive():varput(aName[nSelection])

Change it to look like this:
getactive():varput( substr(aName[nSelection], 1, 2) )




PART TWO
--------

Beginning Tbrowse

In S87, dbedit() introduced us to a new way of browsing databases
and, after a few attempts, most of us were amazed at the
capabilities that command gave us. In some ways, Tbrowse can be
considered the replacement for dbedit() since it also allows you
to browse a database in a window under program control.

In fact, Tbrowse is actually a lot more powerful than dbedit()
ever was-so powerful that the dbedit() included with Clipper
5.01 was written in Clipper using Tbrowse. Most of the power
inherent in Tbrowse comes from its open architecture, which gives
the programmer access to almost all of its "internals". (They
aren't really internals, but when you read the list of all of the
controls, it seems that way.)

You'll need to understand three main things before starting with
Tbrowse:

(1) How to structure an object.
(2) How to write a codeblock.
(3) How to send messages to an object.

Neither is very difficult. First, I'll show you how to create the
codeblocks you'll need; second, how to talk to objects.

Objects

Tbrowse is object-based, so I'll be using some object-oriented
language. Before I start, I want to define a few terms so you'll
understand what I'm talking about later in this discussion.

An "object" is a collection of data (variables) and code
(procedures or functions).

An "instance variable" is a data element in an object. In a
Tbrowse object, four of the instance variables are top, bottom,
left and right. These instance variables contain the dimensions
of the indicated Tbrowse.

A "method" is the code of the object. When you start writing your
own objects, you'll see that a method is just another name for a
function or procedure which is bound to the object. In a Tbrowse
object, four of the methods are top(), bottom(), left() and
right(). These methods implement some of the cursor movement
capabilities in Tbrowse.

"Send" or "message send" is used to describe how we communicate
with objects. As we said above, an object is a collection of
code and data seemingly contained in a variable. In order to send
something, an order or request (called a "message") is sent to
the object using the send operator, better known as a colon. A
send message can cause one of three things to happen: (1) a
method within the object executing; (2) an assignment being made
to an instance variable; or (3) a value of an instance variable
being returned. Sending a message can look like any of these:

object:message
object:message()
object:message()

Any of these formats may return values. As a general rule, the
first example would be expected to return a value; the second
and third options may or may not.


Codeblocks

A codeblock consists of a Clipper expression surrounded by curly
braces and preceded by pipes, like this:

{|| }

For use with Tbrowse, that's about all you need to know about
codeblocks. The expression will be the regular Clipper code you
always use. Following are codeblock examples which define what to
display in various columns:

{|| clients->lastname }
{|| upper( clients->state ) }
{|| transform( clients->phone, "@R 999/999-9999" ) }
{|| padr(trim(clients->city)+" "+clients->state+" "+clients->zip, 35 ) }
{|| if(deleted(), "Deleted", " " }

Make sure that the expression always returns a constant string
length. In the above examples, the string length is the width of
the designated field. Display expressions are evaluated when the
column is defined, using the current record at the time. If, for
example, you use a trim() in your expression and the current
record is blank or has no data in the key field, the expression
would return a string length of 0-and you don't have a column.

It is legal to use multiple expressions in a codeblock; just
separate the expression with commas. An example where this might
be useful is shown here:

{|| lineitem->(dbseek(invoice->inv_no, .f.)), lineitem->qty }

Notice that the first half of the expression seeks for a record
in the lineitem file and the second half returns the value of
the field qty in the found record. Also note that I turned off
soft seek using dbseek()'s optional parameter .f.; if a seek()
fails, the record pointer will be positioned at end of file.

Sending Messages

Next you need to know how to send messages and assignments to
objects and how to request information from objects. This is
even simpler than codeblocks and involves only the colon
character. Here are some examples of this:

browse := TbrowseNew( 5, 5, 17, 65 ) // Create a Tbrowse object
browse:top := 4 // Reset the top of the Tbrowse to row 4
? browse:left // Print the left column of the specified Tbrowse
browse:refreshall() // Tell the Tbrowse that all of the displayed data
// needs refreshing
Finally, Tbrowse

Now on to the real topic of the day, Tbrowse. In this class, I'll
introduce you to Tbrowse and give you enough information so you
can go home and write your own simple Tbrowses. Tbrowse is a
relatively generic browsing mechanism which is capable of
browsing anything; however, in this class we'll concentrate on
browsing databases.

The first step in using Tbrowse is to create a Tbrowse object.
The only information you need to supply is a set of coordinates.
The syntax looks like this:

browse_object := tbrowsedb( , , , )

Tbrowse Columns

At this point, you have a Tbrowse object which knows how big it
is and also contains bits of code which tell it how to move
around in a database, but it doesn't yet have any information
about what to display. To tell the Tbrowse object what to
display, we must define columns for it. The command to create a
column is tbcolumnnew(), which creates a tbcolumn object. This
column object has two properties which we will consider now and
which are defined in the call to tbcolumnnew(). They are: (1)
the title, which is displayed at the top of the column and (2)
the display block, which defines the information to be displayed
in each row of the column. Here are a couple examples of creating
Tbrowse columns:

column := tbcolumnnew( "Name", {|| clients->name } )
column := tbcolumnnew( "Phone", ;
{|| transform(clients->phone, "@R 999/999-9999") } )
column := tbcolumnnew( "Record", {|| clients->( recno() ) } )
column := tbcolumnnew( "Deleted", ;
{|| if(clients->(deleted()), "Yes", "No " ) } )

Now that we've got all the necessary pieces for the Tbrowse to
work, we need to put them together. For this we'll learn about
addcolumn(), a method of Tbrowse which allows us to add columns
to the Tbrowse object. The columns will appear in the order they
are added from left to right.
Addcolumn() is used like this:

browse := tbrowsedb( 5, 5, 17, 65 )
column := tbcolumnnew( "First;Name", {|| clients->fname } )
browse:addcolumn( column )
column := tbcolumnnew( "Last;Name", {|| clients->fname } )
browse:addcolumn( column )

Or alternatively:

browse := tbrowsedb( 5, 5, 17, 65 )
browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )

The advantage of the second set of examples is that it uses fewer
lines of code, slightly less memory, and makes the column object
available for modification; the disadvantage is that it might be
harder to read, especially as the display blocks get long and
complicated. You'll notice that I used semi- colons in the
headings ("First;Name"). Like dbedit(), Tbrowse has automatic
headings which will accept the semi-colon as a new line
character-so the headings for this Tbrowse will occupy 2 lines.

Displaying the Tbrowse

By now, you've learned how to create a Tbrowse which contains
everything necessary to function; but if you were to compile,
link and run the previous sample, you'd find that nothing
displayed. Tbrowse, unlike dbedit(), is completely under program
control and your program will need to tell the Tbrowse object to
display. The stabilize() method implements incremental
stabilization and display and returns a logical containing the
status of the display effort. The following code allows us to
display the Tbrowse object:

browse := tbrowsedb( 5, 5, 17, 65 )
browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )

do while ! browse:stabilize()
enddo

Tbrowse Keystrokes

Not bad so far. In only 5 lines of code, we've created a simple
2-column Tbrowse and drawn it on the screen. However, if you run
that code, you'll find that the Tbrowse will display itself as
expected and then just exit to DOS... not at all the result you
actually want. One of the strengths of Tbrowse is its ability to
do anything you want. The downside (if you want to consider it
that) is that, at least until you get some generic key handling
code written, it seems to take more lines of code for a Tbrowse
than it did for a dbedit(). The following code demonstrates a
simple key handling loop:

browse := tbrowsedb( 5, 5, 17, 65 )
browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )


do while lastkey() <> K_ESC
do while ! browse:stabilize()
enddo
key := inkey(0)
do case
case key == K_UP
browse:up()
case key == K_DOWN
browse:down()
case key == K_LEFT
browse:left()
case key == K_RIGHT
browse:right()
case key == K_PGDN
browse:pagedown()
case key == K_PGUP
browse:pageup()
case key == K_HOME
browse:gotop()
case key == K_END
browse:gobottom()
endcase
enddo

This example clearly demonstrates how simple it is to control a
Tbrowse. Most of the Tbrowse methods in this example are
self-explanatory-up(), down(), gotop() and the others should be
clearly obvious. The one thing you might want to notice is that
I've defined the and keys differently than you
might be used to seeing. I find these keys more user-friendly
than the old and , and as you can see
it was a trivial matter to change them.

Tbrowse Optimization

One problem with the previous example is that you have to wait
for the Tbrowse to stabilize before another keystroke will be
accepted. For keys like K_UP and K_DOWN, the stabilize is short
enough that it hardly matters; but for K_PGUP and K_PGDN, the
whole screen has to be refreshed for every keystroke. This isn't
really a problem unless your clients are on slow machines, or
you've designed a complicated or very large Tbrowse and someone
wants to move down 4 pages. The following piece of code shows
how to short-circuit the stabilization loop and speed up large
movements through the Tbrowse:

do while nextkey() == 0 .and. ! browse:stabilize()
enddo

At this point, we have a completely usable Tbrowse and you now
have all the knowledge you need to start using Tbrowse in your
applications. Here is a fully commented version of all we've
learned so far:

// First the tbrowse object, browse, is created.
browse := tbrowsedb( 5, 5, 17, 65 )

// Next two columns are added
browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )

// Next we'll enter the key processing loop. I've chosen to put the
// exit condition in the loop, but you could just as easily make the
// loop "while .t." and put // the exit condition in the case statement.

do while lastkey() <> K_ESC

// This is the stabilization/display loop with added exit on
// keystroke to speed up the processing of closely spaced keys

do while nextkey() == 0 .and. ! browse:stabilize()
enddo
key := inkey(0)

// This is the keystroke processing case statement. This structure
// needs a case for every keystroke you want to process during the
// Tbrowse. Since this uses inkey(0) to get keystrokes, you'll also
// need to handle all of the SET KEYs and any other keystrokes you
// take for granted.

do case
case key == K_UP
browse:up()
case key == K_DOWN
browse:down()
case key == K_LEFT
browse:left()
case key == K_RIGHT
browse:right()
case key == K_PGDN
browse:pagedown()
case key == K_PGUP
browse:pageup()
case key == K_HOME
browse:gotop()
case key == K_END
browse:gobottom()
endcase
enddo

Enhancements

Now that we've got Tbrowse up and running, you'll probably want
to make it better. What I consider "better" is probably
different than what you'd consider "better", but I'll go through
as many enhancements as seem reasonable for this session. The
first thing we'll look at is handling the special keys: help,
SET KEYs and the like. One of the things we know about SET KEYs
in Clipper 5.01 is that they all are set using setkey() and that
we can use setkey() to inquire into the status of any key. If
you've gone snooping around in getsys.prg, you've probably seen
this or a similar piece of code:

key := inkey( 0 )
if setkey( key ) <> NIL
eval( key, procname(), procline(), "Var" )
endif

This code is what implements the evaluation of SET KEYs in
Clipper code in Clipper itself. For a Tbrowse key processing
loop, you'll probably want to make one change-if the called
function has knowledge of the Tbrowse, it should have the
ability to control what happens after it returns.

key := inkey( 0 )
if setkey( key ) <> NIL
key := eval( key, procname(), procline(), "Var" )
endif

With the one addition of key := eval(...), the called function
suddenly has the ability to control the operation of the Tbrowse
within a very limited range. I'd guess that the most likely
return values would be 0 (to do nothing) and 27 (to exit).
Remember, so far the code doesn't check the type of the return
value. I made the assumption that most SET KEYs would be
procedures and return NIL. Since that could possibly be an
invalid assumption, I'll toss in some error checking in this next
example:

key := inkey( 0 )
if setkey( key ) <> NIL
key := eval( key, procname(), procline(), "Var" )
if valtype( key ) <> "N"
loop
endif
endif

Searching

The next enhancement you might want to make is the ability to
search for a particular record or range of records while
Tbrowsing. The easy way would be to use a pop-up box for the
search string, seek, and then redisplay the browse if the new
selection is found. That piece of code is pretty simple. Here
I'll show pseudo-code because I don't want to implement all of
the box stuff:

// Place this as a case in the key handling case
case key == K_SEARCH_KEY
// Draw pretty box
oldrec := recno
@ row, col get search_string
read
seek search_string
if found()
browse:refreshall()
else
// Put up "search failed" message
goto (oldrec)
endif
// Undraw pretty box

The only new thing in this code is the browse:refreshall().
Remember that Tbrowse doesn't automatically show everything you
do, so the movement of the record pointer will not show up on
screen unless we tell Tbrowse to refresh the screen. For this,
we'll use refreshall() so all the data in all the rows will be
refreshed. The reason for refreshing all of the data is that the
seek probably moved the record pointer, which means that all the
data in all the rows will have changed. Notice also that on a
failed seek, I just put the record pointer back to its original
position and return. In this case, since nothing has changed,
the Tbrowse does not need refreshing. A more interesting way to
implement searching is called incremental searching. This is
where you type on the fly and a new search is made as each
letter is added or deleted from the string. The only difficult
part of this is storing the search string and deciding how to
implement the search. In this example, if a letter causes a
failed search, it will be disregarded and only the remaining
letters will be used. The example respects upper and lower case
letters, but the addition of upper() or lower() would make the
search case insensitive. You'll need to make it reflect the state
of the current index.

// The first case is where an alpha key has been hit and we want
// to search for the current search string.

case isalpha( chr( key ) )

// First we'll save the current recno() and try seeking for the
// new key

oldrec := recno()
if dbseek( srch_string+chr( key ), .f. )

// If the search succeeds, we'll add the new letter to the
// search string, refresh the browse and show the search
// string in some appropriate location.

srch_string += chr( key )
browse:refreshall()
@ row, col say padr( srch_string, 15 )
else

// else if it fails we just put the record pointer back to
// its original position.
goto (oldrec)
endif

// The second case is where a was hit and we need to
// shorten the search string and then seek on the new shorter
// string.

case key == K_BKSP

// If the search string is empty, nothing needs to be done;
// otherwise we'll nick one letter off of the end, seek on the
// shorter string, refresh the Tbrowse, and then redisplay the
// shorter search string.

if len(srch_string) > 0
srch_string := subs( srch_string, 1, len( srch_string )-1 )
dbseek( srch_string+ckey, .f. )
browse:refreshall()
@ row, col say padr( srch_string, 15 )
endif


The Final Example

The last thing we'll look at is the complete, assembled example:

#include "inkey.ch"
Function browser

local browse
local key
local srch_string := ""

use clients

browse := tbrowsedb( 5, 5, 17, 65 )

browse:addcolumn( tbcolumnnew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( tbcolumnnew( "Last;Name", {|| clients->fname } ) )

do while lastkey() <> K_ESC

do while nextkey() == 0 .and. ! browse:stabilize()
enddo

key := inkey(0)

do case
case key == K_UP
browse:up()

case key == K_DOWN
browse:down()

case key == K_LEFT
browse:left()

case key == K_RIGHT
browse:right()

case key == K_PGDN
browse:pagedown()

case key == K_PGUP
browse:pageup()

case key == K_HOME
browse:gotop()

case key == K_END
browse:gobottom()

case isalpha( chr( key ) )
oldrec := recno()
if dbseek( srch_string+chr( key ), .f. )
srch_string += chr( key )
browse:refreshall()
@ row, col say padr( srch_string, 15 )
else
goto (oldrec)
endif

case key == K_BKSP
if len(srch_string) > 0
srch_string := subs( srch_string, 1, len( srch_string )-1 )
dbseek( srch_string+ckey, .f. )
browse:refreshall()
@ row, col say padr( srch_string, 15 )
endif

endcase
enddo

return NIL



Table of Tbrowse methods and instance variables
-----------------------------------------------

Tbrowse Class Functions
-----------------------

TbrowseNew() Create a new Tbrowse object

TbrowseDB() Create a new Tbrowse object for browsing a database file



Tbrowse Exported Instance Variables
-----------------------------------
autoLite Logical value to control highlighting

cargo User-definable variable

colCount Number of browse columns

colorSpec Color table for the Tbrowse display

colPos Current cursor column position

colSep Column separator character

freeze Number of columns to freeze

goBottomBlockCode block executed by Tbrowse:goBottom()

goTopBlockCode block executed by Tbrowse:goTop()

headSepHeading separator character

hitBottomIndicates the end of available data

hitTopIndicates the beginning of available data

leftVisibleIndicates position of leftmost unfrozen column in display

nBottomBottom row number for the Tbrowse display

nLeftLeft-most column for the Tbrowse display

nRightRight-most column for the Tbrowse display

nTopTop row number for the Tbrowse display

rightVisibleIndicates position of rightmost unfrozen column in display

rowCountNumber of visible data rows in the Tbrowse display

rowPosCurrent cursor row position

skipBlockCode block used to reposition data source

stableIndicates if the Tbrowse object is stable


Tbrowse Exported Methods
------------------------

Cursor Movement Methods
-----------------------

down() Moves the cursor down one row

end() Moves the cursor to the right-most visible data column

goBottom() Repositions the data source to the bottom-of-file

goTop() Repositions the data source to the top-of-file

home() Moves the cursor to the left-most visible data column

left() Moves the cursor left one column

pageDown() Repositions the data source downward

pageUp() Repositions the data source upward

panEnd() Moves the cursor to the right-most data column

panHome() Moves the cursor to the left-most visible data column

panLeft() Pans left without changing the cursor position

panRight() Pans right without changing the cursor position

right() Moves the cursor right one column

up() Moves the cursor up one row


Miscellaneous Methods
---------------------

addColumn() Adds a TBColumn object to the Tbrowse object

colorRect() Alters the color of a rectangular group of cells

colWidth() Returns the display width of a particular column

configure() Reconfigures the internal settings of the Tbrowse object

deHilite() De-highlights the current cell

delColumn() Delete a column object from a browse

getColumn() Gets a specific TBColumn object

hilite() Highlights the current cell

insColumn() Insert a column object in a browse

invalidate() Forces redraw during next stabilization

refreshAll() Causes all data to be refreshed during the next stabilize

refreshCurrent() Causes the current row to be refreshed on next stabilize

setColumn() Replaces one TBColumn object with another

stabilize() Performs incremental stabilization


Tbcolunm Class Function
-----------------------

TBColumnNew() Create a new TBColumn object


Tbcolumn Exported Instance Variables
------------------------------------

block Code block to retrieve data for the column

cargo User-definable variable

colorBlock Code block that determines color of data items

colSep Column separator character

defColor Array of numeric indexes into the color table

footing Column footing

footSep Footing separator character

heading Column heading

headSep Heading separator character

width Column display width




More Advanced Tbrowse

Invalidate() and Configure()

As we get into the more advanced aspects we'll need to know about
two more Tbrowse methods, invalidate() and configure().
Invalidate() is used when you've changed the configuration of the
Tbrowse and want to redraw it with the new definitions., You'd
probably use this if you were resizing a Tbrowse. Configure() is
used when you've changed information in the Tbrowses' columns.
Tbrowse will remain unaware of column changes until you tell it
to look using configure(). This behavior comes from Clipper's
attempts to speed Tbrowse up by reading a bunch of information
into internal storage. You would use configure if you had changed
the DefColor or Block variables of a Tbrowse column.

Changing properties of Tbcolumn objects

In most of the previous examples I've added columns to a Tbrowse
like this:

browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )

But in the following sections we're going to find it necessary to
modify instance variables of the columns at definition time.
This means we'll want to create our columns by assigning them to
variables, make all of the changes we want and then finally add
the columns to the Tbrowse, like this:

browse := Tbrowsedb( 5, 5, 17, 65 )

column := TbcolumnNew( "First;Name", {|| clients->fname } )
column:defColor := {2,3}
column:Width := 9
browse:addcolumn( column )

At this point you have 2 choices, 1; you can use the same column
variable for every column. This means you only have one variable
which you don't really need any more after you've finished
building the Tbrowse; or 2; you can use a different variable or
array element for each column. This makes it easier to modify
columns after they've been added to the Tbrowse. Both approaches
are valid and since Clipper treats objects like arrays as far as
memory usage is concerned there is almost no memory hit from
keeping the second copies around. If you keep the second
reference around I'd suggest keeping an array of columns as then
it's easy to get at the columns by number. I don't want you to
get the idea that you need to save a copy of the column if you
intend to change it later. Tbrowse itself allows you to request
a pointer to a particular column which gives you everything you
need to change the definition of a column on the fly, like this:

browse := Tbrowsedb( 5, 5, 17, 65 )

column := TbcolumnNew( "First;Name", {|| clients->fname } )
column:defColor := {2,3}
column:Width := 9
browse:addcolumn( column )

browse:GetColumn(1):width := 11

In this example after I was all done creating the Tbrowse I went
back in an changed the width using the GetColumn() method of
Tbrowse to retrieve a copy of the specified column, in this case
column 1. At first that syntax looks rather odd, but it's
perfectly legit. Let's read it from left to right, first we get
the Tbrowse object, browse; next we send it the message
GetColumn(1) which tell it to give me a copy/reference pointer
to the first Tbcolumn object in the Tbrowse; and lastly we tell
the Tbcolumn object that we want to set it's width instance
variable to 11. With objects, just like with functions, you can
chain them as far as you want. At one time or another we've all
written code like this where we've nested functions 3 or 4 deep:

subs( upper( subs( fname, 1, 1 ) ) + ;
lower( subs( fname, 2 ) ) + " " + ;
trim( upper( lname ) ), 1, 35 )

You can do the same thing with objects or even though you probably
don't think about it that way, you do the same thing whenever you
use multi-dimensional arrays. If you specify an array like this,
array[1][2][5] what you're really saying is give me the fifth
element of the second element of the first element of the array
and if you think about the way Clipper handles arrays that's
exactly how it seems to work internally.

Freezing Columns

Freezing columns was one of the things people always seemed to
want to do in dbedit() and now with Tbrowse not only is it
possible, it's downright simple. All you need to do to freeze
some number of the leftmost columns is to assign a number to the
Freeze instance variable like this:

// freeze the leftmost column
browse:Freeze := 1

Sometimes when you freeze columns you'd like to make sure the
user can't get into the frozen column and though there is no
built in method to keep the cursor out of a particular column the
implementation is almost trivial:

// freeze the leftmost column
browse:Freeze := 1

// After stabilization we'll need to make sure that the cursor is
// not in the first column. The browse:Right() will move the
// cursor to the right, off of the left most column

do while browse:ColPos < 2
browse:Right()
enddo

do while nextkey() == 0 .and. ! browse:Stabilize()
enddo

There is also the case where you'd like to lock the cursor into a
column. In that case you can just assign a value to ColPos, like
this: "browse:ColPos := 3" and then not give them access to the
sideways movement methods.

Color in Tbrowse

Color in Tbrowse is based on pointers into the table defined it
the Tbrowse instance variable ColorSpec. The default value of
ColorSpec is defined by the setcolor() setting at the time the
Tbrowse is created. ColorSpec contains a character variable
though it's always felt to me like it should have been an array
of colors as all color references are made by specifying the
color number you want. You can assign any color string you want
to this variable like this:

browse:ColorSpec := "w/n, n/w, b/w, w/b, r/w, w/r, g/w, w/g"

As color strings this one is rather boring but it will do for
this discussion. The next color control you're likely to use is
the defColor instance variable belong to Tbcolumn. defColor is a
2 element array containing numbers which tell Tbrowse which
colors to use for the column when it's highlighted, position 2
in the array, and when it's not, position 1 in the array. The
default value of this array is {1,2}. The defaults for ColorSpec
and defColor mean that the default Tbrowse colors will appear
just like the colors on a screen of gets. But if we look closer
and notice that defColor belongs to the column you'll realize
that different colors can contain different look-up tables. If we
assign {1, 2} to defColor in the first column and {3, 4} to
defColor in the second column of our Tbrowse we will have a 2
column Tbrowse with columns of different colors. The first column
being white on black with a black on white highlight and the
second being white on blue with a white on blue highlight.

This is a lot cooler than dbedit() ever was, but it's only the
beginning. The other color instance variable is ColorBlock. By
default ColorBlock is NIL and ignored, but you can assign it a
code block which returns a 2 element color array like ColorSpec
contains. If ColorBlock contains a code block this block is
evaluated every time before this row of this column is drawn and
that returned color array is used instead of the one contained
in defColor. Here is an example of a typical ColorBlock:

column:ColorBlock := {|| if(invoice->owed > 0, {5, 6}, {1, 2} }

This block will cause every negative number in this column to
display in red instead of blue. The flexibility embodied in the
ColorBlock is hard to fathom and limited basically only by your
imagination. The one thing to remember before you get to wild
with ColorBlock is that the Color Block is evaled for every row
drawn. If you have too complicated an expression or too many
columns with ColorBlocks you may start to see speed degradation
during repainting.

Automatic index order changing

Sometimes in a Tbrowse it's nice to be able to change the index
order. There are a couple of ways this is normally done. For
small fast browses with only a couple of indexes to chose from
the following works really well:

case key $ "oO"
if indexord() == 3
set order to 1
else
set order to (indexord()+1)
endif
@ row, col say {"Name ", "Zip ", "Phone"}[indexord()]

In this case hitting the "O" key causes the browse to cycle
through the available indexes and paints the current index order
at some appropriate spot on the screen.

Another method would be to pop up an achoice() of available
indexes when the "O" key is hit and let them chose from the
available choices, In fact that could be as easy as this:

case key $ "oO"
set order to achoice(5,5,10,10,{"Name", "Zip ", "Phone"})

@ row, col say {" ", "Name ", "Zip ", "Phone"}[indexord()+1]

One more method, my favorite is to have the index order depend on
the currently highlighted column. This requires some preparation
in that you'll need to have an index that corresponds with each
of the columns and you'll need to make sure that the order of
the indexes is the same as the order of the columns. The code for
this looks something like this:

use clients
set index to fname, lname, phone

browse := Tbrowsedb( 5, 5, 17, 65 )

browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
browse:addcolumn( TbcolumnNew( "Phone:Number", {|| clients->phone } ) )

do while lastkey() <> K_ESC

do while nextkey() == 0 .and. ! browse:Stabilize()
enddo

// this is the order changing code.
// first we check to see if the index order is the same as the
// current column number

if indexord() <> browse:ColPos

// if not we change the order, tell the Tbrowse to refresh the
// data in all of the rows and then re-stabilize

set order to (browse:ColPos)
browse:RefreshAll()
do while nextkey() == 0 .and. ! browse:Stabilize()
enddo
endif

key := inkey(0)

Now that we know how to change the index order wouldn't it be
nice to be able to highlight the index order in a very obvious
way? The most obvious way is to just write indexkey(0) somewhere
on the screen. It's a given that we'll understand what that
means, but will our customers? With some of the keys I use there
is a good chance they would just be confused. If you happen to be
using the dBFSIX drive you could use sx_tagname() which returns
an English name for the index key which works much better and if
you're using ntx or ndx files you could just keep an array of
names hanging around and just display anames[indexord()+1], the
+1 taking care of those times when indexord is set to 0.

I tend to like more subtle but unmistakable signs, my favorite
being changing the color of the column which the order is
currently set too. If we implement a scheme for changing index
orders like the one above there are a couple of ways to
implement color columns. One uses a ColorBlock assigned to all
of the columns and looks like this:

column:ColorBlock := {|| if(browse:ColPos == indexord(), ;
{3, 4}, {1, 2} ) }

This is easy and automatic, but it's slower than necessary
because that block needs to be evaluated for ever data element
drawn by the Tbrowse. The way I'll show next might be a little
slower when you change orders because it involves re configuring
the Tbrowse, but it's faster at scrolling. On a 386 or better it
might not actually matter, but on a PC or slow 286 the
difference can be quite noticeable. The code for this method
looks like this:

use clients set index to fname, lname, phone

browse := Tbrowsedb( 5, 5, 17, 65 )

browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )
browse:addcolumn( TbcolumnNew( "Phone:Number", {|| clients->phone } ) )

// set the correct column to a different color
browse:GetColumn(indexord()):defColor := {3, 4}
do while lastkey() <> K_ESC

do while nextkey() == 0 .and. ! browse:Stabilize()
enddo

// this is the order changing code.
// first we check to see if the index order is the same as the
// current column number

if indexord() <> browse:ColPos

// we know that the indexord() column is the one with the
// different color setting, so we change it back to normal

browse:GetColumn(indexord()):defColor := {1, 2}

set order to (browse:ColPos)

// Now we've changed the index order so we can now set the
// defColor of the new column to the special color

browse:GetColumn(indexord()):defColor := {3, 4}
browse:RefreshAll()

// additionally we'll need to tell the browse to configure()
// itself so it picks up the color changes.

browse:Configure()
do while nextkey() == 0 .and. ! browse:Stabilize()
enddo
endif

key := inkey(0)

The Highlight

Tbrowse automatically takes care of highlighting and
de-highlighting the current cell, but sometimes this can be a
problem, usually when you're trying for some sort of special
visual effect. Tbrowse provides for this with two methods,
HiLite() and DeHiLite() and an instance variable, AutoLite.
AutoLite controls the current state of the automatic highlighting
feature of Tbrowse and if AutoLite is false then HiLite() turns
the highlight on and DeHiLite() turns the highlight off,
otherwise if AutoLite is true the Tbrowse manages it
automatically. A simple example of using these methods follows.
Note that in this example I've slightly changed the stabilize
loop. This was done to make sure that the HiLite() method is not
called unless the Tbrowse is stable. Calling it when Tbrowse is
not stable will cause flickering, something it's nice to avoid.

browse := Tbrowsedb( 5, 5, 17, 65 )

browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )

// Turn off AutoLite
browse:AutoLite := .f.
do while lastkey() <> K_ESC

// The loop now assigns key, avoiding the necessity to access
// the "key := inkey(0) further down which would cause the
// highlight to flicker.

do while (key := inkey()) == 0 .and. ! browse:Stabilize()
enddo

if key == 0
browse:HiLite()
key := inkey(0)
browse:DeHiLite()
endif

Under some conditions where you want to manipulate the row
position by hand, like in the case where you want to maintain
the cursor in the center of the page and have the data scroll
over it. Though doable, this is one of those exercises that
demonstrates one of the Tbrowse's limitations: it does not
actually have a way to scroll the data independent of the
cursor. There is a really dirty way to accomplish this goal:

browse := Tbrowsedb( 5, 5, 17, 65 )

browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )

do while lastkey() <> K_ESC

do while (key := inkey()) == 0 .and. ! browse:Stabilize()
enddo

if key == 0
key := inkey(0)
endif
do case
case key == K_UP
skip -1
browse:RefreshAll()
case key == K_DOWN
skip 1
browse:RefreshAll()
case key == K_PGUP
skip -10
browse:RefreshAll()
case key == K_PGDN
skip 10
browse:RefreshAll()

This works just fine, but it's slow and looks funny because the
screen has to completely repaint with each movement. "Ah ha!"
you say. "What if we take out all those pesky RefreshAll()
commands? You'ddiscover that even though hitting the arrow keys
apparently does nothing. . . but as soon as something which
cause the screen torefresh itself or you try to edit the
"current" record, you'll probably find that you're not where you
thought you were. The better way to do this is a lot of work and
involves a lot of jumping through hoops. It looks something like
this:

browse := Tbrowsedb( 5, 5, 17, 65 )

browse:addcolumn( TbcolumnNew( "First;Name", {|| clients->fname } ) )
browse:addcolumn( TbcolumnNew( "Last;Name", {|| clients->fname } ) )

browse:RowPos := 5
browse:AutoLite := .f.

do while lastkey() <> K_ESC

do while (key := inkey()) == 0 .and. ! browse:Stabilize()
enddo

if key == 0
brow:HiLite()
key := inkey(0)
brow:DeHiLite()
endif

do case
case key == K_UP
do while browse:RowPos > 1
browse:Up()
browse:Stabilize()
enddo
browse:Up()
browse:Stabilize()

do while browse:RowPos < 5
browse:Down()
browse:Stabilize()
enddo

case key == K_DOWN

case key == K_UP
do while browse:RowPos < browse:nTop-browse:nBottom
browse:Down()
browse:Stabilize()
enddo

browse:Down()
browse:Stabilize()

do while browse:RowPos > 5
browse:Up()
browse:Stabilize()
enddo

This works fine and cuts down on the amount of time it takes to
move through the database. For implementing page up and page down
in this scenario it would make perfect sense to do it the same
way as the last example as the whole screen needs to be re-drawn
anyways.




Hopefully this will be enough to get you started. If you have
any questions, comments or want to know the current development
status, give me a call.

Don Allred
CodeSmith Software
(818) 783-5837 Voice 10 A.M. to 6 P.M. Pacific Time.
(818) 783-5837 FAX
CIS 75120,230



 December 10, 2017  Add comments

Leave a Reply