PowerBASIC Input Routines |
A competently written console program cannot be crashed by any sequence
of user keystrokes, however improbable it might be. In this context,
'crash' includes having the screen format messed up by uncontrolled
user-input.
When using a GUI interface, the user enters text in some open-ended box
or other, after which it is processed. Very nice. For the console screen,
however, the programmer must take steps to reject disruptive characters, limit the
length and content of a line of text, and avoid anything else that might trash the
screen.
The only guaranteed way to do that is to trap every keypress as it is
typed, and process it immediately. By so doing, some nifty editing
features become available 'on the fly.' It is unnecessary, for example,
to have a user enter a full line of text, only to inform him later that something is
amiss. When handled properly, an errant keypress can be summarily rejected
in real time, providing the opportunity for an immediate correction.
To that end, the following sample group of equates determines what input is valid for a given situation. When entering a proper name, for example, only characters in the $Alpha class are acceptable; when inputting a digit as a menu option, only something in $Digits need apply. Other such equates can easily be tailored to meet special needs.
$Alpha= "ABCDEFGHIJKLMNOPQRSTUVWXYZ- abcdefghijklmnopqrstuvwxyz" $Digits= "0123456789" $AlphaNumeric= $Alpha + $Digits $OtherChars= "~`!@#$%^&*()_-+=|\{[}]:;'<,>.?/" + CHR$(34) $AnyText= $AlphaNumeric + $OtherChars $Date= $Digits + "/" $Currency= $Digits + ",.$" |
For convenience, I reiterate the macros used on this page:
MACRO Backspace(A)= LOCATE,CURSORX-1: PRINT $SPC;: LOCATE,CURSORX-1: A=LEFT$(A,LEN(A)-1) MACRO Beeep= PlaySound "beep.wav",0,1+&h40004 MACRO Complain= Beeep: ITERATE MACRO EscSUB= IF ESC THEN EXIT SUB MACRO EscEXIT= IF ESC THEN EXIT MACRO GetKey= A$=WAITKEY$: ESC=-(A$=$ESC) MACRO GetUp= A$=UCASE$(WAITKEY$): ESC=-(A$=$ESC) MACRO ISdigit(A)= (ASC(A)>=48 AND ASC(N)<=57) |
For clarity, macro names are shown in brown when used in code.
TEXT INPUT
In the featured routine, characters are input one at a time up to the allowable length of the input string (one must not have a user typing something that scrolls off the screen!). Entries are validated according to the specified equate; any attempt to supply a bogus character begets a warning beep. Variable P prevents a backspace beyond the starting screen column. Variable GotNum reflects the numeric value of the string, in case that is desired.
Entry of function keys and certain control keys is supported. If one of
those is pressed, that key's variable flag is set and the Sub is exited.
Example: pressing <Page Down> sets variable PGDN equal
to -1. If the entry is a function key, then variable FKEY
is set equal to the key's number (1-12).
As with all of my routines, pressing <Esc> means that the user wishes to abort the process. In that case, the flag ESC is set, and the Sub is exited. Other than those options, only <Enter> ends the routine.
Note: the Windows key and multiple-key functions such as <Ctrl-PgDn> are not supported here, but they could be accommodated.
SUB TextInput (CharEquate$, MaxLen&, UcaseF&) '[get controlled text to maxlen chars] 'IN: CharEquate = specifies the allowable characters ' MaxLen = limits the length of Got$ ' UcaseF: 1=convert chars to uppercase; 0=no conversion 'OUT: Got$ = actual string entered ' GotNum = numeric value of Got$ ' ESC,CRLF,TABB = flag the keypress ' FKEY = Function key number, if pressedDIM K&,P& FKEY=0: KEYS=0: CRLF=0: TABB=0: PGUP=0: PGDN=0 'these are global flagsUPARR=0: DNARR=0: RTARR=0: LFARR=0: HOME=0: DEL=0: ENDD=0: INS=0 ''Got$="": GotNum=0: P=CURSORX: INPUT FLUSH DO: GetKey '----- input one character at a time -----IF ESC THEN Got$="": EXIT SUB IF LEN(A$)> 1 THEN 'a keypad/fnkey string = CHR$(0,N)K=ASC(MID$(A$,2,1)) FKEY=(K-58)*-(K >=59 AND K <=68) +(K-76)*-(K=87 OR K=88) 'from ScancodesPGUP= K=73: UPARR= K=72: RTARR= K=77: HOME= K=71: DEL= K=83 PGDN= K=81: DNARR= K=80: LFARR= K=75: ENDD= K=79: INS= K=82 'CAPS= K=58: SCRLK= K=70: NUMLK= K=69 <available, but probably unused>EXIT SUB END IF IF A$=$CR THEN CRLF=Yes: EXIT SUB IF A$=$TAB THEN TABB=-1: EXIT SUB IF A$=$BS THEN IF CURSORX=P THEN Complain ELSE Backspace(Got$): Gott=VAL(Got$): ITERATE IF ISFALSE INSTR(CharEquate$,A$) THEN Complain 'invalid characterIF LEN(got$)=MaxLen% THEN Complain 'strlen limit reached 'IF UcaseF& THEN A$=UCASE$(A$) 'char is valid; use itPRINT A$;: Got$ += A$: GotNum=VAL(Got$) 'add char to stringLOOP '(getkey)END SUB '(TextInput) |
Here's a shorty-version of TextInput that gets just one controlled character, converting it to uppercase as desired.
SUB InputChar(CharEquate$,UcaseF&) '[get 1 char in $equate w/ uppercase opt] 'OUT: Got$, GotNumGot$="": INPUT FLUSH DO: GetKey :EscSUB IF UcaseF& THEN A$=UCASE$(a$) IF INSTR(CharEquate$,A$)=0 THEN Complain PRINT A$;: Got$=A$: GotNum=VAL(A$): EXIT LOOP '(getkey)END SUB '(InputChar) |
GET A YES/NO ANSWER
This simple workhorse module gets an answer to a Yes/No question while
totally error-trapping the user input. Because they are constants, you might
prefer to set up Yes and No as equates:
%Yes=1 or -1, %No=0. I don't bother,
because I weary of typing all those extra percent-signs, and because the
code reads more logically without them.
SUB AskYN(op&) '[choose Yes/No/Esc; $CR defaults to op& value: 1=yes,0=no] 'OUT: Answer (1 or 0)CURSOR ON: INPUT FLUSH DO: GetUp :EscSUB IF A$="Y" THEN Answer=Yes: EXIT IF A$="N" THEN Answer=No: EXIT IF A$=$CR THEN Answer=op&: EXIT Beeep LOOP '(getup)END SUB '(AskYN) |
Sample usage:
SUB MyRoutine
...
PRINT "Play another game (Y/n)?"
AskYN(yes): IF Answer=No THEN END
...
END SUB '(MyRoutine) |
That subroutine sets the Global variable Answer. If preferred, that usage could be avoided by setting up AskYN as a Function:
FUNCTION AskYN(op&) AS LONG
CURSOR ON: INPUT FLUSH
DO: GetUp :EscEXIT
IF A$="Y" THEN FUNCTION=Yes: EXIT
IF A$="N" THEN FUNCTION=No: EXIT
IF A$=$CR THEN FUNCTION=op&: EXIT
Beeep
LOOP '(getup)END FUNCTION '(AskYN) |
Usage:
SUB MyRoutine
...
PRINT "Quit the program (y/N)?"
IF AskYN(no) OR ESC THEN END
...(continue)
END SUB '(MyRoutine) |
ENTER A DATE — MM/DD/YY
A date string is created in the format "xx/yy/zz", with the slashes
supplied automatically on and off the screen. It is impossible for the user to
screw it up, even while making a correction. The programmer specifies the order
of the data (such as MO/DA/YR or YR/MO/DA); this module doesn't care.
SUB InputMoDaYr 'OUT: TheDate$ as "xx/yy/zz" (unverified as to accuracy)LOCAL CT& 'tracks the string/cursor positionTheDate$="": INPUT FLUSH WHILE CT <= 8 GetKey: EscSUB IF A$=$BS THEN IF CT=0 THEN Complain 'no data to backspaceBackspace(TheDate$): DECR CT IF CT=2 OR CT=5 THEN Backspace(TheDate$): DECR CT 'delete slashITERATE END IF IF CT=8 THEN IF A$=$CR THEN EXIT ELSE Complain 'string is fullIF NOT ISdigit(A$) THEN Complain 'PRINT A$;: TheDate$ += A$: INCR CT IF CT=2 OR CT=5 THEN PRINT "/";: TheDate$ += "/": INCR CT WEND '(ct<=8)END SUB '(InputMoDaYr) |
One could augment this routine, if desired, to verify that the input values are within the proper ranges for Month, Day, and Year.
OTHER MENUS
Not all input options require special handling, of course.
A simple menu request can be totally error-trapped with very little code:
SUB MyRoutine
...
LOCATE 8,M: PRINT "(M)orning"
LOCATE,M PRINT "(A)fternoon"
LOCATE,M: PRINT "(E)vening"
LOCATE,M: PRINT "(L)ate"
PRINT: LOCATE,M: PRINT "Select a time: ";
DO: GetUp :EscEXIT
IF INSTR("MAEL",A$)=0 THEN Complain ELSE Got$=A$: EXIT
LOOP
...
END SUB '(MyRoutine) |
In truth, though, as long as the InputChar() facility already has been set up, one might as well use it:
... ... PRINT: LOCATE,M: PRINT "Select a time: "; InputChar("MAEL",1) ... |