AND NOW FOR MY NEXT TRICK
Sometimes a thing seems impossibly awkward to learn—but once you've learned it, it seems equally difficult to do without. Learning to ride a bicycle fell into that category for me, as did fractions—and QuickBASIC's FOR/NEXT construct.
The FOR statement is, in short, used when you want to repeat something a known number of times. WHILE, on the other hand, is used when you want to repeat something an unknown number of times, such as WHILE NOT EOF. In a moment we'll dig deeper into FOR, but let's first look at WHILE, since it's easier to understand. Here's how you could use WHILE to display the numbers from 1 to 10.
I = 1 WHILE I < = 10 'Display counter's value. PRINT I 'Increment counter. I = I + 1 'Jump to previous WHILE. WEND
This isn't bad, and it's pretty straightforward.
Now let's do the same with FOR:
FOR I = 1 TO 10 'Display counter's value. PRINT I 'Jump to previous FOR. NEXT I
Slick. FOR automatically does two things for us: It initializes the counter before the loop starts, and it increments the counter while the loop executes. One fringe benefit is that we never forget to initialize the loop counter. We can do so quite easily in a WHILE loop, since WHILE doesn't have the slightest interest in our loop counter's sordid history.
The FOR I = 1 TO 10 statement first sets I to 1, BASIC then checks to see if the value of the loop counter (I) has exceeded the terminal value, which is 10. Since it hasn't, all statements up to the NEXT are executed. In this example, it's just a PRINT statement. Since the variable I has been set conveniently to 1, all we have to do is print I.
At the NEXT, BASIC increments I, so I is now set to 2. The NEXT causes control to jump back to the FOR line. BASIC again checks the value of I—the loop counter—against the terminal value, which is 10. Still safe, so control passes through and trickles down to the NEXT again. When NEXT sets I to 10, the terminal value is still met so the loop continues a tenth time. But the NEXT bumps I up to 11. When control returns to the FOR, the value of I finally satisfies the terminal condition. The code inside the FOR loop is skipped this time and continues at the statement following the NEXT. A PRINT I statement here would show I to be 11, not 10.
But wait, there's more! Suppose you want to display only the odd numbers between 1 and 10. It's easy with WHILE but looks impossible with FOR, since the NEXT automatically adds 1 to the loop counter each time through. The answer is the optional STEP command, which is followed by a numeric expression that determines how much is added to the loop counter each time a NEXT is encountered.
Here's that odd-number program:
FOR I = 1 TO 10 STEP 2 PRINT I NEXT I
This prints the numbers 1 3 5 7 9 and stops, since the final NEXT bumps I to 11, thus satisfying the terminal condition. But suppose you want to print the numbers in reverse order? Why not just change the FOR line to this: FOR I = 10 TO 1? This seems like a good idea, but if you run the program, you'll see that nothing gets printed. This makes sense if you trace through the loop carefully. The initialization sets I to 10. Fine. The first time through the loop, the loop counter, I, is checked against the terminal condition, which is 1. Aha—I is 10, the terminal condition is 1, so the loop's finished! The answer is a negative step value:
'Print numbers from 10 to 1. 'Initialize counter and' execute code 10 times. FOR I = 10 TO 1 STEP -1 'Display counter's value. PRINT I 'Jump to previous FOR. NEXT I
While it hasn't been pointed out explicitly, a certain programming style has been assumed here. All lines of code within the FOR loop should be indented, with the FOR and the NEXT vertically aligned and to the left of the indented code. Here's an example from this month's program:
'Draw interior FOR Row = UpLeftRow + 1 TO UpLeftRow + High - 1 LOCATE Row, UpLeftCol + 1 PRINT STRING$(Wide - 1, "'); NEXT Row
You can also omit the name of the counter in the NEXT statement, but it's wise to leave the counter there. Deeply nested or indented code or FOR loops with a lot of lines in the loop require you to leave as many cues in as possible.
This month's program consists of DrawBox, a general-purpose subroutine to draw boxes, and a driver program to demonstrate the subroutine. DrawBox can put a box anywhere onscreen screen, make it any size, use any colors, and even use any set of characters to draw the box. Here's the subroutine definition: SUB DrawBox (UpLeftRow, UpLeftCol, Wide, High, TextColor, BackColor, Box$).
The box is drawn starting at coordinates UpLeftRow, UpLeftCol. Now most box-drawing routines I've seen take as the second two parameters the coordinates of the lower right corner of the box. That never did make sense to me; you normally think of the box as being a certain size and at a certain location. So the next two parameters are its width and height.
The next two parameters are the text and background colors of the box, respectively. These are the system video numbers, and they range from 0–15 for the text and 0–7 for the background. See the documentation for SCREEN to find out what color goes with what number. Suffice it to say that the driver program uses 7 (white) for the foreground and 0 (black) for the background.
The last parameter is Box$, and it's a little more complicated. Since some people like boxes to have a double border, others like a single border, and still others want to print the screen on a printer that doesn't support the snazzy extended character set in the video ROM, the characters used to draw the box are also passed in.
Box$ must contain exactly six characters. In this order, they are the characters used to draw horizontal bars, the upper left corner, the upper right corner, the vertical bar, the lower left corner, and the lower right corner. So if you fall into the last category, that of users with low-bit ASCII-only printers, you could simply make all the characters asterisks and the box would print perfectly.
Because it doesn't rely on global variables, you can pop the DrawBox routine out of this listing and place it unchanged into your own code. You need only ensure that the box won't be bigger than the screen when it's drawn; that condition will result in a runtime error.
' DRAWBOX.BAS -- General-purpose box-drawing subroutine and driver code. DEFINT A-Z DECLARE SUB DrawBox (UpLeftRow, UpLeftCol, Wide, High, TextColor, BackColor, Box$) ' These values represent the characters required to draw double and single boxes. DBOX$ = CHR$(205) + CHR$(201) + CHR$(187) + CHR$(186) + CHR$(200) + CHR$(188) SBox$ = CHR$(196) + CHE$(218) + CHR$(191) + CHR$(179) + CHR$(192) + CHR$(217) ' Horizontal Up. left Up. right Vertical Low. left Low. right ' Driver routine: requests box coordinate and size information from user, ' and presents both single and double boxes using those coordinates. CLS ' Get the box's upper left corner. INPUT "Starting row "; Row INPUT "Starting column "; Column ' Get the width and height of the box. INPUT "Width of box "; Wide INPUT "Height of box "; High ' Display the box using double bars. Colors are white (7) on black (0). CALL DrawBox(Row, Column, Wide, High, 7, 0, DBox$) ' Wait for a keystroke. WHILE INKEY$ = "": WEND ' Display the box using single bars, Colors are cyan (3) on blue (9). CALL DrawBox(Row, Column, Wide, High, 3, 9, SBox$) ' DrawBox displays a box starting at UpLeftRow, UpLeftCol. It is ' Wide characters deep and High characters tall. The box characters ' are displayed using the foreground attribute TextColor and the ' background attr. BackColor. The characters used to draw the box ' are in Box$. SUB DrawBox (UpLeftRow, UpLeftCol, Wide, High, TextColor, BackColor, Box$) ' Set the foreground (text) color to TextColor and the ' background color to BackColor. COLOR TextColor, BackColor ' Calculate these now to avoid doing it inside loops. BottomRow = UpLeftRow + High FarRight = UpLeftCol + Wide ' Draw interior FOR Row = UpLeftRow + 1 TO UpLeftRow + High - 1 LOCATE ROW, UpLeftCol + 1 ' Copy an interior row's worth of spaces in the current color ' out to the screen. PRINT STRINGS(Wide - 1, " "); NEXT Row ' Draw the top row. FOR col - UpLeftCol TO FarRight LOCATE UpLeftRow, Col ' Horizontal bar character. PRINT LEFTS(Box$, 1); NEXT Col ' Cap off the top row with corner characters. LOCATE UpLeftRow, UpLeftCol ' Upper left corner character. PRINT MIDS(BoxS, 2, 1); LOCATE UpLeftRow, FarRight ' Upper right corner character. PRINT MIDSfBoxS, 3, 1); ' Draw the sides. FOR Row = UpLeftRow + 1 TO UpLeftRow + High - 1 ' Vertical bar character on the left side. LOCATE Row, UpLeftCol PRINT MID$(BoX$, 4, 1); ' Vertical bar character on the right side. LOCATE Row, UpLeftCol + Wide PRINT MIDS(BoxS, 4, 1); NEXT Row ' Draw the bottom row. FOR Col = UpLeftCol TO FarRight LOCATE BottomRow, Col ' Horizontal bar character. PRINT LEFTS(Box$, 1); NEXT Col ' Cap off the bottom row with corner characters. LOCATE BottomRow, UpLeftCol PRINT MIDS(Box$, 5, 1); LOCATE BottomRow, FarRight PRINT MID$(BOX$, 6, 1); END SUB ' DrawBox