Lesson 9: Text and String Manipulation
Not all games involve pressing buttons to shoot enemies on the screen. Many of the all time favorites involve entering text to play such as Hangman, Scrabble, crossword puzzles, Boggle and Text Twist just to name few. Luckily QB64 has a rich feature set of commands that allow the programmer to manipulate text entry in many different ways.
The UCASE$ and LCASE$ Statements
When a user answers a program's question, for instance, "What is the capital of Ohio?", how will the user answer? Will the user type in the correct answer? Will the answer be in UPPER or lower case or a cOmbInaTion of both? How do you as the programmer check for all of these variations? The answer lies in turning the user's response into something that can easily be checked. Type the following program in.
( This code can be found at .\tutorial\Lesson9\Ucase.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
DIM Answer$ ' the answer supplied by the user
'---------------------
'- Main Program Here -
'---------------------
PRINT
INPUT "What is the capital of Ohio? > ", Answer$ ' get response from user
PRINT
IF UCASE$(Answer$) = "COLUMBUS" THEN ' convert to upper case and test for correct answer
PRINT "Correct!" ' answer was correct
ELSE ' something other than correct answer typed in
PRINT "You lose!" ' user wins nothing
END IF
As long as the user knows how to spell Columbus the case of the response does not matter. The UCASE$ statement in line 14 returns the string parameter associated with it in all UPPER CASE. The LCASE$ statement does just the opposite, returning the string parameter associated with it in all lower case. Line 14 could have just as easily been:
IF LCASE$(Answer$) = "columbus" THEN ' convert to lower case and test for correct answer
and the program would behave just as it did using UCASE$.
The LTRIM$, RTRIM$, and _TRIM$ Statements
User's do funny things like not enter responses as we programmers expect. You always have to take into account the different ways a response can be entered in the WRONG way by a user for your code to be effective. The example code above will fail if the user enter spaces (presses the space bar) either before or after the word Columbus. The spaces will be seen as legitimate characters typed in by the user.
There are three statements that will remove leading, trailing, and both leading and trailing spaces from a string. LTRIM$ removes leading, or left hand, spaces from a string. RTRIM$ removes trailing, or right hand, spaces from a string, and _TRIM$ removes both leading and trailing spaces from a string. If you change line 14 in the example code to:
IF _TRIM$(UCASE$(Answer$)) = "COLUMBUS" THEN
Answer$ will first be converted to upper case and then have both leading and trailing spaces removed from it. Yes, this new line of code looks a bit complicated because one statement is embedded into another. This is a very common thing to do in programming that enables multiple actions to be taken on a parameter at the same time. Simply follow the order of operations to see how the line of code operates. UCASE$ falls inside of _TRIM$'s parenthesis so the UCASE$ statement is acted upon first. Once UCASE$ has returned the upper case form of Answer$ it's _TRIM$'s turn to take that upper case Answer$ and remove the leading and trailing spaces. You now have a string returned that is in both upper case and has had the leading and trailing spaces removed.
You could even change the line of code to read:
Answer$ = _TRIM$(UCASE$(Answer$))
IF Answer$ = "COLUMBUS" THEN
to permanently modify Answer$ by making the changes and then placing those changes back into Answer$ itself. This is handy if you need to reference Answer$ again later in the code as it will save you from having to test for upper case and leading or trailing spaces again.
The INSTR Statement
The previous examples work great for single word answers but those darn users will always test the limits of your code. What if the user were to answer "I think it's Columbus?" Technically the answer has the correct response embedded in the string the user entered but how do we see that in code?
The INSTR statement has the ability to search a base string with a defined search string and return a numeric value of where the search string was found within the base string. Once again let's modify the previous example code to see this in action. When you execute the program type your answer in as "I believe Columbus would be the answer" and see if you are correct.
( This code can be found at .\tutorial\Lesson9\Instr.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
DIM Answer$ ' the answer supplied by the user
'---------------------
'- Main Program Here -
'---------------------
PRINT
INPUT "What is the capital of Ohio? > "; Answer$ ' get response from user
PRINT
IF INSTR(UCASE$(Answer$), "COLUMBUS") THEN ' convert to upper case and search for correct answer
PRINT "Correct!" ' answer was correct
ELSE ' the correct answer was not within the string
PRINT "You lose!" ' user wins nothing
END IF
Figure 1: This is how Skynet got its start!
INSTR requires a string to search called the base string. In this case the base string is the upper case result of UCASE$, so the entire response entered is converted to upper case. The next parameter that INSTR requires is a string to search for called the search string. In line 14 we supplied INSTR with a search string of "COLUMBUS". If the string "COLUMBUS" is found anywhere within the user's response INSTR will return a numeric value of where it was found. If INSTR is anything other than zero then "Correct!" will get printed since we know that the IF...THEN statement will consider any numeric value other than zero as true.
There is one strange behavior of INSTR that needs to be pointed out. The following line of code will result in a value of 1 being returned.
Location% = INSTR("ABCDE", "") ' null string returns a value of 1
A search for a null string ( "" ) will always result in a positive value being returned (unless you search for a null string within a null string). This may sound like a bug but in actuality every string does contain a null string. You just need to be aware of this behavior if you start receiving a result you did not expect.
INSTR also has another trick up its sleeve. It can find multiple occurrences of the search string in the base string and report back where it finds all of them. Type in the following example to see how this works.
( This code can be found at .\tutorial\Lesson9\InstrDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
DIM Position% ' each position the search string is found at
DIM Phrase$ ' the base string
DIM Search$ ' the search string
DIM NextLine% ' the print line explaining where search string found
'----------------------------
'- Main Program Begins Here -
'----------------------------
Phrase$ = "The rain in Spain falls mainly on the plain." ' create INSTR base string
Search$ = "ain" ' create INSTR search string
Position% = 0 ' reset position of string found
NextLine% = 4 ' first text line to print results
PRINT Phrase$ ' display the base string
DO ' loop through the base string
Position% = INSTR(Position% + 1, Phrase$, Search$) ' look for search string at last position
IF Position% THEN ' was a match found?
LOCATE NextLine%, 1 ' yes, set cursor line location
NextLine% = NextLine% + 1 ' increment cursor line location for next time
PRINT "Found "; CHR$(34); Search$; CHR$(34); " at position"; Position% ' print result to screen
LOCATE 2, Position% ' locate cursor position below search string
PRINT CHR$(24); ' print an up arrow symbol where found
END IF
LOOP UNTIL Position% = 0 ' leave when no more matches found
Figure 2: The weather in Spain seems nice
The INSTR statement can accept an optional position parameter as seen in line 20 of the code.
Position% = INSTR(Position% + 1, Phrase$, Search$)
Each time an instance of "ain" is found that position is recorded so the next time around Position% + 1 can be used to resume the search through the remainder of the base string. This continues on until Position% becomes zero, meaning no more matches found, and ending the loop.
The STR$ and VAL Statements
There will be times when you need to convert a string to a numeric value and a numeric value to a string. The STR$ statement is used to convert a numeric value to a string. The VAL statement is used to convert a string to a numeric value.
LINE INPUT "Enter a number between 1 and 10 > ", Number$
Value! = VAL(Number$) ' convert string to a numeric value
The above example shows a number being asked for however it's being saved in a string variable. The VAL statement is used to convert Number$ into an actual numeric value and then saved into the single type variable Value!. If the characters in Number$ are non-numeric, such as "Hello World", VAL will simply return a numeric value of zero.
INPUT "Enter a number between 1 and 10 > ", Value%
Number$ = STR$(Value%) ' convert numeric value to a string
In this example the opposite is being performed. The numeric value contained within Value% is being converted to a string and then saved into Number$. Positive numeric values converted to strings will always contain a leading space. The space is there for the possibility of a negative value that includes a minus sign. For example:
PRINT "*"; STR$(10); "*" ' * 10* printed to the screen
PRINT "*"; STR$(-10); "*" ' *-10* printed to the screen
You'll need to use either LTRIM$ or _TRIM$ to remove the leading space if you do not wish it to be present.
The LEN Statement
The RIGHT$, LEFT$, and MID$ Statements
These statements are used to parse, or break apart, a string into smaller pieces. An example of this is when you use the TIME$ and DATE$ statements to retrieve the time and date from QB64. TIME$ delivers the time in the string form "HH:MM:SS" and DATE$ in the string form "MM-DD-YYYY". In order to get the individual hours, minutes, and seconds from TIME$, and the individual month, day, and year from DATE$ you'll need to to use the LEFT$, RIGHT$, and MID$ statements. Type the following example program into your IDE to show this.
( This code can be found at .\tutorial\Lesson9\ParseDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
DIM MonthName$(12) ' array storing the names of the months
DIM Hours% ' numeric value of hour
DIM Month% ' numeric value of month
DIM Day% ' numeric value of day
DIM Year% ' numeric value of year
DIM Suffix$ ' day suffix
DIM AmPm$ ' AM or PM
'----------------------------
'- Main Program Begins Here -
'----------------------------
MonthName$(1) = "January" ' store the month names
MonthName$(2) = "February"
MonthName$(3) = "March"
MonthName$(4) = "April"
MonthName$(5) = "May"
MonthName$(6) = "June"
MonthName$(7) = "July"
MonthName$(8) = "August"
MonthName$(9) = "September"
MonthName$(10) = "October"
MonthName$(11) = "November"
MonthName$(12) = "December"
DO ' begin main loop
Month% = VAL(LEFT$(DATE$, 2)) ' extract value of month
Day% = VAL(MID$(DATE$, 4, 2)) ' extract value of day
Year% = VAL(RIGHT$(DATE$, 4)) ' extract value of year
Hours% = VAL(LEFT$(TIME$, 2)) ' extract value of hours
IF Hours% > 12 THEN ' military time?
Hours% = Hours% - 12 ' yes, convert to civilian
AmPm$ = "PM" ' it's the afternoon
ELSE ' no
AmPm$ = "AM" ' it's the morning
END IF
IF Day% = 1 OR Day% = 21 OR Day% = 31 THEN ' one of these days?
Suffix$ = "st," ' yes, day ends in st
ELSEIF Day% = 2 OR Day% = 22 THEN ' no, one of these days?
Suffix$ = "nd," ' yes, day ends in nd
ELSEIF Day% = 3 OR Day% = 23 THEN ' no, one of these days?
Suffix$ = "rd," ' yes, days ends in rd
ELSE ' no
Suffix$ = "th," ' day must end in th then
END IF
LOCATE 2, 2 ' position cursor
Dt$ = "The current date is " + MonthName$(Month%) ' build new date string
Dt$ = Dt$ + STR$(Day%) + Suffix$ + STR$(Year%) + " "
PRINT Dt$ ' display date string
LOCATE 4, 2 ' position cursor
Tm$ = "The current time is " + RIGHT$("0" + LTRIM$(STR$(Hours%)), 2) ' build new time string
Tm$ = Tm$ + " Hours, " + MID$(TIME$, 4, 2) + " Minutes and "
Tm$ = Tm$ + RIGHT$(TIME$, 2) + " Seconds " + AmPm$
PRINT Tm$ ' display time string
LOOP UNTIL INKEY$ <> "" ' end loop if key pressed
SYSTEM ' return to Windows
Figure 3: Time to make the donuts!
The LEFT$ statement is used to grab a predetermined number of characters starting at the left-hand side of the string. In line 30 of the program LEFT$ is used to parse just the month portion of DATE$.
Month% = VAL(LEFT$(DATE$, 2)) ' extract value of month (parse MM from MM-DD-YYYY)
A statement within a statement within a statement! WooHoo, as I stated before this is a common occurrence in programming. Let's break it down.
DATE$ returns a string in the form of "MM-DD-YYYY". Then we plug that string into LEFT$:
LEFT$("MM-DD-YYYY", 2) ' get the first two left hand characters from MM-DD-YYYY
LEFT$ grabbed the first two characters of the string which equals the month. In our example this would be "04" or April. The VAL statement is now used to convert that string into an actual numeric value:
VAL("04") ' convert to a true numeric value
LEFT$ was also used in the same manner on line 33 to get the current hour from TIME$.
Hours% = VAL(LEFT$(TIME$, 2)) ' extract value of hours (parse HH from HH:MM:SS)
In line 31 the MID$ statement is used to parse out a string from within, or the middle, of DATE$:
Day% = VAL(MID$(DATE$, 4, 2)) ' extract value of day (parse DD from MM-DD-YYYY)
MID$ starts at position 4 of the string and then parses 2 characters starting at that position.
And finally the RIGHT$ statement is used to parse out the remaining date from the right-hand side in line 32:
Year% = VAL(RIGHT$(DATE$, 4)) ' extract value of year (parse YYYY from MM-DD-YYYY)
The ASC and CHR$ Statements
The CHR$ statement is used to print any value from 0 to 255 that corresponds to a character within the ASCII table. Typically CHR$ is used to print characters that are not accessible through the keyboard. One example would be playing card suits that are in the ASCII table.
PRINT CHR$(3) ' ♥ heart symbol
PRINT CHR$(4) ' ♦ diamond symbol
PRINT CHR$(5) ' ♣ club symbol
PRINT CHR$(6) ' ♠ spade symbol
The ASC statement does just the opposite and returns the ASCII numeric value of a character passed to it.
PRINT ASC("A") ' 65 printed to the screen
PRINT ASC(" ") ' 32 printed to the screen
This example program shows how the ASC and CHR$ statements can be used together to identify keystrokes.
( This code can be found at .\tutorial\Lesson9\CHR_ASC.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
DIM KeyPress$ ' single key presses by user
DIM KeyValue% ' ASCII value of key pressed
'----------------------------
'- Main Program Begins Here -
'----------------------------
DO ' begin main loop
DO ' begin key input loop
KeyPress$ = INKEY$ ' get any key pressed
LOOP UNTIL KeyPress$ <> "" ' leave loop if key pressed
KeyValue% = ASC(KeyPress$) ' get ASCII value of key pressed
IF KeyValue% < 32 THEN ' is value less than 32?
PRINT " Control key "; ' yes, this is a control character
ELSEIF KeyValue% > 47 AND KeyValue% < 58 THEN ' no, is value between 47 and 58?
PRINT " Numeric key "; ' yes, this is a numeric character
ELSEIF KeyValue% > 64 AND KeyValue% < 91 THEN ' no, is value between 64 and 91?
PRINT " UPPERcase key "; ' yes, this is an upper case character
ELSEIF KeyValue% > 96 AND KeyValue% < 123 THEN ' no, is value between 96 and 123?
PRINT " Lowercase Key "; ' yes, this is a lower case character
ELSEIF KeyValue% = 32 THEN ' no, is value 32?
PRINT " Spacebar Key "; ' yes, this is a space character
ELSE ' no
PRINT " Symbol Key "; ' assume all others are symbol characters
END IF
PRINT CHR$(26); " "; CHR$(KeyValue%) ' print right arrow and character
LOOP UNTIL KeyPress$ = CHR$(27) ' leave main loop when ESC key pressed
The ASC statement also has another trick up its sleeve that was not available with the original QuickBasic implementation. QB64 has added an optional position parameter:
code% = ASC(stringExpression$[, position%])
When the position% parameter is applied ASC acts like MID$ in that it will report the ASCII value at the supplied position within the string.
a$ = "The rain in Spain" ' test string
PRINT ASC(a$, 3) ' ASCII value of position 3 in test string (letter e)
PRINT CHR$(ASC(a$, 3)) ' print the character at position 3 in the test string (letter e)
When you need to find the ASCII value of a position within a string use the ASC statement instead of MID$. The ASC statement is much faster than the MID$ statement.
The STRING$ and SPACE$ Statements
When you need a lot of the same character in a row the STRING$ statement has you covered. Simply supply STRING$ with a numeric value and a character like so:
PRINT STRING$(80, "*") ' 80 asterisks will be printed to the screen
You can also supply STRING$ with the ASCII numeric value of a character.
PRINT STRING$(80, 42) ' 80 asterisks will be printed to the screen
The STRING$ statement comes in handy when building text screen boxes by using the extended ASCII characters provided to do so. Type in the following example.
( This code can be found at .\tutorial\Lesson9\ASCIIBoxes.bas )
'----------------------------
'- Main Program Begins Here -
'----------------------------
PRINT
PRINT " An ASCII box drawing demo"
PRINT " -------------------------"
PRINT
PRINT CHR$(218); STRING$(17, 196); CHR$(191)
PRINT CHR$(179); " Single Line Box "; CHR$(179)
PRINT CHR$(192); STRING$(17, 196); CHR$(217)
PRINT
PRINT CHR$(201); STRING$(17, 205); CHR$(187)
PRINT CHR$(186); " Double Line Box "; CHR$(186)
PRINT CHR$(200); STRING$(17, 205); CHR$(188)
Figure 4: Old school ASCII boxes
The SWAP Statement
The SWAP statement is used to switch values between two numeric or string values. The variables to be swapped must be of the same type. The following example shows the SWAP statement in action.
( This code can be found at .\tutorial\Lesson9\SwapDemo.bas )
'--------------------------------
'- Variable Declaration Section -
'--------------------------------
DIM Var1% ' create two integers to swap
DIM Var2%
DIM S1$ ' create two strings to swap
DIM S2$
'----------------------------
'- Main Program Begins Here -
'----------------------------
S1$ = "String 1" ' give variables values
S2$ = "String 2"
Var1% = 1
Var2% = 2
PRINT
PRINT " Before swap"
PRINT " -----------"
PRINT
PRINT " Var1% ="; Var1%
PRINT " Var2% ="; Var2%
PRINT " S1$ = "; S1$
PRINT " S2$ = "; S2$
SWAP S1$, S2$ ' swap the string values between variables
SWAP Var1%, Var2% ' swap the integer values between variables
PRINT
PRINT " After swap"
PRINT " ----------"
PRINT
PRINT " Var1% ="; Var1%
PRINT " Var2% ="; Var2%
PRINT " S1$ = "; S1$
PRINT " S2$ = "; S2$
Figure 5: Variable swapping
The LOCATE, CSRLIN, and POS Statements
The LOCATE, CSRLIN, and POS statements are used to set and retrieve the position of the text cursor on the screen. The following example code show hows to use the LOCATE command.
( This code can be found at .\tutorial\Lesson9\LocateDemo.bas )
'*
'* LOCATE demo
'*
DIM x%, y% ' current mouse pointer position
DIM Cur$ ' string containing current mouse pointer position
DIM Loc$ ' string containing the LOCATE command that would be used
DIM Blink% ' counter used to control blinking character
_MOUSEHIDE ' hide the operating system mouse pointer
DO ' begin main loop
_LIMIT 60 ' 60 frames per second
COLOR 7, 0 ' white text with black background
CLS ' clear screen and print directions
PRINT
PRINT " Use the mouse to move the cursor around on the screen."
PRINT " Click the left mouse button to print a smiley, right button to try again."
PRINT " Press ESC to quit."
WHILE _MOUSEINPUT: WEND ' get latest mouse update
x% = _MOUSEX ' get mouse pointer positions
y% = _MOUSEY
Cur$ = " CURSOR X = " + _TRIM$(STR$(x%)) + " Y = " + _TRIM$(STR$(y%)) + " " ' build string
COLOR 14, 1 ' bright yellow text with blue background
LOCATE 25, (80 - LEN(Cur$)) \ 2 ' center text at bottom of screen
PRINT Cur$; ' print current mouse position
IF x% > 0 AND y% > 0 AND y% < 26 THEN ' is the mouse on the program window?
COLOR 15, 0
LOCATE y%, x% ' yes, position the text cursor
PRINT CHR$(219); ' print solid block as mouse cursor
END IF
IF _MOUSEBUTTON(1) THEN ' left mouse button clicked?
Loc$ = " LOCATE " + _TRIM$(STR$(y%)) + ", " + _TRIM$(STR$(x%)) + " " ' build string
COLOR 14, 1 ' bright yellow text with blue background
LOCATE 25, (80 - LEN(Loc$)) \ 2 ' center text at bottom of screen
PRINT Loc$; ' print LOCATE command that would be used
COLOR 15, 0 ' bright white text with black background
DO ' begin flashing smiley loop
_LIMIT 60 ' keep loop FPS low to conserve CPU resources
LOCATE y%, x% ' position the text cursor
IF Blink% < 31 THEN ' counter less than 31?
PRINT CHR$(1); ' yes, print a smiley face
ELSE ' no, counter is greater than 30
PRINT CHR$(2); ' print a solid smiley face
END IF
Blink% = Blink% + 1 ' increment blink counter
IF Blink% = 61 THEN Blink% = 0 ' reset blink counter when needed
WHILE _MOUSEINPUT: WEND ' get latest mouse update
LOOP UNTIL _MOUSEBUTTON(2) OR _KEYDOWN(27) ' leave loop when right mouse button clicked
END IF
LOOP UNTIL _KEYDOWN(27) ' leave main program loop when ESC pressed
SYSTEM ' return to the operating system
Figure 6: Using LOCATE to position the text cursor
As you move the cursor around the screen it's position is printed at the bottom of the screen. When you click the left mouse button a smiley character appears where the mouse cursor was. The LOCATE statement that would be used to position the text cursor where the smiley is printed is shown at the bottom of the screen. Did you notice something about the numbers that were displayed at the bottom? If you look closely they are reversed. That's because the LOCATE statement requires the text row first (the y coordinate) and then the column (the x coordinate). This is backwards from other coordinate oriented commands:
LOCATE row%, column%
The LOCATE statement can also be used to manipulate the text cursor in other ways. You can use LOCATE to hide or show the flashing cursor and well as control the size and shape of it with optional parameters:
LOCATE row%, column%, cursor%, cursorstart%, cursorstop%
cursor% can be set to 0 to turn the text cursor off or 1 to turn the text cursor on.
The text cursor is made up of 31 scan lines and cursorstart% and cursorstop% can be used to control which of those scan lines are used to change the shape of the text cursor. Both of these optional parameters can accept a value between 0 and 31.
The CSRLIN statement is used retrieve the current text row the cursor resides in (the y coordinate) and POS is used to retrieve the current text column the cursor resides at (the x coordinate). Here is another demonstration of these two commands being used together.
( This code can be found at .\tutorial\Lesson9\CsrlinPosDemo.bas )
'*
'* CSRLIN and POS demo
'*
DIM Row%, Column% ' row and column of saved cursor position
DIM Count% ' generic counter
RANDOMIZE TIMER ' seed random number generator
CLS ' clear screen
PRINT "Let's print a random number of lines." ' inform user what's happening
PRINT
SLEEP 5 ' pause for 5 seconds
FOR Count% = 1 TO 5 + INT(RND * 5) + 1 ' cycle from 5 up to 10
PRINT "Random line number ->"; Count% ' show user the random lines
_DELAY .5 ' pause for 1/2 second
NEXT Count%
PRINT ' inform user of progress
PRINT "*** Now for a random length of characters. ***"
PRINT
SLEEP 5 ' pause for 5 seconds
Rand% = 20 + INT(RND * 40) + 1 ' generate random number between 20 and 60
PRINT STRING$(Rand%, "*"); ' print that many asterisks (notice the ; at end)
Row% = CSRLIN ' get current row position of text cursor
Column% = POS(0) ' get current column position of text cursor
LOCATE Row% + 1, Column% ' place text cursor directly under that spot
PRINT CHR$(24); ' print an up arrow symbol
LOCATE Row% + 2, Column% - 14 ' inform user what happened
PRINT "This position has been saved"
LOCATE Row% + 3, Column% - 11
PRINT "with CSRLIN and POS(0)."
LOCATE Row% + 5, Column% - 13
PRINT "Lets do a countdown there."
SLEEP 5 ' pause for 5 seconds
LOCATE Row%, 1 ' position cursor at the beginning of the asterisk line
PRINT SPACE$(70) ' remove the asterisks
FOR Count% = 10 TO 1 STEP -1 ' cycle from 10 to 1
LOCATE Row%, Column% ' position the text cursor at the saved location
PRINT _TRIM$(STR$(Count%)); " "; ' print current count down value
_DELAY .5 ' pause for 1/2 second
NEXT Count%
Figure 7: Using CSRLIN and POS to save the text cursor location
The above code is fairly straight forward however there is one thing to point out. After the string of asterisks has been printed in line 22 there is a semicolon ( ; ) immediately following the PRINT command. This is to keep the text cursor from moving to the beginning of the next line. Remember that a semicolon used within a PRINT statement tells the text cursor to stay where it is and print what ever follows right after. It's also a handy way to keep the text cursor where you want it as is this case with this code.
The POS statement curiously requires a value within parenthesis after it:
Column% = POS(0) ' get current column position of text cursor
However the value of this number means nothing and 0 is usually placed there. It can be any integer value you wish, but again means nothing.
The Semicolon (;), Comma (,) and TAB Statement
The LOCATE, CSRLIN, and POS statements are great for precise control of the text cursor anywhere on the screen. If all you need is a little text cursor control on a single line however then the semicolon ( ; ), comma ( , ), and TAB statement are what you need.
The semicolon tells the PRINT command to leave the text cursor at the end of the PRINT statement instead of the default behavior of moving the cursor to the next line (also known as a CR/LF or Carriage Return/Line Feed). The following lines of code prints two literal strings on separate lines:
PRINT "Hello there."
PRINT "My name is Bob."
No mystery there as this is the expected behavior. However, add a semicolon ( ; ) after the first PRINT statement and things change:
PRINT "Hello There.";
PRINT "My name is Bob."
Even though two PRINT statements were used they both printed to the same line. The semicolon ( ; ) told the first PRINT statement to leave the text cursor alone and let it remain where it is. The second PRINT statement uses that text cursor position to print its string of information. This can be used to create complex lines of information with a single PRINT statement:
Fname$ = "Bob"
Age% = 28
Job$ = "programmer"
PRINT "Hello there. My name is "; Fname$; ". I'm"; Age%; "years old and I'm a "; Job$; "."
The comma ( , ) is used in the same manner as a semicolon ( ; ) but instead of leaving the text cursor at the end of the line it moves it to the next tab position on the current line line. All screens have hidden tab points on them spaced 15 characters apart. This is a throwback from the function of typewriters that would move the platen over to the next tab position by pressing the TAB key. This was a quick way for a typist to either move the platen quickly to the left or line up fields of information easily. The next lines of code show this behavior:
PRINT
FOR count% = 1 TO 5
PRINT "Column"; count%, ' neatly spaced columns 15 characters apart
NEXT count%
PRINT: PRINT
FOR count% = 1 TO 5
PRINT "13 Characters", ' neatly spaced columns 15 characters apart
NEXT count%
PRINT: PRINT
FOR count% = 1 TO 5
PRINT "14 Characters.", ' too many characters to neatly space in columns using a comma
NEXT count%
The first two FOR ... NEXT loops lined up the output in nice neat columns spaced 15 characters apart. However, you can only have up to 13 characters in a column. If you place more than 13 characters in a column the next column is skipped in favor of the one after that as seen with the last FOR ... NEXT behavior.
If you need precise control of tab positions on a line then the TAB statement is needed. Instead of the default 15 characters you can control where the next tab position occurs.
PRINT
FOR count% = 1 TO 8
PRINT "Tab"; (count% - 1) * 10; TAB(count% * 10); 'neatly spaced columns 10 characters apart
NEXT count%
PRINT: PRINT
FOR count% = 1 TO 8
PRINT "13 Characters"; TAB(count% * 10); ' too many characters to neatly space
NEXT count%
The first FOR ... NEXT loop lines the text up in 8 columns tabbed 10 spaces apart as expected. The TAB(count% * 10); at the end of each line within the loop moved the cursor to that calculated tab position.
However, if the next TAB position is not available because it has already been used the TAB statement will use that position on the following line. This can lead to unexpected results as seen in the second FOR ... NEXT loop.
The COLOR Statement
The COLOR statement is used to change the background and foreground color of text to be printed to the screen. The COLOR statement needs to parameters:
COLOR foreground&, background&
The foreground& is the color of the text and the background& is the color strip contained behind the text. Now that you can manipulate strings of data it's time to give those strings a dash of color. Type in the example code.
( This code can be found at .\tutorial\Lesson9\ColorDemo.bas )
'* SCREEN 0 COLOR Demo
DIM fg&, bg& ' foreground and background color
DIM f$, b$, bf$ ' right aligned foreground, background, and blinking values
DO ' begin main loop
COLOR 7, 0 ' white text on black background
CLS
PRINT TAB(22); "************************************" ' print header
PRINT TAB(22); "** SCREEN 0 text color attributes **"
PRINT TAB(22); "************************************"
PRINT
PRINT TAB(34); "Page"; STR$(bg& + 1); " of 8"
FOR fg& = 0 TO 15 ' cycle through 16 foreground colors
f$ = RIGHT$(" " + _TRIM$(STR$(fg&)), 2) ' create right aligned numbers
bf$ = RIGHT$(" " + _TRIM$(STR$(fg& + 16)), 2) '
b$ = RIGHT$(" " + _TRIM$(STR$(bg&)), 2)
LOCATE CSRLIN, 12 ' position text cursor
COLOR fg&, bg& ' set text color
PRINT " This is COLOR "; f$; ","; b$, ' print non-blinking colored text
COLOR fg& + 16, bg& ' set text color
PRINT " This is COLOR "; bf$; ","; b$; " " ' print blinking colored text
NEXT fg&
COLOR 7, 0 ' white text on black background
PRINT
PRINT TAB(27); "Press ENTER for next color" ' print instructions
PRINT TAB(34); "ESC to exit";
WHILE INKEY$ = "": _LIMIT 10: WEND ' wait for a key press
bg& = bg& + 1 ' increment to next background color
LOOP UNTIL bg& = 8 OR _KEYDOWN(27) ' leave loop when finished or ESC pressed
SYSTEM ' return to operating system
When the above code is executed the column of the text on the right is blinking. Foreground color values can be in the ranges of:
0 through 15 for standard non-blinking colors
16 through 31 for standard blinking colors (simply add 16 to the color value to make text blink)
Background color values can range from 0 to 7 for a total of eight. The standard SCREEN 0 text and background colors are:
0 - BLACK 8 - DARK GRAY
1 - BLUE 9 - LIGHT BLUE
2 - GREEN 10 - LIGHT GREEN
3 - CYAN 11 - LIGHT CYAN
4 - RED 12 - LIGHT RED
5 - MAGENTA 13 - LIGHT MAGENTA
6 - BROWN 14 - YELLOW
7 - LIGHT GRAY 15 - WHITE
Note: The color blue indicated for value 1 above has been altered. The true color of blue for position 1 was too difficult to see given the blue background of this page. The same goes for black.
Legacy SCREENs for the most part have these color limitations however 32bit screens do not. Type in the following code.
( This code can be found at .\tutorial\Lesson9\32bitColorDemo.bas )
'* 32bit COLOR Demo
TYPE RGB ' fading RGB definition
r AS INTEGER ' red component
g AS INTEGER ' green component
b AS INTEGER ' blue component
rdir AS INTEGER ' red fade direction
gdir AS INTEGER ' green fade direction
bdir AS INTEGER ' blue fade direction
END TYPE
DIM RGB(203) AS RGB ' create fade array
DIM c% ' generic counter
RANDOMIZE TIMER ' seed random number generator
FOR c% = 1 TO 203 ' cycle 203 times
RGB(c%).r = INT(RND * 240) + 5 ' random red, green, blue components
RGB(c%).g = INT(RND * 240) + 5 ' between 5 and 250
RGB(c%).b = INT(RND * 240) + 5
DO
RGB(c%).rdir = INT(RND * 2) - INT(RND * 2) ' random red, green, blue directions
RGB(c%).gdir = INT(RND * 2) - INT(RND * 2) ' between -1 and 1
RGB(c%).bdir = INT(RND * 2) - INT(RND * 2)
LOOP UNTIL RGB(c%).rdir AND RGB(c%).gdir AND RGB(c%).bdir ' make sure no direction is zero
NEXT c%
SCREEN _NEWIMAGE(640, 480, 32) ' 32bit graphics screen
DO ' begin main loop
_LIMIT 120 ' 120 frames per second
FOR c% = 1 TO 203 ' cycle 203 times
COLOR _RGB32(RGB(c%).r, RGB(c%).g, RGB(c%).b), 0 ' set next text color
PRINT "Psychodelic"; ' print text in that color
IF RGB(c%).r = 255 OR RGB(c%).r = 1 THEN ' has red component reached limit?
RGB(c%).rdir = -RGB(c%).rdir ' yes, reverse direction
END IF
RGB(c%).r = RGB(c%).r + RGB(c%).rdir ' increment red component
IF RGB(c%).g = 255 OR RGB(c%).g = 1 THEN ' has green component reached limit?
RGB(c%).gdir = -RGB(c%).gdir ' yes, reverse direction
END IF
RGB(c%).g = RGB(c%).g + RGB(c%).gdir ' increment green component
IF RGB(c%).b = 255 OR RGB(c%).b = 1 THEN ' has blue component reached limit?
RGB(c%).bdir = -RGB(c%).bdir ' yes, reverse direction
END IF
RGB(c%).b = RGB(c%).b + RGB(c%).bdir ' increment blue component
NEXT c%
_DISPLAY ' display updates made to screen
LOOP UNTIL _KEYDOWN(27) ' leave when ESC key pressed
SYSTEM ' return to operating system
Figure 8: Groovy
As Figure 8 shows you can assign any _RGB32 value to COLOR to get the full spectrum of colors in a 32bit screen as shown in line 30 of the code above.
The PRINT USING Statement
The PRINT USING statement is used to print text in a predetermined format by supplying a template to use. This is a very powerful statement and one that too often gets overlooked. To understand how it works type in the example code below.
( This code can be found at .\tutorial\Lesson9\UsingDemo.bas )
'* PRINT USING Demo
DIM fn$ ' first name
DIM ln$ ' last name
DIM afn$ ' aka first name
DIM aln$ ' aka last name
DIM ch$ ' character name
DIM dj% ' date joined
DIM cb% ' cyan bar toggle flag
SCREEN _NEWIMAGE(585, 630, 32) ' set up screen
CLS , _RGB32(255, 255, 255) ' white background
COLOR _RGB32(0, 0, 0), _RGB32(255, 255, 255) ' black text on white background
PRINT
PRINT " Avengers Roster 1960's through 1980's" ' print header
PRINT
PRINT " FIRST LAST AKA CHARACTER DATE"
PRINT " NAME NAME FIRST LAST NAME JOIN"
PRINT " ---------- ---------- ---------- ---------- --------------- ----"
Format$ = " \ \ \ \ \ \ \ \ \ \ #### " ' output formatter
cb% = 1
DO ' begin main loop
READ fn$, ln$, afn$, aln$, ch$, dj% ' read in data fields
COLOR _RGB32(0, 0, 0), _RGB32(255, 255, 255) ' black text on white background
IF dj% THEN ' end of data?
IF cb% THEN COLOR _RGB32(0, 0, 0), _RGB32(0, 223, 223) ' no, cyan text background every other line
PRINT USING Format$; fn$; ln$; afn$; aln$; ch$; dj% ' print formatted data fields
cb% = 1 - cb% ' toggle cyan background flag
END IF
LOOP UNTIL dj% = 0 ' leave when no more data
SLEEP ' wait for key press
SYSTEM ' return to operating system
' DATA statements used to simulate getting data from source such as a file
DATA "Henry","Jonathan","Hank","Pym","Ant Man",1963,"Janet","VanDyne","Janet","Pym","Wasp",1963
DATA "Anthony","Edward","Tony","Stark","Iron Man",1963,"Robert","Banner","Bruce","Banner","The Hulk",1963
DATA "Thor","Odinson"," "," ","Thor",1963,"Richard","Milhouse","Rick","Jones","Honorary Member",1963
DATA "Steven","Rogers"," "," ","Captain America",1964,"Clinton","Barton"," "," ","Hawkeye",1965
DATA "Pietro","Maximoff","Pietro","Frank","Quicksilver",1965
DATA "Wanda","Maximoff","Wanda","Frank","Scarlet Witch",1965,"Heracles"," ","Harry","Cleese","Hercules",1967
DATA "T'Challa"," "," "," ","Black Panther",1968,"Victor","Shade"," "," ","Vision",1968
DATA "Dane","Whitman"," "," ","Black Knight",1969,"Natalia","Romanova","Natasha","Romanoff","Black Widow",1973
DATA "Brandt"," "," "," ","Mantis",1973,"Henry","McCoy"," "," ","Beast",1975
DATA "Heather","Douglas"," "," ","Moondragon",1975,"Patsy","Walker","Patsy","Hellstrom","Hellcat",1975
DATA "Matthew","Liebowitz","Matthew","Hawk","Two-Gun Kid",1975,"Robert","Frank"," "," ","Whizzer",1977
DATA "Simon","Williams"," "," ","Wonder Man",1977,"Yondu","Udonta"," "," ","Yondu",1978
DATA "Mar-Vell"," "," "," ","Captain Marvel",1978,"Samuel","Wilson"," "," ","Falcon",1979
DATA "Jennifer","Walters"," "," ","She-Hulk",1982,"Monica","Rambeau"," "," ","Spectrum",1983
DATA "Eros"," "," "," ","Starfox",1983,"James","Rhodes","Iron","Patriot","War Machine",1984
DATA "Barbara","Barton","Agent 19"," ","Mockingbird",1984,"Benjamin","Grimm"," "," ","Thing",1986
DATA " "," "," "," "," ",0000
Figure 9: Super hero roster
Where are all of the string manipulation commands to create the chart seen in Figure 9 above? There are not even any uses of semicolons, commas, or the TAB statement to get this output. That's the power of the PRINT USING command.
The reason this command is so often overlooked is because it was created to aid in formatting text on wide carriage printers that contained 132 columns and on texted based monochrome terminals back in the very early days of computing. Most of the old-school languages, such as COBOL, had statements like this too. When those went out of style programmers simply left this command behind. But as you can see this is an extremely useful tool at your disposal.
The secret lies in line 20 of the code. A string variable named Format$ contains a pre-made template on how to format data given to it. The "\ \" fields within the template means to keep a string value within this area. The "####" field at the end means to keep a numeric value within this area.
Format$ = " \ \ \ \ \ \ \ \ \ \ #### " ' output formatter
Format$ has been set up to receive 5 strings and a numeric value. When the first string is passed in its placed in the first "\ \" field, the second string in the second "\ \" field, and so on. The last field expects a numeric value to format within its "####" area. This is done in line 27 of the code.
PRINT USING Format$; fn$; ln$; afn$; aln$; ch$; dj% ' print formatted data fields
The five strings passed in are placed in the "\ \" fields and the integer is placed in the "####" field automatically. This would be mind-numbing If you had to do this with string manipulation commands such as LEFT$, RIGHT$, STR$, etc.. and then concatenate the formatted strings together. Using semicolons, commas, and TABs would be a pain too because you would always need to account for the length of the individual values so as not to mess the column alignment up.
There is a table of all the available formatting characters to be used with PRINT USING in the QB64 Wiki. The best way to get familiar with the PRINT USING statement is to play around with the various formatting characters. For instance, adding a dollar sign to a value to make it a monetary value and then printing it could be done like this:
Payout! = 100 ' pay day!
po$ = "$" + _TRIM$(STR$(Payout!)) ' insert $ at beginning
decimal% = INSTR(po$, ".") ' get position of decimal point
IF decimal% = 0 THEN ' was there a decimal point?
po$ = po$ + ".00" ' no, add .00 to end
ELSE ' yes, a decimal point exists
po$ = po$ + "00" ' append 00 to end to assure at least two places
po$ = LEFT$(po$, decimal%) + MID$(po$, decimal% + 1, 2) ' build monetary string
END IF
PRINT po$ ' phew! that was a lot of work
The above code ensures that no matter what the value in Payout! is, 100, 100.1, 100.99, etc.. that a decimal point is taken into account and the number of places in the decimal area is always 2. Or you could simply do this:
PRINT USING "$###.##"; Payout! ' easy peasy!
No string manipulation needed and no worrying about the decimal point. Again, very powerful!
A String Manipulation Demo
Here is a sample program that uses string manipulation to create a hidden password function.
( This code can be found at .\tutorial\Lesson9\HiddenPassword.bas )
'-------------------------------- ********************************************************
'- Variable Declaration Section - * Simple hidden password demo highlighting some of the *
'-------------------------------- * string manipulation commands available in QB64. *
' * *
DIM Login$ ' login name user supplies * LEN() - returns the length of a string *
DIM Password$ ' password user supplies * ASC() - returns the ASCII value of string character *
' * LEFT$() - returns left # of characters of a string *
'---------------------------- * STRING$() - returns a string of same characters *
'- Main Program Begins Here - ********************************************************
'----------------------------
PRINT
PRINT " ------------------------"
PRINT " - Ritchie's Web Server -"
PRINT " ------------------------"
PRINT
PRINT " Welcome to my web server!"
PRINT
PRINT " Before you can begin, you must create an account."
DO ' begin login loop
PRINT ' blank line
PRINT " Create a login name between 6 and 16 characters in length ." ' prompt user
LINE INPUT " Login > ", Login$ ' get login name
LOOP UNTIL LEN(Login$) > 5 AND LEN(Login$) < 17 ' continue if length ok
DO ' begin password loop
Password$ = "" ' clear current password
DO ' begin pwd length loop
PRINT " Enter a password that is at least 8 characters long." ' prompt user
Password$ = GetPassword$ ' get password from user
LOOP UNTIL LEN(Password$) > 7 ' continue if length ok
PRINT " Please verify the password by typing it in again." ' prompt user
LOOP UNTIL Password$ = GetPassword$ ' continue if same pwd
PRINT ' blank line
PRINT " Remember for your records:" ' inform user
PRINT ' blank line
PRINT " Login > "; Login$ ' display login name
PRINT " Password > "; Password$ ' display password
END ' end program
'-----------------------------------
'- Function and Subroutine section -
'-----------------------------------
'--------------------------------------------------------------------------------------------------
FUNCTION GetPassword$ ()
'**************************************************************************************************
'* Prompts the user for a password. As user types in password the keystrokes are displayed as *
'* asterisks. The back space key is recognized. When the user presses the ENTER key the password *
'* entered by the user is sent back to the calling routine. *
'**************************************************************************************************
'---------------------------
'- Declare local variables -
'---------------------------
DIM Cursorline% ' current Y location of cursor
DIM CursorPos% ' current X location of cursor
DIM Password$ ' password created by user
DIM KeyPress$ ' records key presses of user
'------------------------
'- Function begins here -
'------------------------
PRINT " Password > "; ' prompt user for input
Cursorline% = CSRLIN ' save cursor Y position
CursorPos% = POS(0) ' save cursor X location
DO ' begin main loop
DO ' begin key press loop
_LIMIT 30 ' limit to 30 loops per sec
KeyPress$ = INKEY$ ' get key user presses
LOOP UNTIL KeyPress$ <> "" ' loop back if no key pressed
IF ASC(KeyPress$) > 31 THEN ' was key pressed printable?
Password$ = Password$ + KeyPress$ ' yes, add it to password string
ELSEIF ASC(KeyPress$) = 8 THEN ' no, was it the back space key?
Password$ = LEFT$(Password$, LEN(Password$) - 1) ' yes, remove rightmost character
END IF
LOCATE Cursorline%, CursorPos% ' position cursor on screen
PRINT STRING$(LEN(Password$), "*"); " "; ' print string of asterisks
LOOP UNTIL KeyPress$ = CHR$(13) ' end main loop if ENTER pressed
PRINT ' move cursor from end of asterisks
GetPassword$ = Password$ ' return the password user supplied
END FUNCTION
'--------------------------------------------------------------------------------------------------
Figure 10: Don't peek!
Your Turn
Create a program that scans a user supplied sentence and counts the individual letters found within the sentence. Figure 11 below shows how the program should execute.
Figure 11: Counting letters
- Each letter in the sentence should be counted regardless of being upper or lower case.
- No graphics screen is needed. The program should run in a standard text screen.
- Save the program as LetterCount.BAS when finished.