Petey 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 pressed DIM K&,P& FKEY=0: KEYS=0: CRLF=0: TABB=0: PGUP=0: PGDN=0 'these are global flags UPARR=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 Scancodes PGUP= 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 character IF LEN(got$)=MaxLen% THEN Complain 'strlen limit reached ' IF UcaseF& THEN A$=UCASE$(A$) 'char is valid; use it PRINT A$;: Got$ += A$: GotNum=VAL(Got$) 'add char to string LOOP '(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$, GotNum Got$="": 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 position TheDate$="": INPUT FLUSH WHILE CT <= 8 GetKey: EscSUB IF A$=$BS THEN IF CT=0 THEN Complain 'no data to backspace Backspace(TheDate$): DECR CT IF CT=2 OR CT=5 THEN Backspace(TheDate$): DECR CT 'delete slash ITERATE END IF IF CT=8 THEN IF A$=$CR THEN EXIT ELSE Complain 'string is full IF 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) ...

PowerBASIC Menu