Advanced BASIC
Advanced BASIC
Advanced BASIC
Byt.
) (6
Navaies(WrAourlittl
The Peter Norton Computing Group
’
7
&
>
oo
_
: * —
ie
* a
-
‘
:
7 ;
is)
i i
|
Advanced BASIC
The Peter Norton Programming Series
What's Inside
* More than 100 ready-to-run programs that show the best ways to work with
windows, pull-down menus, animation, the mouse, and new fast routines
¢ Expert tips to increase program speed and finesse
¢ A learn-by-doing approach to programming that shows code in action in a
direct, highly readable style
7 | 1A ood a oA
it? yisaip Grratasel irhew odw wrenrranrgon saa Lsomrrh? of ene
. wnemeng tod) of comune heg wae ble ban senor yada
=
: =
we divew 02 ope ses! seit sruile = Letcher ste a rhent OE stat GP nal
eu bees Wark Dio), sei ew AYO Novinins @rrisal elake tai J 7
aud?
aunt? ikte ine wages ttesaenty)
vagr Says H Senereh aGct ox
aye» ; »
sgnondit tatucqedeso
Ri nohcl rte eit toad
~ > : :
asbiines teak
_ Oe RAD gil
a oe afl erespantetonn ois Tete
reat
Nee 7 ron
Bee
Advanced BASIC
Brady Publishing
Brady Publishing
LOPOIS
MeO Die sr2
Norton, Peter.
Advanced BASIC / Peter Norton, Steven Holzner.
p.cm.
Includes index.
1. BASIC (Computer program language) I. Holzner, Steven
Il. Title.
QA76.73.B3H68 1991
005.26 2--dce20 90-24638
CIP
ISBN 0-13-658758-5
Contents
Introduction xiii
BASIC Makes a Comeback xiii
The BASIC Professional Development System Xiv
BASIC Taken Seriously Xiv
What’s in This Book XV
What You'll Need Xvii
Chapter 2. Windows
Designing Our Window System
Initializing Our Window System
Filling the Window Arrays
A Window Initializing Example
Displaying a Window
Displaying a Test Window
Hiding a Window
Hiding Our Test Window
Printing Text in a Window
Printing in Our Test Window
vi > Advanced BASIC
Moving a Window
Moving Our Test Window
Deleting a Window
Deleting Our Test Window
Windows in the BASIC PDS
WINDOWER.BAS—A PDS Window Program
Conclusion
401
Chapter 10. Welcome to Assembly Language
Machine Language 403
Assembly Language 404
The MOV Instruction 405
406
Assembly Language Example
410
Our First Assembly Language Program
413
Memory Segmentation 414
Segments in Memory
416
Segment Registers in Use: .COM Files
417
Assembler Directives 417
The .CODE Directive
Labels 418
418
Positioning Code in the Code Segment
419
The END Directive 420
Assembling PRINTZ.ASM
x P» Advanced BASIC
Index 549
Limits of Liability and Disclaimer of Warranty
The authors and publisher of this book have used their best efforts in preparing this book
and the programs contained in it. These efforts include the development, research, and
testing of the theories and programs to determine their effectiveness. The authors and
publisher make no warranty of any kind, expressed or implied, with regard to these
programs or the documentation contained in this book. The authors and publisher shall
not be liable in any event for incidental or consequential damages in connection with, or
arising out of, the furnishing, performance, or use of these programs.
Trademarks
We stress examples in this book and develop them incrementally so you can see
what’s going on. We'll use arrows to show you where we are in a certain piece of
code. In addition, this book will also present a wealth of special tips that are
designed to give you out-of-the-ordinary hints about BASIC programming. We'll
also include some special notes when there is something of historical or machine-
related interest that we should know about.
Here’s a list of the chapters in this book, and a little information about each one.
You might take a few seconds to skim this overview:
QB PROG /L
or
Q@BX PROG /L
xviii > Advanced BASIC
This switch loads the Quick libraries QB.QLB or QBX.QLB into memory. When
we work with assembly langauge near the end of the book, we’ll even see how to
make our own Quick libraries out of assembly language procedures.
In addition, those programs using INTERRUPT( ) or INTERRUPTX( ) — and
the assembly language modules we'll write — will not work in OS/2 protected
mode (although the other programs in this book will). This is because they use the
interrupt structure of BIOS and DOS. These programs will work, however, under
the OS/2 DOS compatibility mode.
And that’s all there is to it. We’re ready to enter the world of advanced BASIC
programming. Let’s begin by turning to Chapter 1, Professional Input.
Professional Input
fe
vy
%
ee! ’
PEs 7 Fa 8
‘ote Gre4
Ps
rm
i.
5
ay
> Professional Input 3
IN THIS CHAPTER, we're going to explore BASIC’s keyboard input capabilities — but
we won't stop there. We'll augment those functions and add a few of our own by operat-
ing at the lowest level and interfacing directly to DOS.
In addition, we'll see what kind of bulletproof input routines are needed in real
applications by developing a professional style input routine that reads integers from the
keyboard. And, before we’re done, we'll take a look at parsing keyboard input by putting
together our own reverse polish calculator.
This chapter is the natural starting point for our book — working through the ways we
can get some input for our programs. And we'll see just about every way of receiving
keyboard input that there is under BASIC. Let’s start by reviewing what BASIC itself
provides.
In this example, we use the INPUT statement to read three values and place them into the
three single-precision variables—A!, B!, and C!. We specify the prompt we want typed
out in the INPUT statement, and then list the variables to fill with input (see Listing 1-1).
Unfortunately, the INPUT statement should be avoided by professional programmers.
One problem is that it places a mandatory question mark after the prompt string. Assume
our program had looked like Listing 1-2.
R43 INPUT
Please type three numbers?
INPUT
It looks more like a plea than a prompt. The real problem, however, is that
the
produces the “Redo from start” error message if the values typed do not fit into
variable list provided or if the values are not separated by valid delimiters such as
commas. For example, we’d get this response if we left out the delimiters:
R: > INPUT
This program takes the average of three numbers.
The numbers? 3 3 3 < type this.
This doesn’t exactly match the idea of user friendliness, nor does it promote a profes-
sional image.
One of INPUT$ chief advantages is that it does not echo characters on the
screen, providing an alternate method of input from the rest of the BASIC
statements and functions.
First we print out our prompt (“Please type a string of 10 characters.”), and then read
the input string:
Now we can search the string for the first e, if there was one, and print out its location:
See our function InKeyNoEcho$( ), developed a little later on, for a solution to
these problems.
In this case we print out a prompt (“Please type a string:”) — note that no annoying
question mark is added — and then we receive our input string. We can work with that
string as we please, including parsing it (as we do at the end of this chapter).
frp: | If you want to read strings, the LINE INPUT statement provides the easiest
so lution. It is easier than INKEY$() and more reliable than INPUT or INPUTS.
Still, LINE INPUT doesn’t give you character-by-character control; nor does it handle
extended ASCII codes. For those capabilities, we have to turn to INKEY$.
6 b> Advanced BASIC
INKEY$
For real control, INKEY$ is the programmer's favorite.
Everybody’s familiar with INKEY$; here’s an example:
DO
InChar$S = INKEYS.
LOOP WHILE InChar$S = ""
In this case, we're just waiting for a character to be typed. Because INKEY$ returns
whatever's in the keyboard buffer immediately, we have to loop, calling it continuously
until something’s there:
INKEY$ has three types of return values: (a) strings of length zero (null strings, “”), (b)
length one, and (c) length two. If the return string is one character long, it’s just a single
character, like d or q.
Use the LEN() function to determine the length of the string returned by
INKEY$. If it’s one character long, you're all set; if it's two characters long,
INKEY$ is returning an extended ASCII code.
If, however, the return string is two characters long, then it represents an extended
ASCII code. The first character in this string is ASCII 0; that is, CHR$(0). The second
character is the key’s scan code. There is a unique scan code for each key or legal key
combination (such as Alt-k) on the keyboard, and you can look them up in the tables in
your BASIC documentation. (Use RIGHT$( ), LEFT$( ), or MID$( ) to separate out the
first and second characters in the returned string.)
Although many programmers aren’t familiar with scan codes, using them can add a lot
of power to your programs; let’s take a look at how to put them to work. In the following
example, we set up a function that indicates which arrow key was pressed.
> Professional Input 7
bo
LOOP WHILE 1
bo
bo
InStr$ = INKEYS
LOOP WHILE InStr$ = ""
LOOP WHILE 1
8 Pb Advanced
Ree eeBASIC ee —
The outer loop, DO...LOOP WHILE 1, loops forever; the inner loop waits until a key
has been pressed. After INKEY$ returns a key, we get its ASCII code and then check to
make sure the length of the incoming string is two; if not, and if WarningBeep% is
nonzero, we beep:
bO
dO
InStr$ = INKEY$
LOOP WHILE InStr$ = ""
ELSE
> IF WarningBeep% THEN BEEP
END IF
LOOP WHILE 1
Now we can check to see which arrow key, if any, has been pressed. That looks like
Listing 1-3, using a SELECT CASE statement (and getting the scan code values from the
BASIC documentation). That’s it; if an arrow key has been pressed, we return the correct
character, and, if not, we look back to the beginning and wait for another one (after
beeping if we’re supposed to).
DO
DO
InStr$S = INKEYS
LOOP WHILE InStr$ = ""
END FUNCTION
This is one way to use the scan codes you can read from INKEY$. As we can see,
INKEY$ is a versatile function. For most purposes, we can put together a good input
routine using INKEY$.
However, there are some times when INKEY$ might not be quite right. We've seen that
the INPUT$(_) function does not echo on the screen, but that it also can’t return extended
ASCII codes.
If you use scan codes frequently, it’s easiest write a small BASIC program to
print data out. For example, to find the right arrow’s scan code, you would run
your program and type that key; the program would print the scan code it got
from INKEY$.
Fortunately, we can put together our own version of INKEY$ that won't echo on the
screen, but which will handle extended ASCII codes. And, since that process will intro-
duce us to the INTERRUPT( ) routine (which is going to be used very often in the rest of
the book), let’s do that right now.
InKeyNoEcho$
Here we can develop our own variation on INKEY$ that operates just as INKEY$ does,
but does not echo typed characters on the screen.
Our function InKeyNoEcho$( ) should not wait for input, just as INKEY$ does not.
And it should return values exactly as you’d expect them from INKEY$ as a null string,
10 » Advanced BASIC
(“”), which means that no character was waiting; a single character that holds the ASCII
value of the struck key; or a double character, which means that an extended ASCII code
was necessary:
Now, since no such function exists in BASIC, we’re going to have to put it together
ourselves. We can’t use any of the input functions that already exist in BASIC to build our
function because they all echo on the screen or suppress scan codes. That means we'll
have to start from scratch, and one way of doing that is interfacing to DOS itself through
the interrupt system.
16-bit registers
CPU
Figure 1-1
Each interrupt examines the way we’ve loaded some or all of these registers, and takes
action accordingly. For a complete listing of each interrupt service and how to load the
registers, look at the appendix of this book; it shows you what must be in each register
before calling INTERRUPT( ) or INTERRUPTX( ), and what kinds out output you can
expect to find.
Since each register is 16 bits long, it is exactly like a BASIC INTEGER, and we can load
integer values into them like 53, 3251, or -219 (see Figure 1-2).
16-bit registers
ax bx cx dx
ai = ex %
Figure 1-2
To the computer, however, each register can also be thought of as two bytes, so you
can also break up these registers into a high 8-bit register and a low 8-bit register. For
example, the high 8-bit part of ax (bx, cx ...) is called ah (bh, ch ...), and the low 8-bit
part of ax (bx, cx ...) is called al (bl, cl ...) (see Figure 1-3).
12 P Advanced BASIC
16-bit registers
CPU
8-bit registers
Figure 1-3
Frequently, we'll have to load one of these 8-bit registers, such as ah, with a particular
value to use an interrupt. When we work with registers, we'll use hexadecimal values.
(Hexadecimal, of course, is just base 16, where digits go from 0 to 9 and then from @HA
to &HE)
Hexadecimal is handy because a 16-bit binary number — the size of each full register
— makes up four hexadecimal digits. That means that the values we can place in the 80 x
86’s registers go from 0 to G@HFFFF (65535) (Figure 1-4).
16-bit registers
ax bx cx dx
CPU
&H1234 &HA294 &HFEFFF iam
Figure 1-4
In addition, 8 bits (a byte) make up exactly two hex digits — @H12 or @H34 — soa
byte can hold values from 0 to @HFF (255). Because we can divide registers like ax into
ah and al, the top byte in a 16-bit word like ax is simply the first two hex digits, and the
bottom byte is the bottom two. For example, if ax held &H1234, then ah contains &H12
and al contains @H34 (Figure 1-5).
> Professional Input 13
16-bit registers
bx CX dx
CPU
&HA294 &HFFFF Fg)
ah = &H12
al = & H34
Figure 1-5
To reach the 80 x 86’s registers from BASIC, we first have to set up two data structures
InRegs and OutRegs, as type Reglype. That type is defined this way:
?
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
Now we'll be able to refer to registers like ax as InRegs.ax, bx as InRegs.bx, and so on.
Note that we had to set aside space for four new registers here bp, si, di, and the flags
register (see Figure 1-6).
16-bit registers
CPU
Figure 1-6
14 Pb Advanced BASIC
The high and low bytes of these new registers cannot be addressed sepa-
rately; for example, si does not split into sh and sl.
With the exception of the flags register, we won't see these new registers until we start
to deal with assembly language. The bp register is normally used in manipulating stack
data, and we'll use it in Chapter 11. The si and di registers are used by the 80 x 86 to
manipulate strings, and we'll see them in Chapter 10. However, we will see more of the
flags register in this chapter.
There are nine flags common to all 80 x 86 processors (there are 13 flags in the 80 x
86); these flags usually report the status of mathematical operations. For example, there is
a zero flag that is set if the result of an operation was zero. The carry flag is normally set if
the last operation resulted in a carry. The lower bits of InRegs.flags hold the nine common
80 x 86 flags, and here they are, bit by bit:
Now that we’ve defined our TYPE ReglType, we're free to use INTERRUPT( ). For
example, to print a character on the screen, we first check the appendix to find that
interrupt G@H21 service 2 is the one we want. INT &H21, service 2 character output
on
screen, is shown below:
Input
ah=2
dl=Character’s ASCII code.
ee > FOlUSSiONal
Professional Input
Input TD
15
Because interrupt @H21 can do many things, it’s divided up into services;
service 2 is
the one that lets us print on the screen. (See the Appendix for INTERRUPT & H21’s
Services.) To select a service from an interrupt, load that number into the ah register (the
top eight bits of ax).
Since we're restricted to working with integers, we'll have to do that by loading 2 into
the as Bie of ax (recall that the first two digits in a four digit hex number make up the
top byte):
We also see that we have to place the character's ASCII code into the dl register, the
lower 8 bits of dx. Let’s print out an A:
Next, we issue the call to INTERRUPTC( ), which executes the interrupt. We have pass
the number of the interrupt we want to use, @H21, and the variable of type RegType that
holds values we want placed in the registers, which we’ve called InRegs. Interrupt ser-
vices can also return output values, and we'll receive those values in OutRegs:
And that’s it: the character A is printed on the screen at the current cursor location.
Now we've interfaced BASIC to DOS with the INTERRUPTC( ) routine.
Input Output
ah = 6
diz QHFF > Zero flag set if no character was ready. Otherwise,
al holds character’s ASCII code.
dl < @HFF > Type ASCII code in dl on screen.
To use this service, we have to load ah with 6 and dl with @HFF If we load dl with any
value but @HFF this service becomes a printing service; DOS treats the value in dl as an
ASCII code and prints it on the screen. In this case, however, we want to receive input, so
we place @HFF in dl. The call to INTERRUPT( ) looks like this:
This service has a variety of return values. If the zero flag of the 80 x 86 is set on
return, then no key was waiting to be read. The zero flag is one of the nine flags internal
to the 80 x 86; from our previous list, we can see that it’s bit 6 in OutRegs.flags:
That means we can check whether a character was waiting like this:
If no character was waiting (i.e., if the zero flag bit was set), then OutRegs.flags AND
2°6 will be nonzero, and we assign a null string to InKeyNoEcho$, just as you’ve expect to
get from INKEY$.
On the other hand, if a key was waiting, we have to examine its value, which is
returned in the al register (the bottom eight bits of OutRegs.ax). If it’s nonzero, then it’s
the ASCII value of the struck key, and we have to turn it into a one character BASIC string
before returning:
Again, this is just what you’d expect from INKEY$. Finally, if the ASCII code in al is 0
after the call to this service, it indicates that the pressed key has an extended ASCII value.
In this case, we have to make a second call to service 6 to receive the key’s scan code. We
then make up a two-character string from ASCII 0 (i.e., CHR$(0)) and the key’s scan
code like this:
This is also just what you’d expect from INKEY$ if an extended ASCII code was to be
returned. In this way, InKeyNoEcho$( ) mimics what you’d see from INKEY$, except that
nothing is echoed on the screen as the user is typing.
IPS | Use this function, InKeyNoEcho$( ), when in graphics mode or typing a pass-
word. Since it doesn’t echo, nothing will appear on the screen.
eee
FUNCTION InKeyNoEcho$
END FUNCTION
With InkeyNoEcho$( ), we’re getting more advanced. We’ve made use of a DOS ser-
vice, and that’s a step ahead. So far, we've worked through the standard BASIC keyboard
input statements and functions: INPUT, INPUT$, LINE INPUT, and INKEY$, and now
we've even added an input function of our own, InKeyNoEcho§( ).
The next step is to build on these elementary functions. In professional programs,
input routines have to be pretty bulletproof, and we'll see just how that looks when we
develop the code to read integers next.
CursorRow% = CSRLIN
CursorColz% = POS(0)
~ dO
LOOP WHILE 1
At the beginning of the loop, we read an input string and name it InString$. In this
outer loop, we'll loop over these typed-in strings until we get one that we can decode into
an integer:
CursorRow% = CSRLIN
CursorCol% = PO0S(0)
dO
LINE INPUT InString$ —
LOOP WHILE 1
We can make a preliminary check on the size of the number by looking at its length;
if
it's more than six characters, we should start over (the maximum length an integer
can be
is six characters — five digits and a sign). Let’s put in a subroutine named StartOve
r to do
just that:
> Professional Input 21
CursorRow% = CSRLIN
CursorCol% = POS(0)
DO
LINE INPUT InString$
ELSE
> GOSUB StartOver
END IF "IF LENCInString$) < = 6...
LOOP WHILE 1
StartOver:
IF WarningBeep% THEN BEEP
LOCATE CursorRow%, Cursortol%
PRINT SPACESC(LEN(InString$));
LOCATE CursorRow%, Cursorcol%
RETURN
Now we have to loop over every character in the input string InString$. If it’s between
O and 9 we add it to the running total that will result in the final integer value. The + and
- signs are okay only if they're the first character in the string. If not, or if we’ve received
any incorrect characters, we have to blank the string on the screen and reset the cursor
back to the original position. This is how we check each character:
ELSE
END IF
NEXT i
ELSE
GOSUB StartOver
END IF ‘IF LENC(InString$) < = 6...
LOOP WHILE 1
22 }& Advanced BASIC
StartOver:
IF WarningBeep% THEN BEEP
LOCATE CursorRow%, CursorCol%
PRINT SPACESC(LEN(InString$));
LOCATE CursorRow%, CursorCol%
RETURN
If the character is acceptable, we have to add it to the running total, check if we’ve had
an overflow (we should keep the running total in a LONG integer to avoid upsetting
BASIC), and, if so, start over. Let’s set up the running total in a variable named SUM&
like this:
CursorRow% = CSRLIN
CursorCol% = POS{0)
dO
LINE INPUT InStringS
NegFlag% = 0
SUM& = 0
IF LENCInString$) < = 6 THEN
FOR i = 1 TO LENCInString$)
Char$S = MID$CInString$, i, 1)
IF Char$ > = "O" AND Char$S < = “9" THEN
~ SUM& = SUM& + CASC(Char$)-ASC("0"))*10° (LENCInString$)-i)
ae
on
END IF
NEXT i
ELSE
GOSUB StartOver
END IF "IF LENCInString$) < = 6...
LOOP WHILE 1
StartOver:
IF WarningBeep% THEN BEEP
LOCATE CursorRow%, Cursorctol%
PRINT SPACES(LENCInString$));
LOCATE CursorRow%, CursorCol%
RETURN
You can convert letters into digits by subtracting the ASCli code for 0; for
example, ASC(0) - ASC(0) is 0, ASC(1) - ASC(0) is 1, and so on.
}
For each digit, we check SUM&. If it’s over the limit for INTEGERs, we beep (if
required) and start again:
> Professional Input 23
CursorRow% = CSRLIN
Cursorcolz% = Posto)
dO
LINE INPUT InString$
NegFlag% = 0
sum& = 0
IF LENCInString$) < = 6 THEN
FOR i = 1 TO LENCInString$)
CharS = MID$CInString$, i, 1)
IF Char$S > = "O" AND Char$S < = "9" THEN
SUM& = SUM& + CASC(Char$)-ASC("0"))*10°(LENCInString$)-i)
> IF SUM& > 32767 THEN "Overflow?
: GOSUB StartOver
EXIT FOR
END IF
END IF
NEXT i
ELSE
GOSUB StartOver
END IF "EF LENCInString$) < = 6...
LOOP WHILE 1
StartOver:
IF WarningBeep% THEN BEEP
LOCATE CursorRow%, Cursorcol%
PRINT SPACES(LENCInStringS));
LOCATE CursorRow%, CursorCol%
RETURN
we're
Otherwise, we have to check if we’ve reached the end of the input string. If so,
done: we set GetInteger% to SUMG and exit the function:
CursorRow% = CSRLIN
Cursorcol% = POS(O)
bo
LINE INPUT InString$
NegFlag% = 0
SuUM& = 0
IF LENCInString$) < = 6 THEN
FOR i = 1 TO LENCInStringS$)
Char$ = MIDSCInStringS, i, 1)
IF Char$ > = “O" AND Char$ < = "9" THEN
CLENCInString$)-i)
SUM& = SUM& + CASCCChar$)-ASCC"0"))*107
24 } Advanced BASIC
END IF
NEXT i
ELSE
GOSUB StartOver
END IF "IF LENCInString$) < = 6...
LOOP WHILE 1
StartOver:
IF WarningBeep% THEN BEEP
LOCATE CursorRow%, Cursorctol%
PRINT SPACES(LENCInString$));
LOCATE CursorRow%, Cursorcol%
RETURN
You can assign variables of two different types to each other in BASIC, as in
the statement: Getinteger% = SUM&. In this case, the value in SUME& is
truncated (its upper bits are lost) so that it fits into the integer GetInterger%.
So far, we’ve only taken characters that are ASCII digits. Now we have to check if the
current character is a + or -. For any other character, we have to start over. Let’s check for
+ or -. These sign characters are acceptable in integers only if they’re the first character.
We check that this way:
CursorRow% = CSRLIN
CursorColz% = POS(0)
DO
LINE INPUT InString$
NegFlag% = 0
SUM& = 0
IF LENCInString$) < = 6 THEN
FOR i = 1 TO LENCInString$)
Char$ = MID$CInString$, i, 1)
IF Char$ > = "0" AND Char$ < = "9" THEN
SUM& = SUM& + CASC(Char$)-ASC("0"))*10* CLENCInString$)-i)
> Professional
eee” ISIN Input
IY 25
END IF
END IF
NEXT i
ELSE
GOSUB StartOver
END IF “IF LENCInStrings) < = 6..;
LOOP WHILE 1 :
StartOver:
IF WarningBeepX THEN BEEP
LOCATE CursorRow%, Cursorcol%s
PRINT SPACESCLENCInString$));
LOCATE CursorRow%, Cursorcolt%
RETURN
If the sign is -, then we note that by setting the negative flag (NegFlag%) to 1 so we can
return -SUM& later. Note that we also have to make sure that, even if the sign is the first
character, there are more characters to come (i.e. the digits). Otherwise, + or - by itself
would be an acceptable input. We check that this way, making note of a negative sign if
we have to:
NegFlag% = 0
sum& = 0
IF LENCInString$) < = 6 THEN
FOR i = 1 TO LENCInString$)
Char$ = MID$CInString$, i, 1)
IF Char$ > = “O" AND Char$ < = “9" THEN
SUM& = SUM& + CASC(Char$)-ASC("0"))*10°(LENCInString$)-i)
IF SUM& > 32767 THEN "Overflow?
GOSUB StartOver
EXIT FOR
26 » Advanced BASIC
END IF
IF i = LENCInString$) THEN
IF NegFlag% THEN SUM& = -SUME&
GetInteger% = SUM&
EXIT FUNCTION
END IF
ELSE
~ IF (Char$ = "=" OR Char$ = “+") AND i = 1 THEN
: IF LENCInString$) = 1 THEN
‘ GOSUB StartOver
EXIT FOR
END IF
IF Char$ = "-" THEN NegFlag% = 1
ELSE
END IF
END IF
NEXT i
ELSE
GOSUB StartOver
END IF ‘IF LENCInString$)< = 6...
LOOP WHILE 1
StartOver:
IF WarningBeep% THEN BEEP
LOCATE CursorRow%, Cursorcol%
PRINT SPACESCLENCInString$));
LOCATE CursorRow%, CursorcCol%
RETURN
Now we're left with characters that are illegal; they’re not 0 to 9, nor are they valid
signs. In that case, we beep (if required) and start over:
CursorRow% = CSRLIN
CursorCol% = POS(0)
DO
LINE INPUT InString$
NegFlag% = 0
SUM& = 0
IF LENCInString$) < = 6 THEN
FOR i = 1 TO LENCInString$)
CharS = MID$CInString$, i, 1)
IF Char$ > = "QO" AND Char$ < = “9" THEN
SUM& = SUM& + CASC(Char$)-ASC("0"))*10°(LENCInString$)
- i)
IF SUM& > 32767 THEN ‘Overflow?
GOSUB StartOver
EXIT FOR
END IF
————
a > Professional Input 27
OC SBIONAI Input 27
IF i = LENCInString$) THEN
IF NegFlag% THEN SUM& = ~—SUME&
GetInteger% = Sums
EXIT FUNCTION
END IF
ELSE
IF (Char$S = “-" OR Char$ = “+") AND 4 = 1 THEN
IF LENCInString$) = 1 THEN
GOSUB StartOver
EXIT FOR
END IF
IF Char$ = "-" THEN NegFlag% = 1
ELSE
GOSUB StartOver
EXIT FOR
END IF
END IF
NEXT i
ELSE
GOSUB StartOver
EMD If *iF LENCInString$S) < = 6...
LOOP WHILE 1
StartOver:
IF WarningBeep% THEN BEEP
LOCATE CursorRow%, CursorCol%
PRINT SPACESC(LENCInString$));
LOCATE CursorRow%, CursorCol%
RETURN
And we're done. You can see that accepting bulletproof INTEGER input is not neces-
sarily an easy task, but now that task is done for us. Listing 1-5 shows the whole function.
Use this function, Get Integer% ( ), to replace INKEY$ or LINE INPUT when
numerical input is required, as in spreadsheet, calculator, or accounting
programs.
CursorRow% = CSRLIN
CursorCol% = POS(0)
DO
LINE INPUT InString$
NegFlag% = 0
28 P& Advanced BASIC
LOOP WHILE 1
StartOver:
If WarningBeep% THEN BEEP
LOCATE CursorRow%, CursorCol%
PRINT SPACES(LEN(CInString$));
LOCATE CursorRow%, CursorCol%
RETURN
END FUNCTION
Now we've advanced even more. We’ve built a fairly rugged input routine out of the
primitive BASIC input functions. The next step up is to accept and work with
even more
complicated input. For example, we can break our input up into manageable
compo-
nents by parsing it.
After you put together a good parsing routine once, you can use it in all pro-
grams that read input.
Let’s put this together. First, we get the expression to compute from the keyboard, and
we can name it CurrentString$:
We can add some minimal error checking by making sure that CurrentString$ is at
least five characters long, which is the absolute minimum (e.g., 2 3 +):
END IF
Next, we can call the function that actually does the parsing NextTerm$( ). For exam-
ple, if CurrentString$ was equal to 23 + 4 -, NextTerm$(CurrentString$) should return
2, and CurrentString$ would be truncated to 3 + 4 -. If CurrentString$ was 3.1 2.1 - 10 *
30 Pb Advanced BASIC
**
END IF
Now we should get the next term, followed by an operator, and perform the operation.
Then we have to keep going as long as there are still characters in CurrentString$, so let’s
set up a loop that will continue WHILE CurrentString$ < > “”:
dO
NewTerm! = VALCNextTerm$(CurrentString$))
Operator$ = NextTerm$(CurrentString$)
> SELECT CASE Operators
CASE hg HF
At the end of this loop over CurrentString$, we’ve exhausted the input string, and the
result is left in RPStack!. We can print that out like this:
DO
NewTerm! = VAL(NextTerm$(CurrentStringS$))
Operator$ = NextTerm$(CurrentString$)
SELECT CASE Operator$
CASE “+"
RPStack! = RPStack! + NewTerm!
CASE oe
We still have to write the parsing function NextTerm$( ). Its job is to accept a string,
find the first term in that string (as bounded bya trailing space), return that term and
truncate the input string. We should start by finding the length of the leftmost term in the
string so that we can chop it off:
DO
NewTerm! = VALCNextTerm$(CurrentString$))
Operator$S = NextTerm$(CurrentString$)
SELECT CASE Operators
CASE tat
END FUNCTION
Here we've searched ahead for the first space with the BASIC function INSTR( ). For
example, in the string 3.1 2.1 - 10 * 5 +, TermLength% would now be set like this:
> Professional Input 33
TermLength% = 3
sully
iggaelNO el
Now that we know where to chop off the first term, we should also figure out the
length of the remaining string (so we can truncate it):
DBO
NewTerm! = VAL(NextTerm$(CurrentString$))
Operator $ = NextTerm$(CurrentString$)
SELECT CASE Operators
CASE ett
END IF
PRINT “Result:", RPStack!
END FUNCTION
be:
In the string 3.1 2.1 - 10 * 5 +, this is what the new string length would
TermLength% = 3
Si20-
10° 5 +
a
NewStringLen% = 14
34 P Advanced BASIC
However, we should also realize that if there was no space in the string, we’ve reached
its end. In that case, we have to set the length of the first term to the length of the string,
and the new length of the string after truncation to 0:
dO
NewTerm! = VAL(NextTerm$(CurrentString$))
Operator$ = NextTerm$(CurrentString$)
SELECT CASE Operator$
CASE Ag tt
END FUNCTION
Now we're ready to chop off the first term in the input string (using LEFT$( )), and
to
truncate the string itself (using RIGHT$( )) as shown in Listing 1-6.
> Professional Input 35
And that’s all there is to it — we've chopped off the first term and truncated the string.
We pass the term we've isolated back to the calling program, and it loops over them,
multiplying or adding as required until all the terms are used up. At that point, we print
out the result and the calculator is done.
36 PB Advanced BASIC
Conclusion
That’s it for our chapter on keyboard input. We’ve worked through the built-in
resources of BASIC, seen how to add a few of our own, put together a bulletproof input
routine of the kind used in real applications, and even seen some keyboard parsing at
work. Now that we’ve made a survey of input, however, it’s time to start looking at some
output, and we do that with our own windows in Chapter 2.
Windows
37
Lb oid
a Tew ol sn eee ~ ae
7 = Ying rsGA ae ions ln cae :
§ % (aa oa
7 : “7 . ean
l=
> Windows 39
IN THIS CHAPTER, we're going to develop a powerful set of functions and subpro-
grams—ones that will let you add windows to your programs. With a few simple calls,
you'll be able to pop a window up on the screen in the color you specify, print to it, and
hide it again. This can augment your programs tremendously, giving them a professional
feel. Towards the end of the chapter, we'll also see what the BASIC PDS has to offer in
terms of creating our own windows. Let’s get started immediately.
ss
Figure 2-1
That makes the process we'll develop in this chapter simple. For example, to put a
window on the screen, print “Now is the time.” in it, and then make it vanish, these are the
steps we'd take:
1. Get a handle for the window with the function we'll write named WindowGetHan-
dle%( ). This function lets you design and initialize the window.
2. Call another subprogram we'll develop, named WindowShow( ), to display the
window. The only argument it needs is the window’s handle.
40 Pb Advanced BASIC
3. Call another new function WindowPrint%( ), passing it the window’s handle and
the text to print: “Now is the time.”
4. Finally, call WindowHide( ), again passing the window’s handle, to make the win-
dow vanish.
Now that we have a plan, we can start writing code. The natural place to begin is with
the window initialization function, WindowGetHandle%( ).
Also, we'll need window’s size—we can pass the number of rows and columns to use.
Finally, we should be able to give it a color, so let’s pass a screen attribute (there is a
complete discussion of screen attributes and how to use them coming up). In other
words, the way we’d use WindowGetHandle%( ) is like this:
Figure 2-3
Adding the SHARED keyword makes these variables available to all the subprograms
and functions in the current module. Now, A%, B%, and C% can be used anywhere in the
entire program by name. Every time you use them, you access the same memory location,
so if you change A% from 3 to 5, it'll be 5 for every other part of the program too. Using
COMMONs, all the window subprograms can all operate independently of each other,
which means that we'll only need to link in the window functions we want to use, not the
42 P& Advanced BASIC
whole system. This way, we can develop our windows in a number of bite-sized subpro-
grams, not as one huge unwieldy monster.
~ CONST MaxWindows = 8
Later, if we want to increase or decrease the number of windows available, this is the
only number to change (there is no limit). Keep in mind, however, that we'll eat up
memory if we increase it too much.
The next step is to store the data that we’ve been passed. There must be an array for
each window variable; the index of that array ranges over the number of windows. In
fact, the index will be the window’s handle. If we’re asked to display window 3, that
window’s attribute will be in Attribute(3).
An important quantity for each window is the number of rows and columns it has, and
we can store those values like this:
CONST MaxWindows = 8
~ DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
> Windows 43
Next, we can store the screen coordinates of each window like this:
Here, the top left corner of the window is (TopRow, TopCol) and the bottom right
corner is (BotRow, BotCol). Following this, we can store the window’s attribute, like this:
CONST MaxWindows
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM Toprow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
= DIM Attribute(1 TO MaxWindows) AS INTEGER
Now we can store the text in the window itself. Let’s treat every row of the window as
its own string; we can set up an array named Text(Maxwindows, 25) AS STRING to hold
these strings. Assume if window 3 looks like Figure 2-4).
Now is the
time for all
good men to
Figure 2-4
all”; and
Then, Text(3, 1) would equal “Now is the ”; Text(3, 2) would equal “time for
rows for each window.)
so on. Here’s that array. (Note that we have to allow up to 25
44 » Advanced BASIC
nl
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM Toprow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
~ DIM Text(MaxWindows, 25) AS STRING
Note that when we hide our window again, we’ll want to restore the text that was
already there on the screen. This means that we have to store the text that we overwrote
when we displayed our window. We can store that text in an array named OldText( ).
However, there is also a screen attribute for each position on the screen, which deter-
mines the colors of that character, and that means that besides the text we’ll have to store
the old screen attributes. Since the screen can be multicolored (we may even be overlap-
ping other, colored windows), we have to store the screen attribute of each screen posi-
tion we overwrite, as well as the character that was there. Since a screen attribute is a
byte, we can store them in strings too:
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM Toprow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
od DIM OldText(MaxWindows, 25) AS STRING
DIM OLdAttrb(MaxWindows, 25) AS STRING
.
.
And that’s almost it. We've stored the window’s complete specification, including its
size and color. We've set up room for the text in the window, and back-up space for the
text that will be overwritten, as well as the screen attributes that will,be overwritten.
The final item we need is an internal variable that indicates whether the window is
actually visible or not (if we didn’t include this value, all windows would be on all the
time). Let’s add a flag indicating whether a window is visible, calling it OnFlag( ). If
eee
ee EE > Windows
eee 45
OnFlag(5) equals 1, for example, window 5 is currently visible. If it’s 0, the window is
hidden. Here’s how we can add that:
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM Toprow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OldAttrb(MaxWindows, 25) AS STRING
> DIM OnFlag(1 TO MaxWindows) AS INTEGER
And that’s it. That’s all the storage we'll need for our window system. All we have to do
now is to set up the commons to communicate these arrays to other subprograms and
then fill the arrays with the values passed to us.
The complete function is shown in Listing 2-1.
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM Toprow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM OnFlag(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OldAttrb(MaxWindows, 25) AS STRING
> COMMON SHARED /WindowA/ Rows() AS INTEGER, Cols() AS INTEGER, _
Toprow() AS INTEGER, TopCol() AS INTEGER, BotRow() AS INTEGER, an
BotCol() AS INTEGER
COMMON SHARED /WindowB/ Attribute() AS INTEGER, OnFlag() AS INTEGER, _
Text() AS STRING, OldText() AS STRING, OldAttrb() AS STRING
46 P Advanced BASIC
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM Toprow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM OnFlag(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OldAttrb(MaxWindows, 25) AS STRING
COMMON SHARED /WindowA/ Rows() AS INTEGER, Cols() AS INTEGER, oe
Toprow( ) AS INTEGER, TopCol() AS INTEGER, BotRow() AS INTEGER, _
BotCol€) AS INTEGER
COMMON SHARED /WindowB/ Attribute() AS INTEGER, OnFlag() AS INTEGER, was
Text() AS STRING, OldText() AS STRING, OldAttrb() AS STRING
FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCol%, Attr%)
WindowGetHandle% = 0
3 FOR i = 1 TO MaxWindows
IF Rows(i) = 0 THEN
WindowGetHandle% = ji
Toprow(i) TopR%
TopCol (i) Topc%
BotRow(i) TopR% + NRow%
foun
END FUNCTION
Note again that the window’s handle is its index in the window arr ays. When Win-
dowGetHandle% () is called, we have to find the first available array index to give
the
new window and return as its handle. We can do that by checking Rows(i),
the first of
our window arrays:
i > Windows
Ndows 47
FOR i = 1 TO MaxWindows
> IF Rows(€i) = 0 THEN
WindowGetHandle% = i
ToprowCi) = TopR%
TopColCi) = Topct%
BotRow(i) = TopR% + NRow%
BotCol(i)d) = TopC% + NCol%
Rows(i) = NRow%
Cols(i) = NCoLl%
Attribute(i) = Attr%
OnFlag(i) = 0
FOR j = 1 TO Rows(i)
Text(i, j) = SPACE$(Cols(i))
NEXT j : :
EXIT FOR
END IF
NEXT i
If we find that, say, Rows(4) equals 0, there is no window 4 (you can’t have zero rows),
so we'll return that value as the window’s handle. Then we fill the arrays with the various
values (Rows(4), Cols(4), and so on).
Now we've written WindowGetHandle%(_), and stored all the necessary window data.
We've taken the first step towards developing our window system. Let’s see this function
in action.
i 6 5 e 3 2 1 0
Green Red
0 0 1 0 0 1 0 0= 00100100B
Figure 2-5
We can mix the colors too by adding the respective bit values together. Here are the bit
values to add in forming the attribute byte:
Bit Value Color Generated
1 Blue Foreground
?) Green Foreground
4 Red Foreground
8 High Intensity
16 Blue Background
BP Green Background
64 Red Background
128 Blinking
For example, to get a normal setting of white on black, we would tum on
all the
foreground colors (the letter itself) this way: 1 + 2 + 4 = 7. All the background
colors are
off, so they are set to 0. This value of 7 is the normal startup screen attribute value.
If we
wanted to make that high intensity, we would add 8 to give 15 = GHE
If we wanted
> Windows 49
blinking reverse video in white, we would set all the background colors on, and add 128
to make it blink: 16 + 32 + 64 + 128 = 240 = Q@HFO.
On monochrome screens we can’t use the individual colors, but we can use the normal
screen (7), high intensity (@HF), blinking normal (@H87), blinking reverse video
(@HFO), and underlined, which graphics monitors don’t have. To turn on underlining,
use a blue foreground (making an attribute of 1). We can also have intense underlining
(attribute of 9).
BIOS interrupt & HIO, service 9 lets you select screen attributes for each
character as you print them, giving you more control than the BASIC COLOR/
PRINT combination, where you have to select the color of the entire string
you're printing. All at once.
After we pass all this information, WindowGetHandle%( ) returns the window’s han-
dle, HANDLE% (an INTEGER between 1 and 8). To display this window on the screen,
we can then call WindowShow(Handle%). To hide it, we'll call WindowHide(Handle%).
In other words, all we've got to pass to subprograms from now on is the window’s handle.
We don’t have to give the dimensions, the location, or its screen attribute.
In the following example, we set up two windows, one whose top left corner is at
(1,1), with five rows and columns, and screen attribute of @H24; and one whose top left
corner is at (10,10), also with five rows and columns, whose screen attribute is @H61:
Window I’s handle is 1, and window 2’s handle is 2, and that’s it. We’ve been able to
emulate the initialization process in the professional windows packages. Let’s get our
window on the screen.
Displaying a Window
Now that we've set up a window with WindowGetHandle%( ), we can either work on
50 P& Advanced BASIC
it behind the scenes or display it directly. Let’s call the subprogram that displays it
WindowShow( ). To display the window (at the location and with the size you specified
to WindowGetHandle%( )), we should just be able to call WindowShow(Handle%),
where Handle% is the window’s handle.
Let’s develop this subprogram. First, we have to save the text that we’re going to
overwrite on the screen, and we've already set aside space for this data in the backup
array OldText( ). We also have to save the current screen attributes of every position on
the screen that the window will overwrite in the backup array OldAttrb( ). We can do
both of these things by looping over the rows and columns that we’ll overwrite on the
screen and using the BASIC SCREEN function (which can read both text and attributes)
to read what’s there. Here’s how we start:
FOR 1 = 1 TO Rows(Handle%)
FOR j = 1 TO Cols(Handlex)
NEXT j
NEXT i
We can save the text and attribute data in temporary strings, temp1$ and temp2$,
respectively:
Then, for each row, we store the completed temporary strings in the backup arrays
OldText( ) and OldAttrb( ):
TR = TopRow(Handle%)
TC = TopCol(Handle%)
FOR i = 1 TO Rows(Handle%)
temp1$ = oe
temp2$ = ""
FOR j = 1 TO Colst€Handle%)
temp1$ = temp1$ + CHRSCSCREENC(TR + 4 = 1, 1C # 3 = 1))
temp2$S = temp2$ + CHRSCSCREENCTR + i - 4, 1C-* j = 1,1)
NEXT j ‘
+ OldText(Handle%, i) = temp1$
OldAttrb(Handle%, i) = temp2s
NEXT i :
.
When we restore the screen, we'll read the old characters and attributes out of these
arrays. Now we have to place our window on the screen; we can use interrupt @H10
service 9, which lets us print both characters and their attributes at the present cursor
position. We find this entry for interrupt @H1O0 service 9 in the appendix:
Note: We can’t use the BASIC PRINT statement, or PRINT USING, since we
can’t print attributes that way.
Input Output
ah=9 Character written on screen at Cursor Position.
al=IBM ASCII code
bh=Page Number
bl — Alpha Modes=Attribute
Graphics Modes=Color
cx=Count of characters to write
To use this service, we have to load ah with 9, al with the character’s ASCII code
(which we get from Text( )), and bh with the screen page number. We could get fancy
and divide the screen output into several pages, but we're always going to use the default
(displayed) page, page 0. We also have to load bl with the character’s attribute (which we
52 P Advanced BASIC
get from Attribute( )), and cx with the number of characters to print (1). To move the
cursor around the screen, we can use LOCATE.
First, we establish our loop over the rows and columns of the window:
TR = TopRow(Handle%)
TC = TopCol(HandleZ)
LOCATE TR, TC
~ FOR i = 1 TO Rows(HandleZ)
2 FOR j = 1 TO Cols(Handte%)
NEXT j
NEXT ji
Next, we move the cursor over the correct locations on the screen (note that we start
off by saving the current cursor row and column in CurRow% and CurCol% — this is so
we can restore the cursor when we’re done):
TR = TopRow(Handle%)
TC = TopCol(Handle%)
NEXT j
OldText(Handle%, i) = temp1$
OldAttrb(Handlex, i) = temp2$
NEXT i
FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols (Handle)
CurRow% = CSRLIN
CurCol% = POS(O)
TR = TopRow(Handle%)
TC = TopCol(Handlez)
FOR i = 1 TO Rows(Handle%)
tempi$ = ""
temp2$ = ""
FOR j = 1 TO Cols(Handtez)
= temp1$ + CHRSCSCREENCTR + 4 = 1, TC # fo - 12)
temp1$
= temp2S + CHRSC(SCREENCTR + 4 — 4, TC + 3. -— 1) 12)
temp2$
NEXT j
OldText(Handle%, i) = temp1$
OldAttrb(Handle%, i) = temp2$
NEXT i
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols(Handle%)
i), ij, 1))
InRegs.ax = &H900 + ASCCMIDS(Text(Handle%,
InRegs.bx = Attribute (Handle%)
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + 1 - 1, TC + j
NEXT j
LOCATE TR + i, TC
NEXT i
54 P& Advanced BASIC
The final step is to indicate that the window is on to other subprograms by setting
OnFlag(Handle%) to 1 and then restoring the cursor from CurRow% and CurCol%.
That’s it. The window is now visible. Listing 2-2 shows the whole subprogram
WindowShow( ).
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM TopRow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM OnFlag(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OldAttrb(MaxWindows, 25) AS STRING
COMMON SHARED /WindowA/ Rows() AS INTEGER, Cols() AS INTEGER, oe
Toprow() AS INTEGER, TopCot() AS INTEGER, BotRow() AS INTEGER, =
BotCol() AS INTEGER :
COMMON SHARED /WindowB/ Attribute() AS INTEGER, OnFlag() AS INTEGER,
Text) AS STRING, OldText() as STRING, OldAttrb() AS STRING
é
SUB WindowShow (CHandle%)
> Windows 55
TR = TopRow(Handle%)
TC = TopCol(Handlex)
FOR i = 1 TO Rows(Handles)
temp1$ = ""
temp2$ = ""
FOR j = 1 TO Cols(Handlez%)
temp1$ = tempi$ + CHRSCSCREENCTR + 4 - 1, TC + j - 12)
temp2$ = temp2$S + CHRSCSCREENCTR + i - 1, TC + j - 45 1D)
NEXT j
OldText(Handle%, i) = tempi$
OldAttrb(Handle%, i) = temp2$
NEXT i
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handlted%)
FOR j = 1 TO Cols(Handle%)
InRegs.ax = &H900 + ASCCMIDS(Text(Handle%, i), j, 4))
InRegs.bx = Attribute(Handlez)
CALL INTERRUPT(&H1G, InRegs, OutRegs)
LOCATE TR # 1 = 1, 1C + j
NEXT j
LOCATE TR + i, TC
NEXT i
OnFlag(Handle%) = 1
END SUB
This is a pretty long one, although it’s very easy to use. To display a particular window,
ely
you just need to call it with that window's handle. Let’s put all this to work immediat
with an example.
That's all there is to it. Both windows have now appeared on the screen; the upper one
is green, and the lower one brown (See Figure 2-6 below). So far, our code is a success —
we've produced windows. Now let’s see about hiding them.
Figure 2-6
Hiding a Window
WindowHide( ) does the reverse of WindowShow( ): it simply removes a window
from the screen and restores whatever was undemeath it. This does not stop you from
working on the window. You can still move it around, print to it or whatever you like —
the results just won't be visible. To hide a window, just call WindowHide( ) with that
window’s handle: CALL WindowHide(Handle%),.
Let’s develop this subprogram. We start by saving the present cursor position (because
we'll be using the cursor ourselves when we restore the old text):
~ CurRow% = CSRLIN
Cur€ol% = POS(O)
END SUB
OWS > Windows 57
OF
CurRow% = CSRLIN
CurColz% = POS(O0)
LOCATE TR, TC
InRegs.cx = 1
— FOR i = 1 TO Rows(Handle%)
.
.
NEXT i
And then there’s an inner loop over the columns. At each location, we restore the old
text and attribute. We've seen how to do this in WindowPrint( ). The only difference here
is that we use the backup text array OldText( ) instead of Text( ), and the array OldAt-
trb( ) to restore the attribute of each character position instead of Attribute( ):
CurRow% = CSRLIN
CurCotZ% = POS(0)
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
~ FOR j = 1 TO Cols(Handle%)
InRegs.ax = &H900 + ASCCMIDS(OLdText(Handle%, i), j, 1)
InRegs.bx = ASCCMIDSCOLdAttrb(Handle%, i), j, 1))
CALL INTERRUPTC(&H10, InRegs, OutRegs)
LOCATE TR + i ~ 1, TC + j
NEXT j
LOCATE TR + i, TC
NEXT i
When we're finished with this double loop, the window has vanished from the screen.
To finish up, we signal that this window is off by setting OnFlag(Handle%) to 0 and
restore the old cursor position with LOCATE CurRow%, CurCol%. At this point, the
window is gone, and we’re done. Listing 2-3 shows the complete subprogram.
58 Pb Advanced BASIC
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM TopRow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MAXWINDOWS) AS INTEGER
DIM OnFlag(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OldAttrb(MaxWindows, 25) AS STRING
COMMON SHARED /WindowA/ Rows() AS INTEGER, Cols) AS INTEGER, oo
Toprow() AS INTEGER, TopCol() AS INTEGER, BotRow() AS INTEGER, _
BotCol() AS INTEGER
COMMON SHARED /WindowB/ Attribute() AS INTEGER, OnFlag() AS INTEGER, he
Text() AS STRING, OldText() AS STRING, OldAttrb() AS STRING
SUB WindowHide CHandlexz)
CurRow% CSRLIN
CurCol% Hoa POS(O)
LOCATE TR, TC
InRegs.cx = 1
END SUB
> Windows 59
Again the listing is pretty long, but not as long as it was for WindowShow( ). That’s
because WindowHide( ) only needs to restore the old text and screen attributes; it doesn’t
have to store the window text before overwriting it — that text is already in the window
COMMONS. Now let’s see WindowHide( ) at work!
In this example, we'll just take the two windows we've already created in the Win-
dowShow( ) example program and make one disappear. First, we'll flash window 1 on
the screen. Next, we'll overwrite that window with a new window, window 2, covering
the message entirely. Then we hide window 2, revealing window 1 again:
CALL WindowShow(Handle1%)
CALL WindowShow(Handle2%)
CALL WindowHide(Handle2%) <—
This use of WindowShow( ) and WindowHide( ) show the versatility of our subpro-
grams. To display a window, just use WindowShow( ); to make it vanish, Win-
dowHide( ). You can see how easy they are to use from a programming standpoint. Once
we've worked through all the details, using them is simple.
At this point, we've initialized windows, made them appear and now dissappear; we've
done a lot. On the other hand, what good is a window without text in it? Let's do a little
printing.
60 » Advanced BASIC
|NOTE: | Note: We are both using WindowPrint%( ) and checking its return value in the
same line; this technique is reminiscent of programming in C.
We can pass the string to print as a normal string. For example, we could print “Now is
the time.” like this:
> PrntRow% = 1
PrntCol% = 1
PrntStr$ = “Now is the time."
IF WindowPrint% (Handlezx, PrntRow%, PrntCol%, PrntStr$) THEN
coy PRINT “String printed in window successfully.”
Figure 2-7
Note that if the string is too long for the current row, we'll have to wrap it around to
the next row. On the other hand, what if we wanted to skip to the next row intentionally?
We should make some provision for including carriage returns in the string to be printed.
Let’s check that string for CHR$(13) characters (a carriage return); if we find one, Win-
dowPrint%(_) should move to the first position on the next line. For example, to print we
should be able to pass this string to WindowPrint%(_): “Now is the’=CHR$(13)=“time.”
Now is the
time.
Figure 2-8
Also note that, because we might want to work on a window behind the scenes, just
printing to a window shouldn't display it. On the other hand, if the window is already
displayed on the screen, WindowPrint( ) should update it.
Let’s write this function. We start off by making non-destructive copies of the param-
62 }& Advanced BASIC
eters passed to us: PrntRow%, PrmtCol%, and PrntStr$. We also save the present cursor
position (because WindowPrint%( ) might have to update the screen):
CurRow% = CSRLIN
CurCol% = POS(O)
WindowPrint% = 1
~ PrintRow% = PrnatRows
PrintCol% = ProtCol%
PrintString$ = Prntstrs
This loop takes a little thought. Our idea here is to keep placing characters from the
print string into the window as long as we do not go past the last row of the window (i.e.,
PrintRow% <= Rows(Handle%)), and as long as there are still characters left to print
(PrintString$ <>“”).
CurRows% = CSRLIN
CurCol% = POS{O0)
WindowPrint% = 1
PrintRow% = PrntRow%
PrintCol% = PrntCol%
PrintString$ = PrntStr$
as bO
Then we have to add an inner loop that loops over the columns of the window. We can
keep printing over columns in the current row as long as we're not past the edge of the
eee ee > Windows 63
OS
window (i.e., PrintCol% <= Cols(Handle%)) and as long as there are still characters left to
print (PrintString$ <> “”),
CurRow% = CSRLIN
CurCol% = POS(O)
WindowPrint% = 1
PrintRow% = PrntRow%
PrintCol% = PrntCol%
PrintString$S = PrntStr$
dO
> dO
PrintCol% = Printcol% + 1
LOOP WHILE (PrintCol% Cols(Handle%)) AND CPrintString$ <> "")
PrintRow% PrintRow% + 1
PrintCol% “ou 4
Notice that we increment the column at the end of every interation over columns, and
increase the row (and set the column back to 1) after every iteration over rows. With
these two loops, we'll loop over the columns of the present row in the window (see Figure
2-9).
Now is >
Figure 2-9
And, when we come to the end of the row, we'll skip to the next one (Figure 2-10).
Figure 2-10
64 Pb Advanced BASIC
At this point, we’re ready for the character to print. Here’s how we strip that character
from PrintString$ and place it at (PrintRow%, PrintCol%) in the window (keep in mind
that the window’s text is stored in the array named Text( )):
CurRows CSRLIN
CurCol% POS (0)
WindowPrint% = 1
PrintRow% = PrntRowsr
PrintCol% = PrntCol%
PrintStringS = PrntStrs
dO
> DO
Char$ = LEFTS$C(PrintString$, 1)
PrintString$ = RIGHTS(PrintString$, LEN(PrintString$) - 1)
IF ASC(Char$) = 13 THEN EXIT DO ;
MIDS(Text(Handle%, PrintRow%), PrintCol%, 1) = Char$
PrintCol% = Printcol% + 1
LOOP WHILE (PrintCol% <=Cols(Handle%)) AND (PrintString$<>"")
PrintRow% = PrintRow% + 1
PrintCol% = 1
LOOP WHILE (PrintRow% Rows(Handlez%)) AND (CPrintString$<>"")
*
*
2
.
In this loop, we actually insert the character into the window. Our destructive copy of
the string to be printed is named PrintString$; we find the character to be printed at the
current location, LEFT$(PrintString$, 1), strip it off PrintString$, and insert it into the
correct row in Text(Handle%).
An easy way to strip off the first word of a string (not just its first character) is to
use the INSTR( ) function like this:
FirstWord$ = LEFT$(MyString$,INSTR(MyString$, “”)-1)
We keep looping over columns until we reach the edge of the window or run out of
characters to print. Note that we also have to check for carriage returns in the print string,
in which case we should skip to the next row. After each row is done, we keep looping
over the following rows until there’s nothing left, or until we’re past the end of the
window.
& Windows’ 65
After these loops, we've either exhausted the print string or reached the end of the
window—and we must check which. If we’ve reached the end of the window, we have to
return a value of 0, indicating that we couldn’ fit all the text into the window:
CurRow% = CSRLIN
CurCol% = POS(0)
WindowPrint% = 1
PrintRow% = PrntRow%
PrintCol% = PrntcCol%
PrintStringS = Prnvtstrs
bO
dO
Char$S = LEFTS(PrintString$, 1)
PrintString$ = RIGHT$(PrintString$, LEN(CPrintString$S) - 1)
IF ASC(Char$) = 13 THEN EXIT DO
MIDS(Text(Handle%, PrintRow%), PrintCol%, 1) = Char$
PrintCol% = PrintCol% + 1
LOOP WHILE (PrintCol% Cols(Handte%)) AND (PrintString$<>"")
PrintRow% = PrintRows + 1
PrintCol% = 1
Otherwise, we'll leave WindowPrint% set to 1, indicating success. The last thing to do
is check if the window is currently visible (i.e., if OnFlag(Handle%) equals 1), in which
case we have to update it on the screen. We can just borrow the code to draw the window
on the screen from WindowShow( ). Listing 2-4 shows the whole function.
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
66 P& Advanced BASIC
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM TopRow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM OnFlag(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OLdAttrb(MaxWindows, 25) AS STRING
COMMON SHARED /WindowA/ Rows() AS INTEGER, Cols(€) AS INTEGER, nts
Toprow() AS INTEGER, TopCot() AS INTEGER, BotRow() AS INTEGER, _
BotCol() AS INTEGER
COMMON SHARED /WindowB/ Attribute() AS INTEGER, Onflag() AS INTEGER, S)
Text() AS STRING, OldText() AS STRING, OLdAttrb() AS STRING
CurRow% CSRLIN
CurCol% Hof Post)
WindowPrint% = 1
PrintRow% = PrntRow%
PrintCol% = PrntcCol%
PrintString$ = PrntStrs
dO
dO
Char$S = LEFT$C(PrintString$, 1)
PrintString$ = RIGHTS(PrintString$, LENCPrintString$) - 1)
IF ASC(Char$) = 13 THEN EXIT DO
MIDS(Text(Handle%, PrintRow%), PrintCol%, 1) = Char$
PrintCol% = PrintCol% + 1
LOOP WHILE (PrintCol% <= Cols(Handle%)) AND (PrintString$ <> "")
PrintRow% = PrintRow% + 1
PrintCol% = 1
IF OnFlag(Handle)%
THEN
arn > Windows 67
TR = TopRow(Handle%)
TC = TopCol(Handlex)
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols(Handle%)
TnRegs.ax = &H900 + ASCC(MIDS(Text(Handle%, i), Je 12)
InRegs.bx = Attribute(Handle%)
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + 7 = 91, Tc + 3
NEXT j
COCATE TR + 4, TC
NEXT i
END IF
END FUNCTION
This is another fairly long listing only because we have to update the window if it’s on
the screen — the real meat of printing to the window is done in a dozen or so lines. Now,
let’s get the chance to see WindowPrint%(_) at work.
You can also use WindowPrint% ( ) to print borders in your window if you use
the ASCII line drawing characters.
CALL WindowShow(HandleZ)
68 Pb Advanced BASIC
The result of this program is a mini-window that looks like Figure 2-11.
Hello
lest:
Figure 2-11
Now we're able to set up a window, show it, print in it, and hide it. We’ve come pretty
far. Another option we haven't explored yet, however, is moving windows around the
screen. For example, we may want to move a window from the center of the screen to
somewhere off on the periphery. Let’s give this a try with a function we can call
WindowMove%( ).
Moving a Window
Although we can set a window’s initial screen position with WindowGetHandle%( ),
we may not want to keep it there forever. Instead, we can set up a function named
WindowMove%( ) to move windows around at will. Because moving a window may not
be successful — it may end up past the edge of the screen, for example — this function
should return 1 for success and 0 for failure.
And, like WindowPrint%( ), WindowMove%( ) should not make a window visible.
However, if a window is already on the screen, we should update the screen by making
the window vanish from its old position and reappear at the new one. In other words, if
» Windows 69
the window was already on the screen, and we want to move it, we'll have to go through
these steps:
1. Restore area of screen covered by current window;
2. Save target area of screen; and
3. Pop window into target area of screen.
These three steps are going to make WindowMove%( ) lengthy, but keep in mind that
we've already written the code for these tasks in WindowShow( ) and WindowHide( ).
Recycling your own code makes programming much faster. As you program,
keep in mind all other places where you could use the same code. This also
makes debugging easier.
To use WindowMove%( ), we'll have to supply the handle of the window to move and
its new position (i.e., the new position of the top left corner). That might look like this if
we wanted to move the window to the upper left of the screen:
— NewTopRow% = 1
NewTopCot% = 1 :
IF WindowMove% (Handle%, NewTopRow%, NewTopCol%) THEN
PRINT “Window moved successfully.”
ELSE
PRINT “Error. Could not move window.”
END IF ;
Let’s develop the program. We start off by doing some boundary checking — would
the new location of the window put it off the screen? Here’s the code:
70 & Advanced BASIC
WindowMove% = 1
IF NewTopRow% < 1 ORNewTopCol% < THEN
WindowMovez% = 0
EXIT FUNCTION
END IF
If we’re still in the function, the move is legal. To move the window, all we really have
to do is to refill the location arrays in the window commons for this window, Top-
Row(Handle%), TopCol(Handle%), BotRow(Handle%), BotCol(Handle%), where the
window is set up like Figure 2-12.
(TopRow(Handle%), TopCol(Handle%))
WindowMove% = 1
— TopRow€Handle%) = NewTopRow%
TopCot(Handle%) = NewTopCot%
BotRow(Handle%) = NewTopRow% + Rows (Handle%) :
BotCol(Handle%) = NewTopCol% + Cols(Handle%) : ;
IF OnFlag(Handte%) = 0 THEN EXIT FUNCTION
ee
»> Windows 71
If the window is not displayed, then we’re done, and we exit the function. Otherwise,
we've got a lot of work ahead of us. The bulk of the code in this program will be to move
the window on the screen if it’s presently visible (i.e., OnFlag(Handle%) = 1).
In that case, we have to hide the window about to be moved by restoring the old text
that was there before the window was displayed. We can adapt the code in Win-
dowHide( ) to do that for us. All we have to do is to note the original position of the
window before we update its coordinates, and restore the text there like this:
CurRow% = CSRLIN
CurCol% = POS(0)
WindowMove% = 1
END SUB
72 > Advanced BASIC
Then we have to move the window. We start off by storing what’s in the target area of
the screen now, using code from WindowShow( ):
CurRow% = CSRLIN
CurCol% = POSs(0)
WindowMovez = 1
OldTopRow = TopRow(Handle%)
OldTopCol =TopCol (Handle%)
TopRow(Handlez) = NewTopRow%
TopCol(CHandle%) = NewTopCot%
BotRow(Handlex) = NewTopRow% + Rows (Handle%)
BotColCHandle%) = NewTopCol% + Cols (HandleX)
TR = OlLdTopRow
TC = OldTapCot
LOCATE Tr, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols (Handlex)
InRegs.ax = &H90O + ASCCMIDSCOLdText(Handie%, i), j, 1))
InRegs.bx = ASCCMIDSCOLdAttrb(Handle%, 4), j, 1))
CALL INTERRUPTC&H10, InRegs, OutRegs)
LOCATE TR + 4 -~ 1, TC + j
NEXT j
LOCATE TR + i, TC
NEXT i
REM Save the target section of screen
md
ne TR = TopRow(Handle%)
TC = TopCol(Handle%)
FOR 7 = 1 TO Rows(Handle%)
temp1$ = *"
;
temp2$ = ""
FOR } = 1 TO Cots(Handie%)
temp1$ = temp1$ + CHRS(SCREENCTR + 4 - 1, TC + j = 1))
temp2$ = temp2$ + CHRS(SCREENCTR + i - Vy: TC J = 4, 13)
> Windows 73
NEXT j
OldText(Handle%, i) = temp1$
OldAttrb(Handle%, i) = temp2$s
NEXT i
Now we can display the window on the screen, again using code from Win-
dowShow( ). Listing 2-5 shows the whole function:
TYPE RegTtype
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM TopRow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM OnFlag(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OldAttrb(MaxWindows, 25) AS STRING
COMMON SHARED /WindowA/ Rows() AS INTEGER, Cols() AS INTEGER, _
CurRow% = CSRLIN
Curcol% = POSs(0)
WindowMove% = 1
IF NewTopRow% < 1 OR NewTopCol% < 1 THEN
WindowMove% = 0
EXIT FUNCTION
74 & Advanced BASIC
OldTopRow = TopRow(Handle%)
OldTopCol =TopCol(Handle%)
TopRow(Handte%) = NewTopRow%
TopCol(Handle%) = NewTopCol%
BotRow(Handlez) NewTopRow% + Rows (Handle%)
BotCot (Handle) NewTopCol% + Cols(€Handle%)
TR = OlLdTopRow
TC = OldTopCol
LOCATE TR, TC
InRegs.cx =1
FOR i = 1 TO Rows(HandleZ)
FOR j = 1 TO Cols(Handle%)
InRegs.ax = &H900 + ASCC(MIDS(OldText(Handle%, i), j, 1))
InRegs.bx = ASCC(MIDSCOLdAttrb(Handle%, i), j, 1))
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + 1 - 4, TC + j
NEXT j
LOCATE FR + 47, TC
NEXT i
REM Save the target section of screen
TR = TopRow(HandteZ)
TC = TopCol(Handle%)
FOR i = 1 TO Rows(Handle%)
tempi$ = “"
temp2$ = ue
FOR j = 1 TO Cols(Handle%)
temp1$ = temp1$ + CHRS(SCREENCTR + i - 1, TC #/j.= 1))
temp2s Hi temp2S + CHRS(SCREEN(TR + i - 1, Tc + $221, 72)
NEXT j
OldText(Handle%, i) = temp1$
OtdAttrb(Handle%, 4) = temp2$
NEXT i
> REM Now print the new text
ae
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols (Handle%)
InRegs.ax = &H900 + ASCCMIDS (Text (Handle%, 423,212)
InRegs.bx = Attribute (Handie%)
ee > Windows
OWS 75
LS
And that’s it; we’ve been able to move our window(s) around the screen. Let’s see this
in action.
CALL WindowShow(Handle1%)
CALL WindowShow(Handle2Z)
In this example, we put two windows on the screen, then wait for a key to be pressed.
When we read a key, we move window 2 from screen coordinates (20, 20) to (5, 20) (see
Figure 2-13). Note that these coordinated are the screen coordinates of top left corner of
the window we're moving.
76 »& Advanced BASIC
bani Window 2
oe cae,
Figure 2-13
Let’s take stock of our window system. At this point, we’ve developed a set of subpro-
grams and functions that will let you add windows to your programs, setting them up,
printing in them, and moving them around the screen freely. But how about getting rid of
them? There should be some way of deleting a window and freeing its handle for later
reassignment.
Deleting a Window
Let’s develop a a final window subprogram to delete a window and free its handle. All
we'll need is the window’s handle — to delete a window, we can just call, say, Win-
dowDelete(Handle%). This is especially valuable if you’re near the maximum number of
windows you can fit into memory, and you know that another part of your program is
going to allocate a few windows.
Deleting a window is more than just hiding it. The window’s handle is
deassigned, and you can’t display the window again.
Let’s develop this subprogram. We can begin by hiding the winds if it's being
displayed, with code we’ve seen in WindowHide( ):
» Windows 77
CurRow% = CSRLIN
CurCol% = POS(0)
Come
- TR = TopRow(Handle%)
TC = TopCol(Handlez)
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols(Handlez)
InRegs.ax = &H900 + ASC(MIDSC(OLdText(Handle%, 1), j, 1))
InRegs.bx = ASCCMIDSCOLdAttrb(Handle%, i), j, 12)
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + i ~ 1, TC + j
NEXT j :
LOCATE TR + i, TC
NEXT i
END IF
oe
ne
END SUB
Now we're ready for the real business of WindowDelete( ), which is to convince
WindowGetHandle%( ) that this handle is free. The function WindowGethandle%( )
checks for free entries in the window arrays by looking at the array Rows( ), like this
(from WindowGetHandle%( )):
FOR i = 1 TO MaxWindows
> IF Rows(i) = O THEN
WindowGetHandle% = i
Toprow(i) = TopR%
TopCol(i) = Topct%
BotRow(i) = TopR% + NRow%
BotCol(i) = TopC% + NCol%
Rows(i) = NRow%
Cols(€i) = NCol%
AttributeC(i) = Attr%
OnFlag(i) = 0
FOR j = 1 TO Rows(i)
Text(i, j) = SPACES(Cols(i))
NEXT j
EXIT FOR
END IF
NEXT i
78 > Advanced BASIC
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
CONST MaxWindows = 8
DIM Rows(1 TO MaxWindows) AS INTEGER
DIM Cols(1 TO MaxWindows) AS INTEGER
DIM TopRow(1 TO MaxWindows) AS INTEGER
DIM TopCol(1 TO MaxWindows) AS INTEGER
DIM BotRow(1 TO MaxWindows) AS INTEGER
DIM BotCol(1 TO MaxWindows) AS INTEGER
DIM Attribute(1 TO MaxWindows) AS INTEGER
DIM OnFlag(1 TO MaxWindows) AS INTEGER
DIM Text(MaxWindows, 25) AS STRING
DIM OldText(MaxWindows, 25) AS STRING
DIM OldAttrb(MaxWindows, 25) AS STRING
COMMON SHARED /WindowA/ Rows() AS INTEGER, Cols() AS INTEGER, _
Toprow() AS INTEGER, TopCol() AS INTEGER, BotRow() AS INTEGER, _
BotCol() AS INTEGER
COMMON SHARED /WindowB/ Attribute() AS INTEGER, OnFlag() AS INTEGER,
Text) AS STRING, OldText() AS STRING, OltdAttrb{) AS STRING
THEN
IF OnFlag(Handle% )
TR TopRow(Handle%)
1c TopCol (Handle%)
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
a > Windows
OWS79
That's all there is to WindowDelete( ). Now we have a tool that will let us do some
housecleaning, if necessary, in our window system. Use WindowDelete( ) whenever
youre approaching the maximum number of windows allowed by the system to free a
few handles.
CALL WindowShow(Handle1%)
CALL WindowShow(Handle2z%)
CALL WindowDelete(Handle2%)
This program displays a window and covers it immediately with another window.
Then it deletes the second, covering, window (which removes it from the screen since it’s
being displayed) and frees its handle for reassignment.
And that’s it for our window system. We’ve designed windows, displayed them, hid-
den them, printed in them, moved them around the screen — and now deleted them.
We've put together a formidable set of programming tools, ready for action. We should
also note that the BASIC PDS has a number of window tools available; let’s take the time
now to see what it offers us here.
80 > Advanced BASIC
Making your own Quick libraries is a very powerful technique. Putting your
tested and compiled modules into a Quick library means they'll all load at
once, and the program as a whole will compile much faster.
If we were to call our example program WINDOWER.BAS, then we could then load it
with QBX.EXE like this:
This loads WINDOWER.BAS and the quick library we just made. Before we can run
the program, however, we'll have to load the PDS files MENU.BAS, GENERAL.BAS,
WINDOWBAS, and MOUSE.BAS as well — the PDS window system uses code in all
these modules. After we do this, we’re set, and we can run WINDOWER to make our
window appear.
If you’re using BC.EXE, you can create a library named UIASM.LIB to hold all the
modules you'll need. This file has to contain not only the UIASM.OBJ module, but also
the compiled versions of MENU.BAS, GENERAL.BAS, WINDOWBAS, and MOUSE.BAS
as well. Compile those files with BC.EXE and create the library UIASM.LIB like this:
LIB UTASM.LIB+UIASM.OBJ+GENERAL.OBJ+MOUSE.OBJ+MENU.OBJ+WINDOW.0BJ+QBX.LIB;
> Windows 81
Now we've got everything we need. Compile WINDOWER.BAS with BC.EXE and then
create the .EXE file like this:
LINK WINDOWER,,,UIASM;
At this point, we’re ready to run WINDOWER.EXE, and we'll get the same result as we
would under QBX.EXE.
SINCLUDE: *C:\BCZWINDOW.BI'
SINCLUDE: "CIBC 7\IMENU.BI'
SINCLUDE: "C:\BC7\MOUSE.BI'
SINCLUDE: *C:\BC7GENERAL.BI'
Next, just as in the code we developed ourselves, we have to set up some window
commons:
* $INCLUDE: 'C:\BC7\WINDOW.BI*
' $INCLUDE: 'C:\BC7\MENU.BI'
" $INCLUDE: 'C:\BC7\MOUSE.BI'
' $INCLUDE: 'C:\BC7\GENERAL.BI"
Now we're ready to start the initialization process. Three systems have to be initalized
to put a window on the screen — the window system, the menu system, and the mouse
system (even though we’re not using menus or the mouse):
SINCLUDE: 'C:\BC7\WINDOW.BI'
SINCLUDE: 'C:\BC7\MENU.BI'
SINCLUDE: 'C:\BC7\MOUSE.BI'
SINCLUDE: 'C:\BC7\GENERAL.BL'
At this point, we’re almost ready to display our window with the PDS subprogram
WindowOpen( ). First, however, we have to figure out values for the numerous argu-
ments we must pass to that subprogram. Here they are, in order:
Handle% The window’s number (1 for us)
Rowl%, Coll % Coordinates of upper left corner
Row2%, Col2% Coordinates of lower right corner
TextFore% Text foreground color (0-15)
TextBack% Text background color (0-7)
Fore% Window foreground color (0-15)
Back% Window background color (0-7)
Highlight% Color of highlighted buttons (0-15)
Movewin% If TRUE (defined in the .BI files), window can
be moved
Closewin% If TRUE, window can be closed
> Windows 83
CALL MenulInit
CALL WindowInit
CALL MouseShow
After we’ve done this, the window is on the screen. To print in it, we have to position
the cursor with WindowLocate( ); then we can use the PDS subprogram WindowPrint( )
to do the actual printing (see Listing 2-7).
84 Pb Advanced BASIC
CALL WindowOpen(1, 10, 20, 15, 30, 2, 1, 2, 1, 15, TRUE, TRUE, TRUE, _
TRUE, 0, "")
> CALL WindowLocate(1, 1)
CALL WindowPrint(1, "“Hello.")
In this example, we use WindowLocate( ) to position the cursor at (1,1) in the win-
dow, and then print in this window (window 1) with WindowPrint( ); at this point,
“Hello.” appears in our window. And that’s it — the PDS window is set up. If you have
the PDS, give it a try; there are many different window options, and you might find some
of them that work well for you.
Conclusion
And that’s it for windows. In this chapter, we've seen how to set them up, show
them
or hide them, print to them, move them, and delete them — and we've
seen what the
BASIC PDS has to offer as well. But now we’re going to turn from output
to input; we’re
going to augment BASIC by adding support for the mouse in Chapter 3,
The Mouse.
The Mouse
85
iy “epi
—— ot|
IN THIS CHAPTER, we're going to add support for the mouse. We can do that by
using DOS interrupt GH33 (the mouse interrupt) to reach the mouse from BASIC.
There’s going to be frequent use of the INTERRUPT( ) routine in this chapter.
|NOTE: | Because all the subprograms and functions in this chapter use the INTER-
RUPT( ) routine, you must load them with the /L switch if you're using Quick-
BASIC — either QB.EXE or QBX.EXE.
We'll see how to initialize the mouse, read position and button information from it,
and work with the mouse cursor. We'll also see examples, showing how to make use of
mouse information. And, in the next chapter, we'll really put what we've learned here to
use, when we combine windows and the mouse to produce pull-down menus.
Getting Started
There are two things you need to know before we start to use the mouse. First, under
DOS, you must load the mouse driver software that came with your mouse before trying
to use it. You can do that by running the .COM file that comes with the mouse (e.g.,
MOUSE.COM for a Microsoft or a Logitech mouse, or MOUSESYS.COM for a Mouse
Systems mouse). You must run this driver program before any program can use your
mouse. For more information, consult the mouse’s documentation. Note that under OS/2
or the DOS mode of OS/2 you do not need to do this — OS/2 sets up the mouse for you.
|NOTE: | The mouse driver program loads the code to handle the mouse interrupt, inter-
rupt &H33.
Second, you have to initialize the mouse before using it. Just as in the window func-
tions we developed, where we had to use WindowGetHandle%( ), you need to initialize
the mouse system before putting it to work. We'll start this chapter off by developing a
function named Mouselnitialize%( ) to do exactly that; before you can use the mouse,
you must call Mouselnitialize%( ). After that, you can use any other mouse function or
subprogram that we develop below in whatever order you want, as demonstrated by the
examples coming up.
88 Pb Advanced BASIC
Mouselnitialize% ()
This function just uses interrupt @H33 service 0 (i.e., ah = 0) to initialize the mouse
system. As mentioned, this is the necessary first step to using any other mouse function.
Here’s all we do: load ah with 0 and call interrupt QH33, then we set Mouselnitialize% to
the value returned in ax:
If this value is not zero, the mouse was successfully intialized; otherwise the operation
failed. Make sure you test this return value.
Listing 3-1 shows the whole function:
TYPE RegType
ax AS INTEGER
bx AS INTEGER
CX AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
DECLARE SUB INTERRUPT (CIntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)
FUNCTION MouselInitialize%
DIM InRegs AS RegType, OutRegs AS RegType
InRegs.ax = 0
CALL INTERRUPT(&H33, InRegs, OutRegs)
MouseInitializez% = OutRegs.ax
END FUNCTION
REMExample of MouselInitialize
LOCATE “Tt. 4
IF MouselInitialize% THEN
PRINT “Mouse Initialized.”
ELSE
PRINT “Mouse driver not installed.”
END IF
an
We check the value returned by Mouselnitialize%( ) — if it’s zero, we print out
error message (“Mouse driver not installed.”) and quit. Otherwise, we print “Mouse
Initialized.” and then quit.
by
Now that we’ve set up the mouse system for use, let’s start our use of the mouse
displaying the mouse cursor.
90 P& Advanced BASIC
MouseShowCursor( )
Like all the of mouse functions and subprograms we'll write, we just use another
interrupt GH33 service here; in this case, we use service 1, which displays the mouse
cursor:
When you use MouseShowCursor( ), the cursor appears on the screen. Listing 3-2
shows the whole subprogram.
SUB MouseShowCursor
Using MouseShowCursor( )
Here’s how to put MouseShowCursor( ) to work:
Check% = Mouselnitialize%
LOCATE 1, 1
IF Check% = 0 THEN
PRINT “Mouse driver not installed."
ELSE
PRINT "Mouse Initialized."
CALL MouseShowCursor
END IF
Notice that we first initialize the mouse with the function Mouselnitialize%( ), and, if
that worked, then we display the cursor with MouseShowCursor( ).
ripe: | If the mouse system is not initialized for some reason, calls to interrupt &H33
have no effect.
Now that we've displayed the mouse cursor on the screen, our next step will be to hide it.
|NOTE: | If the mouse cursor is already off, it stays off when we hide it.
There is one more little-known — but very important — reason for hiding the
mouse cursor. As the mouse cursor moves over the screen, the mouse driver software
reads the character at the present screen position before displaying the mouse cursor.
Then, when the mouse cursor moves on, that character is restored, attribute and all.
However, if you’ve changed the screen display behind the mouse cursor, it will still
restore the original (and wrong) character.
92 » Advanced BASIC
For example, if you move the mouse cursor to position A, the driver reads the charac-
ter at position A for later restoration, and then it overwrites it with the mouse cursor.
Now, before you move the mouse cursor, let’s say you turn a window on there, using
WindowShow( ). Next, you move the mouse cursor, and the driver software, unaware of
appearance of the window, replaces the original character at position A, leaving a one-
character hole in your window.
To avoid this problem, you should always turn the mouse cursor off when displaying a
window or overwriting the mouse cursor in any way (using the subprogram we're about
to write, MouseHideCursor( )), and turn it on again immediately afterwards (using
MouseShowCursor( )). This solves the problem completely.
MouseHideCursor( )
Here we just make use of interrupt @H33 service 2, which hides the mouse cursor:
Listing 3-3 shows what the whole subprogram looks like, ready to roll.
TYPE RegType
ax. AS INTEGER
bx AS INTEGER
Cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
et >The Mouse
al 93
END SUB
LOCATE 1,1
IF Check%= O THEN
PRINT "Mouse driver not instatled."
ELSE
PRINT "Mouse Initialized."
CALL MouseShowCursor
PRINT “Press any key to hide the mouse cursor."
“po
LOOP WHILE INKEY$ = ""
CALL MouseHideCursor
END IF
Again, we initialize the mouse system, and, if successful, show the mouse cursor and
print out the prompt: “Press any key to hide the mouse cursor.” When a key is pressed,
we hide the mouse cursor with MouseHideCursor( ).
At this point, we’ve been able to set the mouse system up, show the cursor and hide it
at will. Those are good beginning steps, but now it’s time to start reading information
from the mouse.
For example, we may want to check the status of the mouse at some given time. Is
there a button being pressed? And where is the mouse cursor? We'll work out the answer
to these questions next.
94 P Advanced BASIC
tal
pte
Mouselnformation( )
It would be ideal to have a subprogram we could call this way: CALL MouselInforma-
tion (Right%, Left%, Row%, Col%) The variables we pass could be set this way on return:
Right% = 0
Left% = 0
Next, OutRegs.dx holds the present screen row in pixels, and OutRegs.cx holds the
present screen column in pixels. The mouse and menu work we're going to do will
usually be in text mode, however, so let’s report screen row and column numbers (i.e.,
1-25 and 1-80, not pixel ranges like 0-199 and 0-639). We convert from pixel ranges to
the normal screen row and column ranges like this:
Right% = 0
Left% = 0
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
Right% = 0
Left% = 0
Row% = OutRegs.dx
Col% = OutRegs.cx OO ee a
00
END SUB
Check% = MouseInitialize%
LOCATE 1, 1
IF Check% = 0 THEN
PRINT "Mouse driver not installed."
ELSE
>The Mouse 97
IF RIGHTZX THEN
PRINT “Right button down."
ELSE
PRINT “Right button up."
END IF
IF LEFTX THEN
PRINT “Left button down."
ELSE
PRINT “Left button up.”
END IF
END EF
Here you can see how we use MouselInformation( ) and then decode the returned
values Right%, Left%, Row%, and Col%. To use this program, make sure you’ve loaded
the mouse driver as outlined in the beginning of this chapter. When you press any key on
the keyboard, this program will report the present mouse state.
The most severe limitation here is that MouseInformation( ) only provides an instant
snapshot of what’s going on with the mouse. If you want to use it for mouse input, you
have to keep “polling” it; that is, looping over it until something happens.
A better option is the subprograms we will develop later that use other, spe-
cialized interrupt &H33 services. In them, button action is stored in a “queue,”
and it waits until you call for it. This is the way most mouse programming is
done — with queues. You don’t have to catch a button being pressed exactly
as it is being pressed — you can find out about it when you're ready to deal
with it.
Also, you might prefer to have the row and column numbers returned in pixel format
rather than text format (i.e., in ranges like 0-199 and 0-639 rather than 1-25 and 1-80). If
so, just set Row% and Col% equal to OutRegs.dx and OutRegs.cx, respectively, at the end
of Mouselnformation( ):
Row% = OutRegs.dx
Cot% = OutRegs.cx
98 » Advanced BASIC
In text mode, the mouse cursor jumps from character position to character
position anyway. The pixel value that is returned corresponds to the top left
corner of the character position.
Let’s continue our exploration of the mouse with service 4, which lets you move the
mouse cursor at will.
MouseMoveCursor( )
It would be simplest if we could position the mouse by just passing the desired screen
row and column numbers like this:
— InRegs.dx = 8 * (Row% - 1)
InRegs.cx = 8 * (Col% - 1)
InRegs.ax = 4
CALL INTERRUPT(&H33, InRegs, OutRegs)
Even if you request a position far off the screen, this service does not return an error
(which is why MouseMoveCursor( ) does not produce any output); it simply places
the
cursor at the edge of the screen. Listing 3-6 shows the whole subprogram.
» The Mouse 99
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
END SUB
Check% = MouselInitialize%
LOCATE 1, 1
IF Check% = 0 THEN
PRINT “Mouse driver not installed.”
ELSE
PRINT “Mouse Initialized.”
CALL MouseShowCursor
LOCATE 10, 20
any key to move the mouse cursor to (1,1)"
PRINT “Press
dO
LOOP WHILE INKEYS = ""
CALL MouseMoveCursor (1,1)
END IF
100 P& Advanced BASIC
You can see that it’s easy to use MouseMoveCursor( ) — just pass the desired row and
column number. Of course, getting no error back from this service is both a blessing and
a curse. On one hand, you're not troubled by error messages; on the other, if you made a
genuine error in placing the mouse cursor, you should know about it.
One reasonable change to MouseMoveCursor( ) is to check the row and column num-
ber requested. If they’re out of range, you should still move the cursor, but you can pass
back an error code.
The next interrupt &(H33 service is among the most useful. Service 5 tells you how
many times a specific button was pressed since the last time you inquired — and using
this service means that we won't have to catch button presses as they happen.
MouseTimesPressed( )
We need to query service 5 about the number of time a specific button — right or left
— was pressed. The return values we expect are the number of times the button has been
pressed, and the mouse cursor’s position the last time the button was pressed. In other
words, we can set up MouseTimesPressed( ) like this:
We can use the same designation for the variable Button% as the interrupt @H33
services themselves use — a value of 0 for the left button, and 1 for the right button.
On return, we'll be able to read the other variables like this:
NumberTimes% The number of times the specified button was pushed since the
last time MouseTimesPressed( ) was called.
Row% The screen row (1-25) of the mouse cursor the last time that
button was pressed.
Col% The screen column (1-80) of the mouse cursor the last time that
button was pressed.
Using interrupt @H33 service 5 makes this subprogram pretty easy. We just place the
button number (0 for the left button, 1 for the right) into InRegs.bx and call service 5:
— InRegs.bx = Button%
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
2
ry
The results come back in bx, cx, and dx. The bx register holds the number of times the
specified button, left or right, was pressed since the last time service 5 was called. The dx
register holds the screen (pixel) row where the button was last pushed, and cx holds the
corresponding column. We can convert these to text-mode row and column ranges (1-25
and 1-80) and return:
InRegs. bx = Button%
InRegs. ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
= NumberTimes% = OutRegs.bx
Row% = OutRegs .dx 8 + 1
Col % = OutRegs.cx 8 + 1
102 » Advanced BASIC
act lta
Listing 3-7 shows the entire code for MouseTimesPressed( ), ready to be put to work:
TYPE Regtype
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE —
InRegs.bx = Button%
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
Row% = OutRegs.dx 8 + 1
Col% = OutRegs.cx 8 + 1
END SUB
Check% = MouselInitialize%
LOCATE 1, 1
IF Check% = 0 THEN
PRINT “Mouse driver not installed.”
ELSE
PRINT “Mouse Initialized.”
CALL MouseShowCursor
Stes "Press the right mouse button a number of times and then any key."
0
LOOP WHILE INKEY$ = “"
CALL MouseTimesPressed(1, NumberTimes%, Row%, Col%)
PRINT “You pressed it ";NumberTimes%;" times."
PRINT “The last time was at (€";Row%;","sC0l%;-")."
END IF
Let’s go through the steps: we initialize the mouse with Mouselnitialize( ), display the
cursor with MouseShowCursor( ), and then read mouse information with
MouseTimesPressed( ).
The limitation here is that MouseTimesPressed( ) only returns the location of the Jast
time a specific button was pressed, and you still have to poll this subprogram periodically
to find out what’s going on with the mouse (but not as often as with Mouselnforma-
tion( ), in which you have to catch the mouse event in the act).
In practice, this means that you should check MouseTimesPressed( ) frequently
enough to make sure that mouse events don’t get a chance to stack up in the mouse
queue.
Another limitation here is that you’re often more interested in when the mouse button
was released, not pressed. For example, releasing the mouse button is important when
you're dragging an object across the screen or making a menu selection. Luckily, the next
interrupt @H33 service, service 6, lets us handle that.
MouseTimesReleased ( )
Here’s how we might call MouseTimesReleased( ):
Again, we set Button% to 0 if we want right button information, and to 1 for the left
button. And, following MouseTimesPressed( ), this is how we can design the return
values:
NumberTimes% INTEGER The number of times the specified button was released
since the last time MouseTimesReleased( ) was called.
Row% INTEGER The screen row (1-25) of the mouse cursor the last time
that button was released.
Col% INTEGER The screen column (1-80) of the mouse cursor the last
time that button was released.
InRegs.bx = Button%
—+ InRegs.ax = 6
CALL INTERRUPTC(&H33, InRegs, OutRegs)
After the call, we decode the information in exactly the same way as we did for
MouseTimesPressed( ):
InRegs.bx = Button%
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
— NumberTimes% = OutRegs.bx
& The Mouse 105
Rows OutRegs.dx 8 + 1
Cols OutRegs.cx 8 + 1
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
InRegs.bx = Button%
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
Row% = OutRegs.dx 8 + 1
Cot% = OutRegs.cx 8 + 1
END SUB
Check% = MouseInitialize%
LOCATE 1, 1
IF Check% = O THEN
PRINT “Mouse driver not installed."
ELSE
PRINT “Mouse Initialized.”
CALL MouseShowCursor
PRINT “Press the Left mouse button a number of times and then any key.”
DO
LOOP WHILE INKEY$ = ""
CALL MouseTimesReleased(0, NumberTimes%, Row%, Col%)
END IF
Again we initialize the mouse, then show the mouse cursor and, after a key is pressed,
read mouse information from MouseTimesReleased( ). That’s all we have to do.
We've been following the interrupt @H33 services throughout this chapter, and there
are a few more that deserve our attention. For example, the next service, service 7, allows
you to restrict the mouse cursor to a specific range of columns on the screen.
Together with service 8, which restricts the row range, you can restrict the
mouse cursor to a specific window on the screen if you wish, giving a very
professional effect.
MouseHorizontalRange( )
As we've seen, interrupt @H33 service 7 lets us restrict the mouse cursor’s horizontal
range by specifying the right column of that range in cx and the right column in dx. We
simply convert to pixel ranges and call interrupt @H33 to make this subprogram work:
— InRegs.cx = 8 * CRight% - 1)
InRegs.dx = 8 * (Left% - 1)
InRegs.ax = 7
CALL INTERRUPT(&H33, InRegs, OutRegs)
And that’s all there is to it (service 7 does not return any values). Listing 3-9 shows the
whole subprogram.
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
InRegs.cx = 8 * (Right% - 1)
108 P» Advanced BASIC
ee ee
Subprogram. 2 of 2
Listing 3-9. MouseHorizontalRange
InRegs.dx = 8 * (Left% - 1)
InRegs.ax = 7
CALL INTERRUPT(&H33, InRegs, OutRegs)?
END SUB
MouseHorizontalRange( ) at Work
In this example program, we restrict the mouse cursor to the left half of the screen by
passing a leftmost column of 1 and a rightmost column of 40:
Check% = MouseInitialize%
LOCATE 1,1
IF Check% = 0 THEN
PRINT “Mouse driver not installed."
ELSE
PRINT “Mouse Initialized.”
CALL MouseShowCursor
PRINT "Press any key to restrict mouse to the left half of the screen.”
dO
LOOP WHILE INKEY$ = ""
CALL MouseHorizontalRange(1, 40)
END IF
Service 7 sorts out the two values (as you can imagine, the leftmost column
has to be less than or equal to the rightmost column), so it actually does not
matter if we call MouseHorizontalRange(40, 1) or MouseHorizontalRange(1,
40).
If you'd like to use graphics mode, you can change MouseHorizontalRange( ) to use
pixel ranges rather than column ranges.
The next service does the same thing, except that it restricts vertical, not horizontal
motion. Let’s look into it.
ene le repre pe llieneinttee e >The Mouse 109
CALL MouseVerticalRange
(Top%, Bottom%)
MouseVerticalRange ( )
We just have to use interrupt &H33 service 8 in much the way we used service 7.
Now, however, we are restricting the mouse cursor not to a specific set of columns, but to
a specific set of rows:
That’s all there is to it. Listing 3-10 shows the ready-to-use subprograms.
TYPE RegType
ax AS INTEGER
110 PB Advanced BASIC
InRegs.cx = 8 * (Top% - 1)
InRegs.dx = 8 * (Bottom% - 1)
InRegs.ax = 8
CALL INTERRUPT(&H33, InRegs, OutRegs)
END SUB
MouseVerticalRange ( ) at Work
In this program, we restrict the mouse cursor to the top half of the screen by calling
MouseVerticalRange(12, 1):
Check% = MouselInitialize%
LOCATE 1, 1
IF Check% = 0 THEN
PRINT “Mouse driver not installed."
ELSE
PRINT “Mouse Initialized."
CALL MouseShowCursor
PRINT “Press any key to restrict mouse to the top half of the screen.”
DO
LOOP WHILE INKEYS = “"
CALL MouseVerticalRange(12, 1)
END IF
!
> The Mouse 111
Like service 7, service 8 sorts out the two values (the top row has to be less
than or equal to the bottom row), so it does not matter if we call MouseVer-
ticalRange(12, 1) or MouseVerticalRange(1, 12).
There is one more interrupt @H33 service we might be interested in. Service QHA lets
us set the style of the mouse cursor itself. Let’s see how that works.
The screen mask determines how much of a character’s ASCII code to keep when the
mouse cursor lands at its screen position. This mask is ANDed with that character’s ASCII
code and with the character’s attribute. For example, to keep the character at that screen
position intact, use a screen mask character of &HFF and a screen mask attribute of
SYHFF To overwrite it entirely, use a screen mask character and attribute of 0.
The cursor mask then determines what the cursor will look like. The cursor mask
character and cursor mask attribute are XORed with the result of ANDing the present
character and attribute with the screen mask to produce the mouse cursor.
Let’s see how this works in practice. We might call this subprogram MouseSetCursor,
and call it like this:
For example, to use a particular ASCII character as the mouse cursor, overwrite the
existing screen character entirely by setting SMaskChar% and SMaskAttr% to 0. Then
load the ASCII code of the character you want as the mouse cursor (such as an up-arrow,
ASCII 24) into CMaskChar%, and the desired mouse cursor attribute (such as 7 for white
on black) into CMaskAttr%.
hile You can have your mouse cursor color invert characters on the screen by
setting SMaskChar% to &HFF, which preserves the ASCII code of the charac-
ter on the screen, and SMaskAttr% to &HFF to preserve its attribute byte.
Then set CMaskChar% to 0, which will XOR the character with 0 and thus
preserve it, and CMaskAttr% to &H77 to invert its attribute with XOR (use
&H77, not &HFF, to avoid turning the blinking and intensity bits on). If you just
want to invert the character and not the background behind it, set CMaskAttr%
to 7.
> The Mouse 113
In this subprogram, we're going to use interrupt @H33, service QHA. To set the
mouse cursor, we have to load 0 into bx, the full screen mask (attribute and character
bytes) into cx, and the cursor mask (attribute and character bytes) into dx. The main
difficulty is loading these two bytes into OutRegs.cx and OutRegs.dx.
Because of the way BASIC keeps track of negative numbers (i:e., with two’s comple-
ment math — see Chapter 11 for an explanantion), you can’t let the top bit of an integer
change as the result of a math operation. If you do, BASIC generates an overflow error.
This is a problem for us because we want to load SMaskAttr% into ch, the top byte of
InRegs.cx, but we can’t just say InRegs.cx = 2°8 * SMaskAttr%. If, for example,
SMaskAttr% was equal to GHFF the top bit of InRegs.cx would change and BASIC would
generate an overflow error. Instead, we have to (tediously) set the top bit of InRegs.cx
ourselves and then add the rest of the bits in.
First, we check to see if the top bit of SMaskAttr% (the bit that might cause a problem)
is set. If it is, we set up an integer called TempS%, with only its top bit set:
REM Watch out for overflow as we load the attribute into the high byte
out for overflow as we load the attribute into the high byte
REM Watch
Finally, we can load the masks into their registers like this:
REM Watch out for overflow as we load the attribute into the high byte
InRegs.bx = 0
— InRegs.cx = TempS% OR (2°78 * (SMaskAttr% AND &H7F) + SMaskChar%)
InRegs.dx = TempC% OR (2°78 * (CMaskAttr% AND @H7F) + CMaskChar%)
InRegs.ax = &HA
CALL INTERRUPTC&H33, InRegs, OutRegs)
Notice that we’ve gotten rid of the potentially troubling top bit in both SMaskAttr%
and CMaskAttr% by ANDing them with @H7F before boosting what’s left up into the top
byte (which we do by multiplying by 2°8). Then we add the lower byte, SMaskChar% or
CMaskChar%, and use the result to make up the lower 15 bits of InRegs.cx or InRegs.dx
respectively. After these registers are loaded, we call interrupt @H33 service QHA to set
the mouse cursor, and we’re done.
Listing 3-11 shows the whole subprogram.
REM Watch out for overflow as we load the attribute into the high byte
InRegs.bx = 0
InRegs.cx = TempS% OR (256 * (SMaskAttr% AND &H7F) + SMaskChar%)
InRegs.dx = TempC% OR (256 * (CMaskAttr% AND &H7F) + CMaskChar%)
InRegs.ax &HA
CALL INTERRUPT(&H33, InRegs, OutRegs)
END SUB
And now we can use it to set the mouse cursor in text mode.
Check% = MouseInitialize%
LOCATE 1, 1
116 Pb Advanced BASIC
IF Check% = 0 THEN :
PRINT "Mouse driver not installed.”
ELSE
PRINT “Mouse Initialized."
CALL MouseShowCursor
This program just changes the mouse cursor to a single dot (i.e., CHR$(250)) in the
middle of the character position with a normal attribute (i.e., white on black) of 7.
You might want to use one of the ASCII characters that look like an arrow
instead for your mouse cursor:
Keep in mind that you can only use MouseSetCursor( ) in text mode, and you are
restricted to using one of the ASCII characters. For a complete set of the ASCII characters,
see your BASIC documentation.
That completes our mouse support. We've set the mouse system up, displayed the
mouse cursor, hidden it, and read button information. Now it’s time to take a look at the
BASIC PDS’ Mouse support.
To start, you must call Mouselnit( ) to initalize the mouse (as we do above with
Mouselnitialize( )). Next, you can restrict the mouse cursor range to a specific area of the
screen by calling MouseBorder(row1%, coll%, row2%, col2%), where the limiting box is
described by (row1%, coll%) and (row2%, col2%):
€ (row2%, col2%)
Figure 3-1
Then you can display the mouse cursor with MouseShow( ) and hide it again with
MouseHide( ).
To read actual mouse events, however, you're limited to polling (i.e., calling repeat-
edly) the subprogram MousePoll( ). MousePoll( ) works like our subprogram Mouse-
Information( ) above, which means that you have to catch mouse actions as they occur.
After MousePoll( ), there’s only one last mouse subprogram — MouseDriver( ), which
lets you pass arguments directly on to interrupt @H33 (and therefore access the other
DOS mouse services directly). Since we can do that as easily with INTERRUPTC( ),
MouseDriver( ) doesn’t add much utility.
Let’s develop a program to report mouse position using the PDS mouse tools. If you're
using QBX.EXE, you'll have to use the /L switch when loading. First, we initialize the
mouse:
CALL MouselInit
Now we can use MousePoll( ) to wait until you press the right mouse button. The
variables it fills are: row%, col%, IButton%, and rButton%. Here, IButton% and rButton%
return values of either TRUE or FALSE depending on whether or not the button is
pushed, and the screen position is returned in text-mode coordinates:
CALL MouselInit
CALL MouseShow
PRINT “This program waits for you to press the right mouse button...”
+ dO
CALL MousePoll(row%, col%, LButton%, rButton%)
LOOP WHILE rButton% = FALSE
After the button is pressed, we can report the position of the mouse cursor like this:
CALL MouselInit
CALL MouseShow
PRINT "This program waits for you to press the right mouse button...”
DO
CALL MousePoll(row%, col%, (Button%, rButton%)
LOOP WHILE rButton% = FALSE
— PRINT “The mouse cursor was at: ("; Cows; % > colts "5"
And that’s all there is to it — we’ve set up the mouse and used it in the PDS.
Unlike
the menu and window functions, the mouse functions in the BASIC ToolBox
are quite
simple.
> The Mouse 119
Conclusion
That’s it for the specific mouse subprograms and functions. Now, however, we’re going
to put them to work immediately in our pull-down menu system, coming up next in
Chapter 4.
ia
gat. “7 @
: = ny
if
"i ahs
ee
Z-
: ”
. eae
;
=» @ a
‘ —
/
»- — a =
aD
a
, >
Po
,
> - 7
nie
’ ¥ a 7
a
oe.
on ee
i‘
a ’ :i we
4 Tp
7 :
: a "Als na —
4
: j a
:
Oy <<.
~~ eae
:42 : ee PE
’ ee OS fon
a
;
7
= ; :
ss
Pull-down Menus
121
> Pull-down Menus’ 123
IN THIS CHAPTER, we're actually going to write a complete, fully functional menu
system, ready for use. You'll be able to pull down menus from a menubar and make
selections with either the keyboard or the mouse. This is going to take some time and
effort on our part, but we have already developed much of the programming technology
needed when we covered windows and the mouse; all that remains now is to combine the
two into a functional whole.
We'll do that in this chapter (as well as taking a look at the PDS menu tools at the end).
Our first step, as it should always be with programs of any size, is to get a clear picture of
what we want.
MenuBar my
The user then moves the mouse cursor to a specific line in the menu and releases the
mouse button, and our program should then be informed which menu — and which
choice in that menu — was chosen.
In addition, the menus should function in either text or graphics mode — and with or
without a mouse. If there’s no mouse, they should default back to keyboard-only use
automatically. For example, we should be able to press Alt-M, where M stands for the first
letter of the menu we wish to open. That menu pops open, and the user can then make a
choice by pressing Alt-C, where C stands for the first letter of the menu choice he or she
wishes to make.
We might, however, want to close the menu without making a choice. In that case, we
could press Esc to close the open menu (this is standard for menu systems), or we could
press Alt-A, where A is the first letter of another menu, or we could press Alt-M once
again to toggle the menu closed.
mousedown, indicating that a mouse button was either pushed or released, and Menu-
GetEvent$( ) reports the screen coordinates of that event. On the other hand, if the string
is menuchoice, then one of the selections in a menu was chosen; MenuGetEvent$( )
reports the menu the choice was made from, and indicates which choice it was.
In this way, MenuGetEvent$( ) will be our primary input routine. The process, as
we've designed it so far, goes like this:
1. Call Menulnitialize%( ) to initialize the menu system with the names of the
menus and the names of the choices inside each menu.
2. To display the menu, use MenuShow( ). (MenuHide( ) will hide it again.)
3. Now use MenuGetEvent$( ) as the primary input to our program; treating it as a
combination of INKEY$, a mouse monitor, and a menu monitor.
That's all there is to it. We've wrapped all keyboard, mouse, and menu input to your
program into MenuGetEvent$( ). Now the only thing left is to write these routines.
Choice 1
Choice 2
Choice 3 < Menu 1
Choice 4
Choice 5
Choice 6
Figure 4-2
So we'll have to initialize all the menu names (the names that appear in the menubar),
the menu choices (the names that appear in a given menu), the screen attribute of the
menubar, and the screen attribute of each menu.
126 P& Advanced BASIC
Let’s design Menulnitialize%( ) now. To start, we have to pass the names of the menus
themselves as they are to appear in the menubar. To pass those names we can use a one-
dimensional array of type STRING named MenuNames( ). For example, if we set Menu-
Names(1) to this:
MenuNames(1) = “Fruits"
then the first entry in the menubar would be Fruits (see Figure 4-3).
CALL MenuInitialize%(MenuNames(
)...)
Next, we have to pass the names of the individual choices in the menus that we’ve just
named. We should be able to pass them as a two-dimensional array called, say, Menu-
Choices( ), also of type STRING. For example, MenuChoices(1, 1) would be the first
choice in menu 1, MenuChoices(1, 2) holds menu 1, choice 2, MenuChoices(2, 5) would
represent menu 2, choice 5, and so on. For example, to put five choices in menu 1, we
might do this:
> Pull-down Menus 127
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = “Bananas"
MenuChoices(1, 3) = "Grapes"
MenuChoices(1, 4) = "Peaches"
MenuChoices(1, 5) = “Oranges”
Apples
Bananas
Grapes
Peaches
Oranges
Figure 4-4
Not all menus have to have the same number of entries, of course, and we must allow
for that. We will only put in the number of entries for each menu that it should have
(which leaves the unfilled entries as null strings). For example, we might set up menu 2
like this, with only three entries:
MenuChoices(2, 1) = “Peas”™
MenuChoices(2, 2) = “Corn”
MenuChoices(2, 3) = “Broccoli”
The only items that are left are the screen attributes we want to use for the menubar
and the menus themselves. Passing an attribute for the menubar is easy — we can just
pass an INTEGER named BarAttrb%. This is the screen attribute you want the menubar
to have:
Finally, we can pass a different screen attribute for each of the menus using a one-
dimensional array of INTEGERs named MenuAttrbs( ), menu attributes. For example,
load MenuAttrbs(3) with the screen attribute you want menu 3 to have when it pops
open. And we're done designing the call to Menulnitialize%( ):
We may want a given menu choice to open a whole new set of menus and thus
display a whole new menubar. For example, one menu choice could be File
System. Once selected, we might want the menubar to change so that the
menus now correspond to, say, Read, Write, Open, Close. To change menu-
bars and menu choices, we can use Menulnitialize%( ) again, on the fly. We
can call Menulnitialize%(_) to set up the new menus, MenuHide( ) to hide the
current one, and then MenuShow( ) to display the new one (and then Menu-
GetEvent$( ) to receive input).
Menulnitialize%( )
Here are the inputs we’ve worked out for Menulnitialize%( ):
> Pull-down Menus 129
Next, still treating each menu as a window, we have to store the normal window arrays
for each one. Text( ) will hold the contents of the window; i.e., the menu choices, row by
row. OldText( ) will hold the text that was on the screen before the menu appeared, and
OldAttrb( ) the attributes of each screen position that we overwrite. Finally, we need an
array to hold the menu’s attributes, Attribute ):
130 P» Advanced BASIC
What we’ve done for the menus/windows, we must now do for the menubar itself. We
can store the text in the bar as a simple string named Bar$. We'll also need the bar’s screen
attribute, BarAttrb, the text that was on the screen before the menubar appeared,
OldBar$, and the screen attributes at each character position we’re going to overwrite,
which we can store in a string named OldBarAttrb$.
There are two last, important menu variables here. First, we'll need the number of
menus, and we can store that in a variable named NumMenus%. In addition, we have to
know whether or not the menubar is on the screen, and therefore whether or not the
menu system is active. We can keep track of that with a variable named MenusOnFlag%:
That’s it for the COMMONSs; we can start Menulnitialize%( ) itself with a few prelim-
ary boundary checks on the incoming arrays, then clear the necessary memory locations:
NumMenus% = UBOUND(MenuNames, 1)
IF NumMenus% > 7 THEN EXIT FUNCTION
IF UBOUND(MenuChoices, 2) > 24 THEN EXIT FUNCTION
ERASE Rows
ERASE Cols
ERASE TopRow
ERASE TopCol
ERASE BotRow
ERASE BotCol
ERASE Attribute j
ERASE Onflag :
ERASE Text
ERASE OldText
> Pull-down Menus 131
ERASE OldAttrb
Bars = ""
MenusOnFlag% = 0
Now it’s just a matter of filling them again. We start off by looping over each menus
(there are NumMenus% of them), and assembling the menubar:
NumMenus% = UBOUND(MenuNames, 1)
IF NumMenus% > 7 THEN EXIT FUNCTION
IF UBOUND(MenuChoices, 2) > 24 THEN EXIT FUNCTION
ERASE Rows
ERASE Cols
ERASE TopRow
ERASE TopCol
ERASE BotRow
ERASE BotCol
ERASE Attribute
ERASE OnFlag
ERASE Text
ERASE OldText
ERASE OldAttrb
Bars = ial
MenusOnFlag% = 0
BarAttrb = BarAttrb%
FOR i = 1 TO NumMenus%
Bar$S = Bar$+"“ "+ MenuNames(i)+ SPACE$(8-LEN(MenuNames(i))) + CHRS$(179)
ee
on
NEXT i
We place each menu name from MenuNames( ) into the bar and separate them with the
vertical bar character, CHR$(179). While we're looping over all menus, we can fill each one
with the correct choices from MenuChoices( ). These choices make up the text for each
menu, so we'll store this information in the array Text( ), as we did for our windows. For a
given menu, we have to loop over the number of choices MenuChoices( ), treating each
choice as a row in the menu (and padding it with spaces):
NumMenus% = UBOUND(MenuNames, 1)
IF NumMenus% > 7 THEN EXIT FUNCTION
132 Pb Advanced BASIC
ERASE Rows
ERASE Rows
ERASE Cols
ERASE TopRow
ERASE TopCol
ERASE BotRow
ERASE BotCol
ERASE Attribute
ERASE OnFlag
ERASE Text
ERASE OldText
ERASE OldAttrb
Bars = au
MenusOnFlag% = 0
BarAttrb = BarAttrbz%
FOR i = 1 TO NumMenusZ%
Bar$ = Bar$St+" "+ MenuNames(i)+ SPACES(8 - LEN(MenuNames(i))) + CHRS$(179)
FOR j = 1 TO UBOUND(MenuChoices, 2) :
Temp$S = MenuChoices(i, j)
IF TempS <> “" THEN
IF LEN(Temp$) > 15 THEN EXIT FUNCTION
Rows(€i) = Rows(i) + 1 "Number of rows in menu i
> Text(i, j) = " " + Temp$ + SPACES(15 - LEN(Temp$))
END IF
NEXT j
NEXT i
Finally, there are a few more variables to be set each time we loop through for a given
menu. We have to record, among other things, its top and bottom rows on the screen
(recall that we’re treating each menu as a window, and, therefore, the top row is row 2 —
right under the menubar — for each menu); the left and right columns of the menu; and
the attribute of the menu. After this, we round out the menubar so it’s a full 80 characters
long — the width of the screen.
NumMenus% = UBOUND(MenuNames, 1)
IF NumMenus% > 7 THEN EXIT FUNCTION
IF UBOUND(MenuChoices, 2) > 24 THEN EXIT FUNCTION
ERASE Rows
ERASE Cols
ERASE TopRow
ERASE TopCol
ERASE BotRow :
ERASE BotCol
ERASE Attribute
ERASE OnFlag
ERASE Text
> Pull-down Menus’ 133
ERASE OldText
ERASE OldAttrb
Bar$ = ""
MenusOnFlag% = 0
BarAttrb = BarAttrb%
FOR i 1 TO
NumMenus%
Bars iou Bar$+" "+ MenuNames(i)+ SPACES(8-LEN(MenuNames(i))) + CHRS(179)
FOR j = 1 TO UBOUND(MenuChoices, 2)
Temp$ = MenuChoices(i, j)
IF Temp$ <> "" THEN
IF LENCTemp$) > 15 THEN EXIT FUNCTION
Rows(i) = Rows(i) + 1
Text(i, j) = " “ + TempS + SPACE$(15 - LEN(Temp$))
END IF
NEXT j
> Cols(€i)d = 16
. TopRow(i) = 2
z TopCol(i) = 1+ 10 * (i - 1)
BotRow(i) = 1 + Rows (i)
BotCol(i) = TopCol¢(i) + 16
Attribute(i) = MenuAttrbs(i)
NEXT i :
MenuInitialize% = 1
CONST MaxMenus = 7
DIM Rows(1 TO MaxMenus) AS INTEGER
DIM Cols(1 TO MaxMenus) AS INTEGER
DIM TopRow(1 TO MaxMenus) AS INTEGER
DIM TopCol(1 TO MaxMenus) AS INTEGER
DIM BotRow(1 TO MaxMenus) AS INTEGER
DIM BotCol(1 TO MaxMenus) AS INTEGER
DIM Attribute(1 TO MaxMenus) AS INTEGER
DIM Text(1 TO MaxMenus, 25) AS STRING
DIM OldText(1 TO MaxMenus, 25) AS STRING
DIM OldAttrb(1 TO MaxMenus, 25) AS STRING
COMMON SHARED /MenuA/ Rows( ) AS INTEGER, Cols() AS INTEGER, _
TopRow( ) AS INTEGER, TopCol() AS INTEGER, _
BotRow( ) AS INTEGER, BotCol() AS INTEGER
COMMON SHARED /MenuB/ Attribute( ) AS INTEGER, Text() AS STRING, _
OldText( ) AS STRING, OtdAttrb() AS STRING
COMMON SHARED /MenuC/ Bar$, BarAttrb, MenusOnFlag%, NumMenus%, OldBar$,_
OldBarAttrb$
134 Pb Advanced BASIC
MenulInitialize% = 0
NumMenus% = UBOUND(MenuNames, 1)
IF NumMenus% > 7 THEN EXIT FUNCTION
IF UBOUND(MenuChoices, 2) > 24 THEN EXIT FUNCTION
ERASE Rows
ERASE Cols
ERASE TopRow
ERASE TopColt
ERASE BotRow
ERASE BotCol
ERASE Attribute
ERASE OnFlag
ERASE Text
ERASE OldText
ERASE OldAttrb
Bars = aetr
MenusOnFlag% = 0
BarAttrb = BarAttrb%
FOR ji 1 TO NumMenus%
Bar$ BarS+" "+ MenuNames(i)+ SPACES(8 - LEN(MenuNames(i))) + CHRS$(179)
FOR j = 1 TO UBOUND(MenuChoices, 2)
Temp$S = MenuChoices(i, j)
IF Temp$ <> “” THEN
IF LEN(€Temp$) > 15 THEN EXIT FUNCTION
Rows(i) = Rows¢i) + 1
Text(i, j) =" " + Temp$ + SPACE$(15 - LEN(Temp$))
END IF
NEXT j
Cols(i) = 16
TopRow(i) = 2
TopCol(i) = 1+ 10 * (i - 1)
BotRow(i)d) = 1 + Rows(i)
BotCol(i) = TopCol(i) + 16
Attribute(i) = MenuAttrbs(i)
NEXT ji
Menulnitialize% = 4
END FUNCTION
At this point, all the menu COMMONs are filled and ready to roll. Keep in mind
that
to display the menubar on the screen, we'll have to call MenuShow( ) later.
MenuNames(1) = "Fruits"
MenuChoices(1, 1) = "Apples”
MenuChoices(1, 2) = "Bananas"
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = "Peaches"
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = "Veggies"
MenuChoices(2, 1) = "Peas”
MenuChoices(2, 2) = “Corn"
MenuChoices(2, 3) = "Broccoli"
MenuNames(3) = "Meats"
MenuChoices(3, 1) = "Chicken"
MenuChoices(3, 2) = "Pork"
MenuChoices(3, 3) = "Beef"
MenuChoices(3, 4) = "Fish”
FOR 4. = 1-10.35
MenuAttrbs(i) = &H61
NEXT i
BarAttrb% = &H24
You can see how we load each of the arrays before calling Menulnitialize%(_). Once we
do, however, we're all set; we can use MenuShow( ) — coming up next — to display the
menubar.
later). This is to avoid overwriting the mouse cursor with the menubar; as mentioned last
chapter, if you overwrite the mouse cursor and then move the mouse, the mouse driver
will restore the original (i.e., before the mouse cursor got there) character there, leaving a
hole in the menubar:
SUB MenuShow
CurRows = CSRLIN
CurCol% = POSCO)
> InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
ee
sa
If there is no mouse or the mouse system was never initialized, then when your
program turns the mouse cursor “back” on, there is no effect.
There are three steps to the body of MenuShow( ). First, we save the region of the
screen we're about to overwrite — that is, the top row — with the BASIC SCREEN
function (SCREEN allows us to pick up both characters and attributes):
SUB MenuShow
CurRow% = CSRLIN
CurCol% Pos (0)
: temp1$ = ""
temp2$ = '""
FOR j = 1 To 80
tempi$ = temp1$ + CHRSCSCREEN(1, j))
temp2$ = temp2$ + CHRSCSCREEN(1, j, 1))
NEXT j
OldBar$ = tempi$
OldBarAttrb$ = temp2$
wee
> Pull-down Menus’ 137
Next, we display the new menubar on the screen, using interrupt QH10 service 9
exactly as we did in WindowShow( ):
CurRow% CSRLIN
CurCol% POS(0)
temp1$ = ""
temp2$ = ""
FOR j = 1 TO 80
temp1$ = temp1$ + CHRSCSCREEN(1, j))
temp2$ = temp2$ + CHRSCSCREEN(1, j, 1))
NEXT j
OldBar$S = temp1$
OldBarAttrb$ = temp2$s
|
ow
ue InRegs.cx = 1
FOR j = 1 TO 80
LOCATE: 1,3
InRegs.ax = &H900 + ASC(MIDS$(Bar$, j, 1))
InRegs.bx = BarAttrb
CALL INTERRUPT(&H10, InRegs, OutRegs)
NEXT j
Finally, we have to clear the way for the rest of the menu system. We do that by turning
the mouse cursor back on, indicating that the menubar is on — and therefore that the
menu system is active (by setting MenusOnFlag% to 1) — resetting the cursor to its
original position, and clearing the mouse queue:
CurRow% CSRLIN
CurCol% POs (0)
temp1$ ae
temp2$ wou
FOR j = 1 TO 80
temp1$ = temp1$ + CHRSCSCREEN(1, j))
temp2$ = temp2$ + CHRSCSCREEN(1, j, 1))
NEXT j
OldBarS = temp1$
138 Pb Advanced BASIC
ol dBarAttrb$ = temp2$
InRegs.cx = 4
FOR j = 1 TO 80
LO CATE: 175-3
In Regs.ax = &H900 + ASCCMIDS$S(Bar$, j, 1))
In Regs.bx = BarAttrb
CA LL INTERRUPTC(&H10, InRegs, OutRegs)
NEXT j
REM Turn mouse cursor back on
InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)
MenusOnFlag% = 1
InRegs.bx = 0
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = 1
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = 0
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = 1
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
And the menubar is now on, across the top of the screen. Listing 4-2 shows the whole
subprogram.
ES) diate me
OP Men uShow ( ) Subprogram—Displays the Menubar.
DECLARE SUB MenuShow (¢ )
CONST MaxMenus = 7
DIM Rows(1 TO MaxMenus) AS INTEGER
DIM Cols(1 TO MaxMenus) AS INTEGER
DIM TopRow(1 TO MaxMenus) AS INTEGER
DIM TopCol(1 TO MaxMenus) AS INTEGER
DIM BotRow(1 TO MaxMenus) AS INTEGER
DIM BotCol(1 TO MaxMenus) AS INTEGER
DIM Attribute(1 TO MaxMenus) AS INTEGER
DIM Text(1 TO MaxMenus, 25) AS STRING
DIM OldText(1 TO MaxMenus, 25) AS STRING
DIM OldAttrb(1 TO MaxMenus, 25) AS STRING
COMMON SHARED /MenuA/ Rows() AS INTEGER, Cols €) AS INTEGER, |
TopRow( ) AS INTEGER, TopCol() AS INTEGER, _
BotRow( ) AS INTEGER, BotCol() AS INTEGER
COMMON SHARED /MenuB/ Attribute( ) AS INTEGER, Text() AS STRING, _
OldText( ) AS STRING, OldAttrb() AS STRING
COMMON SHARED /MenuC/ Bar$, BarAttrb, MenusOnFlag%, NumMenus%, __
OldBar$, OldBarAttrb$
SUB MenuShow
CurRow% CSRLIN
CurCol% Pos(Q)
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
temp1$ = ""
temp2$ = ""
FOR j = 1 TO 80
temp1$ = temp1$ + CHRSCSCREEN(1, j))
temp2$ = temp2$ + CHRS(SCREEN(1, j, 1))
NEXT j
OldBar$ = temp1$
OldBarAttrb$ = temp2$
InRegs.cx 1
FOR j = 1 TO 80
LOCATE 1, j
InRegs.ax = &H900 + ASC(MID$S(Bar$, j, 1))
InRegs.bx = BarAttrb
CALL INTERRUPT(&H10, InRegs, OutRegs)
NEXT j
MenusOnFlag% = 1
InRegs.bx 0
InRegs.ax 5
CALL INTERRUPTC(&H33, InRegs, OutRegs)
InRegs.bx = 1
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = 0
InRegs.ax = 6
CALL INTERRUPT(8&H33, InRegs, OutRegs)
InRegs.bx = 1
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
END SUB
MenuNames(1) = "Fruits"
MenuChoices(1, 1) = “Apples
MenuChoices(1, 2) = "Bananas"
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = “Peaches”
MenuChoices(1, 5) = "Oranges"
MenuNames(2) = “Veggies”
MenuChoices(2, 1) = “Peas"
MenuChoices(2, 2) = “Corn”
> Pull-down Menus 141
MenuChoices(2, 3) = "Broccoli"
MenuNames(3) = “Meats"
MenuChoices(3, 1) = "Chicken"
MenuChoices(3, 2) = "Pork"
MenuChoices(3, 3) = "Beef"
MenuChoices(3, 4) "Fish"
FOR i = 1 To 3
MenuAttrbs(i) = &N61
NEXT i
BarAttrb% = &H24
Check% = Menulnitialize%(MenuNames(
), MenuChoices(), BarAttrb%,
MenuAttrbs())
IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.”
END IF
Now the menubar appears on the screen (but is inactive). Note that one significant
limitation of displaying the menubar is that you have to be very careful when scrolling the
screen — you can scroll the menubar right off the top. If you do, of course, you can just
call MenuShow( ) again without harm, but that gives the menubar a flickering
appearance. Usually, programs that use menus carefully manage the screen anyway, and
often don’t scroll at all, so in practice this is not such a severe limitation.
You can actually control BASIC’s attempts to scroll the screen in assembly
language, although it is complicated. You need to write a terminate-and-stay-
resident program to intercept calls to BIOS interrupt & H10, services 6 and 7—
this is how BASIC scrolls the screen. For more details, see the appendix.
At this point, we’ve got the menubar on the screen, so the next natural step is to take it
off.
One place you should definitely use MenuHide( ) is at the end of your pro-
gram, to restore the screen.
All this subprogram does is to read the original top row of the screen from the menu
COMMONS and replace it on the screen (after handling the mouse cursor as in Men-
uShow( )). It doesn’t have to store the menubar first, of course, because that is already in
memory (in the string Bar$).
First, we save the position of the cursor on the screen and turn off the mouse cursor:
SUB MenuHide
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
CurRow% = CSRLIN
CurColz = POoSsco)
see
SUB MenuHide
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
CurRow% = CSRLIN
CurCol% = Ppos(Q)
InRegs.cx = 1
> FOR j = 1 TO 80
LOCATE 1, j
InRegs.ax = &H900 + ASCCMID$(COLdBar$, j, 1))
InRegs.bx = ASCCMIDSCOLdBarAttrb$, j, 1))
De INTERRUPT(&H10, InRegs, OutRegs)
.
=
> Pull-down Menus 143
At the end, we indicate that the menubar is off (and hence that the menu system is
inactive and should simply pass input through to the calling program) by setting Menus-
OnFlag% to 0. We also restore the mouse cursor, and move the screen cursor back to the
position it had when MenuHide( ) was called:
SUB MenuHide
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
CurRows CSRLIN
CurCol% POS(QO)
InRegs.cx = 1
FOR j = 1 TO 80
LOCATE 1, j
InRegs.ax = &H900 + ASC(MIDS$(OldBar$, j, 1))
InRegs.bx = ASC(MIDS$COLdBarAttrb$, j, 1))
CALL INTERRUPT(&H10, InRegs, OutRegs)
NEXT j
Ca
ae InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)
MenusOnFlag% = O
CONST MaxMenus = 7
DIM Rows(1 TO MaxMenus) AS INTEGER
DIM Cols(1 TO MaxMenus) AS INTEGER
DIM TopRow(1 TO MaxMenus) AS INTEGER
DIM TopCol(1 TO MaxMenus) AS INTEGER
DIM BotRow(1 TO MaxMenus) AS INTEGER
DIM BotCol(1 TO MaxMenus) AS INTEGER
DIM Attribute(1 TO MaxMenus) AS INTEGER
DIM Text(1 TO MaxMenus, 25) AS STRING
DIM OLdText(1 TO MaxMenus, 25) AS STRING
DIM OldAttrb(1 TO MaxMenus, 25) AS STRING
COMMON SHARED /MenuA/ Rows) AS INTEGER, Cols() AS INTEGER, _
TopRow( ) AS INTEGER, TopCol() AS INTEGER, ef
BotRow( ) AS INTEGER, BotCol() AS INTEGER
COMMON SHARED /MenuB/ Attribute( >) AS INTEGER, Text) AS STRING, _
: OldText( } AS STRING, OldAttrb( ) AS STRING
COMMON SHARED /MenuC/ Bar$, BarAttrb, MenusOnFlag%, NumMenus%, 2S
OldBar$, OldBarAttrb$
SUB MenuHide
DIM InRegs AS RegType, OutRegs AS RegType
CurRow% CSRLIN
CurCol% iow POS (0)
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.cx = 1
FOR j = 1 TO 80
LOCATE 1, j
InRegs.ax = &H900 + ASCCMIDS(OLdBar$, j, 1))
InRegs.bx = ASCCMID$(OLdBarAttrb$, j, 1))
CALL INTERRUPT(&H10, InRegs, OutRegs)
NEXT j
InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)
MenusOnFlag% = 0
END SUB
> Pull-edown Menus’ 145
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = “Bananas”
MenuChoices(1, 3) = "Grapes"
MenuChoices(1, 4) = “Peaches”
MenuChoices(i, 5) = "Oranges"
MenuNames(2) = “Veggies”
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) = "Corn"
MenuChoices(2, 3) = "Broccoli"
MenuNames(3) = “Meats”
MenuChoices(3, 1) = “Chicken”
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) = “Beef”
MenuChoices(3, 6) = "Fish"
FOR i = 1 TO 3
MenuAttrbs(i) = &H61
NEXT i
BarAttrb% = &H24
MenuChoices(), BarAttrb%, _
Check% = MenuInitialize%(MenuNames(),
MenuAttrbs())
IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.”
END IF
CALL MenuShow
Now we're set. We’ve initialized the menubar and each of the menus. We’ve been able
to place the menubar on the screen and take it off again. The next step is to receive input
with MenuGetEvent$( ).
Calling MenuGetEvent$( ) is what makes the menubar active. You call it and then wait
for input. MenuGetEvent$( ) is the heart of the menu system. This function has to handle
many different types of input, however, and it’s going to be a giant.
If you don’t want to wait for input, we'll also write another function, Menu-
CheckEvent$( ), the INKEY$ of our menu system. If there’s a keyboard,
mouse, or menu event waiting, it will report it. Otherwise, it returns immediately
with a value of “”, as INKEY$ would.
Because we want MenuGetEvent$( ) to monitor the menus, the mouse, and the key-
board, we can design it to be used like this:
Note the variables that we pass to MenuGetEvent$( ) here; they should be filled like
this, depending on the string that was returned:
InString$ Means
STRING of LEN 1 Single character typed. Read as you would from INKEY$.
STRING of LEN 2 Character typed with an extended ASCII code. Read as you
would from INKEY$.
> Pull-down Menus’ 147
_ L
"“menuchoice” = MenuGetEvent$ (MenuNo%, ChoiceNo%, Button’, ScRow%, ScCol%z)
These numbers correspond to the indices in the arrays we’ve passed to Menulnitial-
ize%( ). For example, if we’ve initialized menu 1 like this:
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = “Bananas”
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = "Peaches"
MenuChoices(1, 5) = “Oranges”
148 Pb Advanced BASIC
J L L
“mousedown" = MenuGetEvent$ (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%)
And, if a key was struck, MenuGetEvent$( ) will act just like INKEY$( ). In this way,
we'll be able to report each type of input event.
We've already specified how the menu system should work in the beginning of the
chapter — let’s translate that into an outline for MenuGetEvent$( ):
DO
: IF Ca key was pressed) THEN
: MenuGetEvent$ = key that was pressed
- IF (menu system is on) THEN
s IF (Esc was pressed) THEN
: IF (menu was open) THEN
: Eclose menu and EXITIJ
: ELSE
: EXIT FUNCTION
: END IF
: END IF
: IF CALt key matched a menu or choice name) THEN
: Cset menu arguments and EXIT]
= ELSE
: EXIT FUNCTION
: END IF
‘ END IF
: EXIT FUNCTION
: END IF
Even in outline, it’s a lengthy listing. There are two primary parts to the code: the first
checks to see if a key was typed and tries to interpret that key; and the second checks the
mouse queue and tries to interpret mouse events. Let’s start by taking a look at the key
reading part. First, we start off by assigning a null string, “ ”, to MenuGetEvent$, and set
up a DO...LOOP WHILE 1 loop that will continually wait for input:
DO
LOOP WHILE 1
Now we try to read in a key. If one is waiting, and the menubar is off (i.e., Men-
usOnFlag% = 0), we return with that value:
dO
InChar$ = INKEYS
Next, we check to see if it was an Esc key, which closes a menu if it’s open. If it was an
Esc key and no menu is open, we pass the Esc on to the calling program.
On the other hand, if a menu was open and Esc was pushed, this is a “menuclose”
event, and we close the menu, set the return value MenuNo% to inform the calling
program which menu was closed and exit.
We
How will we know whether or not a menu is open? This is an important point:
no way to
have a flag, MenusOnFlag%, to indicate that the menubar is displayed — but
open. For that reason, let’s introduc e a new
keep track of which menu might actually be
150 P Advanced BASIC
variable named MenuNowOpen%. This important variable, which will appear frequently
in MenuGetEvent$( ), holds the number of the menu now open; if no menu is open, it
will hold 0 (we'll set the value in MenuNowOpen% whenever we open or close a menu).
To check whether any menu is now open, therefore, we only have to check the value in
MenuNowOpen%:
DO
InCharS = INKEYS
We don’t have to put MenuNowOpen% into the menu commons since it’s
entirely local to MenuGetEvent$( ), unlike MenusOnFlag%, which is also used
by MenuShow( ) and MenuHide( ).
If there is a menu open, on the other hand, we'll have to close it, since Esc was pushed.
Since we'll be opening and closing menus frequently, let’s set up two subprograms named
TumOnMenu( ) and TurnOffMenu( ) that display and hide individual menus. These two
routines are simple adaptations of our WindowShow( ) and WindowHide( ) subpro-
grams, respectively. To close the currently open menu, for example, all we have to do is to
CALL TurnOffMenu(MenuNowOpen%):
DO
InChar$ = INKEYS
If it wasn’t an escape key but the length of the string INKEY$ returned was 1, then it’s a
normal key with no significance for us — we return it to the calling program. Otherwise,
we check to see if it’s an Alt key by examining the scan code (the second character in the
string INKEY$ returned, which we’ve named InChar$). If it’s not an Alt key, this key is
not for us and we pass it to the calling program:
DO
InChar$ = INKEY$
°
.
At this point, if we’re still in the function, we've identified the key as an Alt key, and
placed it in AltKey$. We have to check if it matches the first letter of a choice in the
currently open menu (if one is currently open; see Figure 4-6). If it does, this is a
menuchoice event; a choice was made in that menu. We have to set the values MenuNo%
and ChoiceNo% appropriately for use by the calling program, close the menu, and exit:
Apples
Bananas
Grapes
Peaches
Oranges
Figure 4-6
dO
InChar$ = INKEYS
AltKey$ = ""
IF ScanCode >=16 AND ScanCode <25= THEN AltKeyS=MIDSC"QWERTYULOP",
ScanCode - 15,1)
IF ScanCode <=30 AND ScanCode >38= THEN Al tKey$=MIDSC"ASDFGHJKL",
ScanCode - 29,1)
IF ScanCode <=44 AND ScanCode >50= THEN AltKey$=MID$C"ZXCVBNM",__
ScanCode - 43,1)
IF AltKey$ = “" THEN EXIT FUNCTION
one
If we haven't exited the function at this point, then the Alt key hasn’t matched a choice
in a menu that’s currently open (if any). In that case, we have to check and see if we’re
supposed to close that menu (i.e., referencing a menu with the same Alt key toggles it on
and off); in that case, this is a menuclose event.
Otherwise, we must check if this Alt key matches another menu name in the menubar.
If it does, this is a menuopen event; we open that menu (after first closing any open
menu). Finally, if nothing matches, the Alt key wasn’t for us and we pass it on to the
calling program (keep in mind that NumMenus% is the number of menus in the
menubar):
DO
InChar$ = INKEY$
MenuGetEvent$ = "menuclose”
EXIT FUNCTION
END IF
END IF "Escape key
IF LENCInChar$) = 1 THEN
EXIT FUNCTION
ELSE
ScanCode = ASC(RIGHTS(InChar$,1))
AltKey$ = ""
THEN AltKeyS=MIDS("QWERTYUIOP”,_
IF ScanCode >=16 AND ScanCode <=25
ScanCode - 15,1)
ALtKeyS=MIDS("“ASDFGHJKL",_
IF ScanCode >=30 AND ScanCode <=38 THEN
ScanCode - 29,1)
IF ScanCode >=44 AND ScanCode <=50 THEN AltKeyS=MIDS("ZXCVBNM” ,_
ScanCode - 43,1)
IF AltKey$ = “" THEN EXIT FUNCTION
FOR i =
1 TO NumMenus%
IF AltKey$ = UCASES(MIDS$(Bar$,10*(i-1)+2,1)) THEN
se]
eo IF MenuNowOpen% = i THEN ‘This menu toggled off
MenuNo% = i
CALL TurnOffMenuli>d
MenuNowOpen% = 0
MenuGetEvent$S = “menuclose”
EXIT FUNCTION
END IF
IF MenuNowOpen% 0 THEN CALL TurndOffMenu(MenuNowOpenz)
MenuNo% = i "Another menu turned on
ChoiceNos = 0
MenuNowOpens’ = i
CALL TurnOnMenu(i)
MenuGetEvent$ = “menuopen"
END IF
NEXT i ‘for i = 1 TO NumMenus%
END IF "Check char length
EXIT FUNCTION
END IF ‘Check key
That’s it for the key reading part of MenuGetEvent$( ). So far, we’ve covered half the
function MenuGetEvent$( ). Let’s turn to the mouse monitoring part.
Here, let’s only work through the part of the code that deals with the left mouse
button, the one that works the menu system. The part of MenuGetEvent$( ) that handles
> Pull-down Menus’) 155
the right mouse button is pretty straightforward, and we'll put it into the full listing later;
there’s no use taking up pages covering it.
The left mouse button part itself can be broken up into two parts — was the left mouse
button pressed or released? First we can check if it’s been pressed. If it has been, we place
the mouse cursor’s screen row and screen column into the variables SCRow% and ScCol%
in preparation for returning them to the calling program (and because we'll use those
variables ourselves). To do that, we simply take the values returned in dx and cx, integer
divide by 8, and add 1.
Note that if SCRow% was 1 and the menubar was on (MenusOnFlag% = 1), then this
is a menuopen event (if the mouse cursor was lying on a menu name). In that case, we set
the return value in MenuNo%, open the menu on the screen and then exit. Otherwise, we
still have to report a mousedown event, and do so like this, where NumberTimes% is the
number of times the left button was pressed:
Button% = 0
InRegs.bx = 0
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
IF NumberTimes% \ O THEN
IF MenusOnFlag% AND (ScRow% = 1) THEN
MenubarCandidate% = ScCol% \ 10 + 1
IF MenubarCandidate% <= NumMenus% THEN
MenuNo% = MenubarCandidate%
ChoiceNoz = 0
MenuNowOpen% = MenubarCandidate%
CALL TurnOnMenu(MenubarCandidate%)
MenuGetEvent$S = "menuopen"
EXIT FUNCTION
END IF
ELSE
MenuGetEventS = "“mousedown"
EXIT FUNCTION
END IF
END IF
That’s all there is to handling left button presses (it’s only when you release the left
button that you make a menu choice). Let’s look at the left button released section of
MenuGetEvent$( ) next. Again, if the button has been released, we load ScRow% and
ScCol%.
156 P& Advanced BASIC
Now it gets a little complex. If the menu bar is on (MenusOnFlag% = 1) and a menu is
open (MenuNowOpen% is not 0), then we have to check if a menu event took place.
Otherwise, we just exit and report a mouseup event because the left button was released.
Here’s the left button released part of the code:
InRegs.bx = 0
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1
ELSE
MenuGetEvent$ = “mouseup"
EXIT FUNCTION
END IF ‘Menu open
END IF "NumberTimes% <> 0
To check for a menu event, we have to see if the mouse cursor was inside the menu
that was open when the left button was released. If it was out of range (in either rows or
columns), then the menu is supposed to close, and this is a menuclose event. Here’s how
that looks:
InRegs.bx 0
InRegs.ax Hof 6
CALL INTERRUPT(@H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \' 8 + 1
ELSE
MenuNo% = MenuNowOpen%
CALL TurnOffMenu(MenuNowO0pen%)
MenuNowOpen% = 0
MenuGetEvent$ = “menuclose"”
EXIT FUNCTION
END IF
ELSE
MenuNo% = MenuNowOpen%
CALL TurnOf fMenu(MenuNow0pen%)
MenuNowOpen% = 0
MenuGetEvent$ = “menuclose"”
EXIT FUNCTION
END IF
ELSE
MenuGetEvent$ = “mouseup"
EXIT FUNCTION
END IF ‘Menu open
END IF "NumberTimes% <> 0
This is the heart of the mouse part right here — the left button was released when the
mouse cursor was inside an open menu. Now we have to find out which choice was
made. That turns out to be easy; because the top row of each menu is 2, the choice that
was made, ChoiceNo%, is just SCRow% - 1 (see Figure 4-7). We report that choice and
close the menu:
InRegs.bx 0
InRegs.ax 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% OutRegs.cx \ 8 + 1
END IF
ELSE
MenuNo% = MenuNowOpen%
CALL TurnOf fMenu(MenuNowOpenz)
MenuNowOpenz = 0
MenuGetEvent$ = "menuclose”
EXIT FUNCTION
END IF
ELSE
MenuGetEvent$ = “mouseup”
EXIT FUNCTION
END IF ‘Menu open
END IF 'NumberTimes% <> 0
ScRow% = 1
ScRow% = 2 Apples ChoiceNo% = 1
ScRow% = 3 Bananas ChoiceNo% = 2
ScRow% = 4 Grapes ChoiceNo% = 3
Peaches 4
Oranges
Figure 4-7
This concludes the mouse part of MenuGetEvent$( ) — for the left mouse button. The
right mouse button is much easier to handle since it can only generate mousedown and
mouseup events. We'll leave that part up to the interested programmer. Listing 4-4 shows
the whole giant function.
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
‘
DECLARE SUB INTERRUPT (CIntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)
CONST MaxMenus = 7
> Pull-down Menus’ 159
MenuGetEvent$ = ""
bo
InChar$ = INKEYS
FOR i%
= 1 TO NumMenus%
IF AltKeyS = UCASES(MIDS(Bar$,10*(i%-1)+2,1)) THEN
IF MenuNowOpen% = i% THEN
MenuNo% = 1%
CALL TurnOffMenu(iz)
MenuNowOpenz = 0
MenuGetEvent$ = “menuclose”
EXIT FUNCTION
END IF
IF MenuNowOpen% <> 0 THEN CALL TurnOffMenu(MenuNowO0pen%)
MenuNo% = 1%
ChoiceNo% = 0
MenuNowOpen% = if
CALL TurnOnMenuliZz)
MenuGetEvent$ = “menuopen"
END IF
NEXT 1% ‘For i% = 1 TO NumMenus%
END IF "Check char Length
EXIT FUNCTION
END IF ‘Check key
Button% = 0
InRegs.bx = 0
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1
InRegs.bx 0
InRegs.ax "tou 6
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScColZ% = OutRegs.cx \ 8 + 1
Button% = 1
InRegs.bx = 1
InRegs.ax = 5
INTERRUPT(&H33, InRegs, OutRegs)
CALL
NumberTimes% = OutRegs.bx
ScRow% OutRegs.dx \ 8 + 1
ScCol% Hou OutRegs.cx \ 8 + 1
IF NumberTimes% \ 0 THEN
MenuGetEvent$ = “mousedown™
162 Pb Advanced BASIC
InRegs.bx = 1
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1
LOOP WHILE 1
END FUNCTION
CurRowZ CSRLIN
CurCol% hou POS(O)
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
rr = TopRow(MNumber%)
cc = TopCol (MNumber%)
rt = Rows (MNumberZ)
ct = Cols(MNumber%)
FOR i = 1 7T0 rt
temp1$ = un
temp2$ = ""
FOR j = 1 T0 ct
tempi1$ = temp1$ + CHRSCSCREEN(rr + 7 - 1, ce + j - 1))
temp2$ = temp2$ + CHRS(SCREEN(rr + Gi - 1p C0 © 9 = 4, 49)
NEXT j
OldText(MNumber%, i) = temp1$
OldAttrb(MNumber%, 1) = temp2$
NEXT j
TR TopRow(MNumber%)
sn Rou TopCol (MNumber%)
LOCATE TR, TC
InRegs.cx = 1
> Pull-down Menus’ 163
InRegs.ax = 1
CALL INTERRUPT(8&H33, InRegs, OutRegs)
END SUB
CurRow% = C SRLIN
CurCol% = POoSs(0)
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
TR TopRow(MNumber%)
TC = TopCol(MNumberZ)
LOCATE TR, TC
InRegs.cx = 1
InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)
END SUB
164 Pb Advanced BASIC
You can see that this is a monster. It even has its own two subprograms, TurnOn-
Menu( ) and TurnOffMenuC( ). Let’s put this to work, making our menu system active and
receiving menu input.
MenuNames(1) = "Fruits”
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = “Bananas”
MenuChoices(1, 3) = "Grapes"
MenuChoices(1, 4) = "Peaches"
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = "Veggies"
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) “Corn”
MenuChoices(2, 3) “Broccoli
MenuNames(3) = "Meats"
MenuChoices(3, 1) = "Chicken"
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) = "Beef"
MenuChoices(3, 4) = “Fish”
FOR. 4 = 4270 °3
MenuAttrbs(i) = &H61
NEXT 7
BarAttrb% = &H24
CALL MenuShow
> Pull-down Menus’ 165
Check% = MouselInitialize%
CALL MouseShowCursor
DO
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
LOOP UNTIL Check$ = "“menuchoice”
This example program takes a little explaining. We’re familiar with the way to set up
menus using Menulnitialize%( ) and how to call MenuShow( ) directly after that. How-
ever, you should notice that the menu system does not turn the mouse cursor on auto-
matically (because you may not want to use a mouse at all). Instead, we’re responsible for
doing that ourselves with Mouselnitialize%( ) and MouseShowCursor( ). After we do,
we're free to use the mouse with our menus:
CALL MenuShow
— Check% = MouselInitializez
CALL MouseShowCursor
The whole example program sets things up except for the meat of the last few lines:
CALL MenuShow
Check% = MouseInitialize%
CALL MouseShowCursor
> dO
ChoiceNoz, Button%, Row%, Col%)
Check$ = MenuGetEvent$(MenuNo%,
LOOP UNTIL Check$ = “menuchoice”
Here we call MenuGetEvent$( ) and keep calling it until it returns a menuchoice event.
In that case, we know that the selection made was choice ChoiceNo% of menu Menu-
No%. And we know what that was because we've initialized the menu system ourselves.
That choice corresponds to MenuChoices(MenuNo%, ChoiceNo%).
When you run this program, then, a menubar appears and it prompts you to make a
menu selection. When you do, using either the mouse or the keyboard, it prints out the
exact selection you’ve made (Peaches, Apples, or whatever).
Congratulations, you’re now using your own pull-down menus.
The second input function we'll work out is MenuCheckEvent$( ), which differs from
MenuGetEvent$( ) only in that it doesn’t wait for input.
After this line, InString$ will be set to one of the following values:
InString$ Means
“” (null string) No key, mouse event, or menu event was pending.
STRING of LEN 1 Single character typed. Read as you would from
INKEY$.
STRING of LEN 2. Character typed with an extended ASCII code. Read
as you would from INKEY$.
> Pull-down Menus’ 167
°
IF (a key was pressed) THEN
MenuGetEvent$ = key that was pressed
IF (menu system is on) THEN
IF (Esc was pressed) THEN
IF (menu was open) THEN
Eclose menu and EXIT]
ELSE
EXIT FUNCTION
END IF
END IF
IF C(ALt key matched a menu or choice name) THEN
ECset menu arguments and EXITI
ELSE
EXIT FUNCTION
END IF
END IF
EXIT FUNCTION
END IF
26
#6
96
Gf
28
eh
oo
Be
be
ae
ts
oe
Ge
se
ue
os
ce IF (a mouse button was used) THEN
IF Cit was the left mouse button) THEN
IF Cit was in a menu) THEN
{set menu arguments and EXITI
168 > Advanced BASIC
ELSE
Eset mouse arguments and EXIT]
END IF
ELSE "It was the right button
ECset mouse arguments and EXIT]
END IF
EXIT FUNCTION
: END IF
LOOP WHILE 1
Without this loop, we will no longer wait for input. The only other change is that every
occurrence of MenuGetEvent in this listing has been replaced with MenuCheckEvent.
That’s all there is to it. See Listing 4-5 for reference.
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
CONST MaxMenus = 7
DIM Rows(1 TO MaxMenus) AS INTEGER
DIM Cols(1 TO MaxMenus) AS INTEGER
DIM TopRow(1 TO MaxMenus) AS INTEGER
DIM TopCol(1 TO MaxMenus) AS INTEGER
DIM BotRow(1 TO MaxMenus) AS INTEGER
DIM BotCol(1 TO MaxMenus) AS INTEGER
DIM Attribute(1 TO MaxMenus) AS INTEGER
DIM Text(1 TO MaxMenus, 25) AS STRING
DIM OldText{1 TO MaxMenus, 25) AS STRING
DIM OLdAttrb(1 TO MaxMenus, 25) AS STRING
COMMON SHARED /MenuA/ Rows( ) AS INTEGER, Cols€) AS INTEGER,
TopRow(€ ) AS INTEGER, TopCol( ) AS INTEGER os
BotRow( ) AS INTEGER, BotCol() AS INTEGER
o am
MenuCheckEvent$ = ""
InChar$ = INKEYS
FOR i% = 1 TO NumMenus%
IF AltKey$ = UCASES(M1D$(Bar$,10*(i%-1)+2,1)) THEN
IF MenuNowOpen% = i% THEN
MenuNo% = if
CALL TurnOffMenuliz)
MenuNowOpen% = 0
MenuCheckEvent$ = “menuclose”
EXIT FUNCTION
170 P& Advanced BASIC
Button%Z = 0
InRegs.bx 0
InRegs.ax 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1
InRegs.bx = 0
InRegs.ax = 6
CALL INTERRUPT (&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 14
ScCol% = OutRegs.cx \ 8 + 4
Button% = 1
InRegs.bx = 1
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScColL% = OutRegs.cx \ 8 + 1
InRegs.bx 41
InRegs.ax 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1
CurRow% CSRLIN
CurCols% ou POS(0)
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
rr = TopRow(MNumberZ)
cc = TopCol (MNumber%)
rt = Rows(MNumberZ%)
ct = Cols(MNumberZ%)
FOR i = 170 rt
tempi$ = ""
temp2$ = ""
FOR j = 1 TO ct
tempi$ = temp1$ + CHRS(SCREEN(rr + i - 1, ce + j - 1))
temp2$ = temp2$ + CHRS(SCREEN(rr + i - 1, cc + S227 te
NEXT j
OldText(MNumberZ%, i) = temp1$
OldAttrb(MNumber%, i) = temp2$
NEXT i
TR = TopRow(MNumber%)
TC = TopCol (MNumber%)
LOCATE TR, TC
InRegs.cx = 1
InRegs.ax = 1
CALL INTERRUPT(8&H33, InRegs, OutRegs)
END SUB
InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)
TR TopRow(MNumber%)
TC fou TopCol (MNumber%)
LOCATE TR,. TC
InRegs.cx = 1
FOR i = 1 TO Rows(MNumber%)
ORS = OldText(MNumber%, 7)
OCS = OldAttrb(MNumber%, 7)
FOR j = 1 TO Cols(MNumber%)
InRegs.ax = &H900 + ASC(MIDSCORS, j, 1))
InRegs.bx = ASCC(MID$C(OC$, j, 1))
CALL INTERRUPTC&H10, InRegs, OutRegs)
LOCATE TR + i - 1, TE + j
NEXT j
LOCATE TR + i, TC
NEXT i
InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)
END SUB
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = "Bananas”
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = “Peaches”
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = "Veggies"
MenuChoices(2, 1) = "Peas”
MenuChoices(2, 2) = "Corn"
MenuChoices(2, 3) = "Broccoli"
MenuNames(3) = "Meats"
MenuChoices(3, 1) = "Chicken"
MenuChoices(3, 2) = "Pork"
MenuChoices(3, 3) = “Beef”
MenuChoices(3, 4) = “Fish”
FOR i = 1 T0 3
MenuAttrbs(i) = &H67
NEXT i
BarAttrb’ = &H24
IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.”
END IF
CALL MenuShow
Check% = MouseInitializez%
CALL MouseShowCursor
bo
Check$ = MenuCheckEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
LOOP UNTIL Check$ = “menuchoice”
In this case, this example program is almost identical to the example program for
MenuGetEvent$( ). We just loop over MenuCheckEvent$( ) here until we get an menu-
choice event, and then print it out.
There are a few more things we should do to make our menu system more complete.
For example, some some menu choices may be “toggle” items. For example, one menu
choice in a word processor might be Spellchecking; if this is toggled on, then the auto-
matic spelling of words is enabled. Let’s add this capability to our set of menu tools.
eS > Pull-down Menus’) 175
TS
Since this code is so short, you might want to include it directly in your code
instead of linking it in.
And that’s it. Listing 4-6 shows the whole subprogram. Note that we still had to include
the menu COMMONs (but only one line of code!)
CONST MaxMenus = 7
DIM Rows(1 TO MaxMenus) AS INTEGER
DIM Cols(1 TO MaxMenus) AS INTEGER
DIM TopRow(1 TO MaxMenus) AS INTEGER
DIM TopCol(1 TO MaxMenus) AS INTEGER
DIM BotRow(1 TO MaxMenus) AS INTEGER
176 & Advanced BASIC
eee
END SUB
Notice also that the choice is not immediately displayed on the screen as checked; that
is, MenuMarkChoice( ) does not display the menu with the new checkmark in it. That is
because toggling a choice on or off is itself a menuchoice event — when you toggle, say,
Spellchecking on, the menu closes, as it should. To see that it is actually toggled on, you
have to pull down that menu again. Now let’s see how to use MenuMarkChoice( ).
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = "Apples"
MenuChoices(1, 2) = “Bananas”
MenuChoices(1, 3) = "Grapes”
MenuChoices(1, 4) “Peaches”
ONS> Pull-down Menus’ 177
UE
MenuChoices(1, 5) = "Oranges"
MenuNames(2) = "Veggies"
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) = "Corn"
MenuChoices(2, 3) = "Broccoli"
MenuNames(3) = “Meats”
MenuChoices(3, 1) "Chicken"
MenuChoices(3, 2) = "Pork'
MenuChoices(3, 3) = “Beef”
MenuChoices(3, 4) = "Fish"
FOR i = 1 TO 3
MenuAttrbs(i) = &H61
NEXT ji
BarAttrb% = &H24
IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.”
END IF
CALL MenuShow
Check% = MouseInitialize%
CALL MouseShowCursor
DO
Check$ = MenuGetEventS(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
IF Check$S = “menuchoice” THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASES(Check$) = “Q*
Then we loop until a menu choice is made, and toggle that choice on with Menu-
MarkChoice( ) (until q or Q is pressed, to indicate that we should quit):
DO :
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
IF Check$ = "“menuchoice" THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASES$(Check$) = "“Q"
Since we now have a way of toggling menu choices on, the natural next step is to
toggle them off.
And all it does is make the first character in the line holding the indicated menu choice a
space, «
“”. Listing 4-7 shows the whole subprogram.
0
CONST MaxMenus = 7
DIM Rows(1 TO MaxMenus) AS INTEGER
DIM Cols(1 TO MaxMenus) AS INTEGER
DIM TopRow(1 TO MaxMenus) AS INTEGER
DIM TopCol(1 TO MaxMenus) AS INTEGER
DIM BotRow(1 TO MaxMenus) AS INTEGER
DIM BotCol(1 TO MaxMenus) AS INTEGER
DIM Attribute(1 TO MaxMenus) AS INTEGER
> Pull-down Menus’ 179
END SUB
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = “Bananas”
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = "Peaches"
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = "Veggies"
MenuChoices(2, 1) = "Peas"
2) = "Corn" :
MenuChoices(2,
MenuChoices(2, 3) = “Broccoli”
MenuNames(3) = “Meats”
MenuChoices(3, 1) "Chicken"
MenuChoices(3, 2) “Pork”
180
Ll >tice
Advanced
lahatBASIChn seer
MenuChoices(3, 3) = "Beef"
MenuChoices(3, 4) = “Fish”
FOR i = 1 TO 3
MenuAttrbs(i) = &H61
NEXT i
BarAttrb% = &H24
), MenuChoices(), BarAttrb%, _
Check% = Menulnitialize%(MenuNames(
MenuAttrbs())
IF Check% = O THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.”
END IF
CALL MenuShow
Check% = MouseInitializez
CALL MouseShowCursor
DO
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
IF Check$ = “menuchoice” THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%r)
LOOP UNTIL UCASES(Check$) = “Q" .
dO
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
IF Check$ = “menuchoice" THEN CALL MenuUnMarkChoice(MenuNo%, ChoiceNoxz)
LOOP UNTIL UCASES(Check$) = “Q"
This example program asks you to mark a few choices, as we've done in the previous
example program:
DO
Check$ = MenuGetEvent$(MenuNoz, ChoiceNo%, Button%, Row%, Col%)
IF Check$ = “menuchoice” THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASES(Check$) = "Q”
see>COW
Pull-down Menus
Menus’ 181
en
Then you press “q” or “Q” and are given the Opportunity to unmark them if you wish:
“ ”
60
Check$ = MenuGetEvent$(MenuNo%, ChoiceNox%, Button%, Row%, Col%)
IF Check$ = "menuchoice" THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASES(Check$) = “q"
oe DO
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%Z, Row%, Col%)
IF Check$ = “menuchoice" THEN CALL MenuUnMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASESC(Check$) = "Q"
But you may not have kept the original arrays with the menu and choice names around.
In that case, we should be able to interrogate our menu system to find out just what, say,
Menu 5, Choice 3 is. For that reason, our last menu function is named Menu-
ReadChoice$( ).
Checking Up on a Menu
If you want to see what a single menu choice was, e.g., the choice corresponding to
MenuNo% and ChoiceNo% returned by MenuGetEvent%( ), you can call Menu-
ReadChoice$(MenuNo%, ChoiceNo%).
Let’s add another feature to this function: if we pass a menu number of 0, we can make
MenuReadChoice%( ) return menu names from the menubar. For example, Menu-
ReadChoice$(0, 2) should return the name of the second menu in the menubar (see
Figure 4-8).
182 P Advanced BASIC
‘
[Menot [Menu
Choice 1
_MenuS [Menu
Choice 2
Choice 3
Choice 4
Choice 5
Choice 6
Figure 4-8
Here’s how we might use MenuReadChoice$( ):
IF MenuNo% = 0 THEN
MenuReadChoice$ = RTRIMSCMIDS(Bar$, (ChoiceNo%-1)*10 + 2, 8))
ee
> Pull-down Menus’ 183
We use the BASIC function RTRIM$( ) to trim off trailing spaces. Otherwise, we have to
report the name of a choice in a particular menu. Using both LTRIM$( ) and RTRIM$C ),
that looks like this:
IF MenuNo% = 0 THEN
oe MenuReadChoice$ = RTRIMSCMIDS(Bar$, (ChoiceNo%-1)*10 + 2, 8))
CONST MaxMenus = 7 oe
DIM Rows(1 TO MaxMenus) AS INTEGER—
DIM Cols(1 TO MaxMenus) AS INTEGER
DIM TopRow(1 TO MaxMenus) AS INTEGER
DIM TopCol(1 TO MaxMenus) AS INTEGER
DIM BotRow(1 TO MaxMenus) AS INTEGER
DIM BotCol(1 TO MaxMenus) AS INTEGER
DIM Attribute(1 TO MaxMenus) AS INTEGER
DIM Text(1 TO MaxMenus, 25) AS STRING
DIM OldText(1 TO MaxMenus, 25) AS STRING
DIM OldAttrb(1 TO MaxMenus, 25) AS STRING
COMMON SHARED /MenuA/ Rows( ) AS INTEGER, Cols( ) AS INTEGER, _
TopRow( ) AS INTEGER, TopCol( ) AS INTEGER, _
BotRow( ) AS INTEGER, BotCol( ) AS INTEGER
SHARED /MenuB/ Attribute( ) AS INTEGER, Text( ) AS STRING, _
COMMON
OldText( ) AS STRING, OldAttrb( ) AS STRING
COMMON SHARED /MenuC/ Bar$, BarAttrb, MenusOnFlagiz, NumMenus%, OldBar$,_
OldBarAttrb$
IF MenuNoz% = 0 THEN
= RTRIMS(MIDS(BarS, (ChoiceNo%-1)*10 + 2, 8))
MenuReadChoiceS
ELSE
MenuReadChoice$ u RTRIMSCLTR (MenuNo%,
Text IMS( ChoiceNoz)))
END IF
END FUNCTION
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = "Bananas"
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = “Peaches”
MenuChoices(1, 5) = "Oranges"
MenuNames(2) = “Veggies”
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) = “Corn”
MenuChoices(2, 3) = "Broccoli"
MenuNames(3) = “Meats”
MenuChoices(3, 1) = “Chicken”
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) = "Beef"
MenuChoices(3, 4) = "Fish"
FOR i = 1 TO 3
MenuAttrbs(i) = &H61
NEXT i
BarAttrb% = SH24
IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT "Menu is set up."
END IF
All this does is to set up a menu, tell you what menu 1, choice 1, is
and what the name of
menu 1 is.
And that’s it for our menu system. We've been able to set our menus
up, show or hide
the menubar, read menu/keyboard/mouse input, toggle menu
items on or off, and now
interrogate the menu system for particular choice names. We've
done a lot of work.
ee eeeeesesssss—‘(‘“CSCSP> Pull-down
Own Menus’
Menus 185
185
If you have the BASIC PDS, however, you might want to use the built-in menu
system
there. Let’s take a look at what it has to offer.
|NOTE: | Even if you don’t have the PDS, it’s worth reading this section to see what’s
available.
Note that if you're using QBX.EXE, you'll have to load the Quick library we made in
Chapter 2, UIASM.QLB. If we were to call our demo program MENUTEST.BAS, for
example, we'd start QBX.EXE like this:
QuickBASIC (QB or QBX) gives you the ability to load a number of files at once
using .MAK files. To produce a .MAK file, load all the appropriate modules and
select the save option in the file menu. The next time you load the main mod-
ule, all the others will be loaded automatically.
On the other hand, if you’re using BC.EXE, you'll have to include the library we made,
UIASM.LIB, in your list of libraries to link.
Now let’s see how the PDS menu system works. In the beginning of our program, we
have to include the correct .BI files:
186 » Advanced BASIC
S$INCLUDE: 'C:\BC7\GENERAL.BI'
SINCLUDE: 'C:\BC7\MOUSE.BI"
SINCLUDE: 'C:\BC7\MENU.BI'
SINCLUDE: 'C:\BC7\WINDOW.BI'
.
s
*
*
* $INCLUDE: 'C:\BC7\GENERAL.BI’
" SINCLUDE: 'C:\BC7\MOUSE.BI'
" SINCLUDE: 'C:\BC7\MENU.BI"
* SINCLUDE: 'C:\BC7\WINDOW.B1'
To make our example correspond to the one we’ve already developed for our function
MenuGetEvent$( ), we can dimension the menu and choice name arrays like this:
* SINCLUDE: ‘C:\BC7\GENERAL.BI'
* SINCLUDE: 'C:\BC7\MOUSE.BI‘
" SINCLUDE: ‘C:\BC7\MENU.BI'
* SINCLUDE: "C:\BC7\WINDOW.BI'
SINCLUDE: "C:\BC7\GENERAL.BI'
SINCLUDE: "C:\BC7\MOUSE.BL'
SINCLUDE: "C:\BC7\MENU.BI!
SINCLUDE: "C:\BC7\WINDOW.BI!
MenuNames(1) = "Fruits" e
MenuChoices(1, 1) = “Apples" :
MenuChoices(1, 2) = "Bananas" “
MenuChoices(1, 3) = "Grapes"
MenuChoices(1, 4) = “Peaches”
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = “Veggies”
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) = “Corn”
MenuChoices(2, 3) = "Broccoli"
MenuNames(3) = "Meats" .
MenuChoices(3, 1) = "Chicken”
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) = "Beef"
MenuChoices(3, 4) = "Fish"
FOR i = 1 T0 3
MenuAttrbs(i) = &H61
NEXT i
BarAttrbz = &H24
Now we're ready to use the PDS menu system. We start by initializing that system with
a call to the PDS subprogram Menulnit( ), and then we can load the menu and choice
names with MenuSet( ).
You use MenuSet( ) like this:
* SINCLUDE: ‘'C:\BC7\GEERAL.BI'
* SINCLUDE: "C:\BC7\MOUSE.BI'
" SINCLUDE: ‘'C:\BC7\MENU.BI'
" SINCLUDE: ‘C:\BC7\WINDOW.BI'
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = “Apples"
MenuChoices(1, 2) = "Bananas"
MenuChoices(1, 3) = "Grapes"
MenuChoices(1, 4) = “Peaches”
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = “Veggies”
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) = “Corn”
MenuChoices(2, 3) = “Broccoli”
MenuNames(3) = “Meats”
MenuChoices(3, 1) = "Chicken"
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) = "Beef"
MenuChoices(3, 4) = “Fish”
re > Pull-down Menus 189
IOUS: -1B8
FOR i = 1 T0 3
MenuAttrbs(i) = &H61
NEXT ji
BarAttrb% = &H24
CALL MenulInit
ref
os FOR i% = 1 TO 3
CALL MenuSet(iz%, 0, 1, MenuNames(i%), 1)
FOR j% = 1 T0 5
IF MenuChoices(iz, j%) <> "" THEN
CALL MenuSet(iz, 4%, 1, MenuChoices(iz, j%), 1)
END IF
NEXT j%
NEXT iz
Then we can call the PDS subprogram MenuColor( ) to set the color scheme, like this:
These are the inputs (see the discussion of attributes in Chapter 2 for more informa-
tion about which colors are associated with which values):
Fore% Menu foreground color (0-15)
Back% Menu background color (0-7)
High% Highlight color of the access key (0-15)
Disab% Text color of disabled choices (0-15)
Curfore% Menu cursor foreground color (0-15)
Curback% Menu cursor background color (0-7)
CurHi% Menu cursor color when over the access key
(0-15)
After MenuColor( ), the next step is to call MenuPreProcess( ) to build the internal
PDS indices needed to run the menus:
SINCLUDE: 'C:\BC7\GENERAL.BI'
SINCLUDE: 'C:\BC7\MOUSE.BI'
SINCLUDE: ‘'C:\BC7\MENU.BI'
SINCLUDE: 'C:\BC7\WINDOW.BI'
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = “Bananas”
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = "Peaches"
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = "Veggies"
MenuChoices(2, 1) = “Peas”
MenuChoices(2, 2) "Corn"
MenuChoices(2, 3) “Broccoli”
MenuNames(3) = "Meats"
MenuChoices(3, 1) "Chicken"
MenuChoices(3, 2) = “Pork"
MenuChoices(3, 3) = “Beef”
MenuChoices(3, 4) = “Fish”
FOR 1 = 1 TO 3
MenuAttrbs(i) = &H61
NEXT ji
BarAttrb% = &H24
CALL MenulInit
FOR 1% = 1 T0 3
CALL MenuSet(iz, 0, 1, MenuNames(i%), 1)
FOR j% = 110 5
IF MenuChoices(iz, j%) <> “" THEN
CALL MenuSetCiz, j%, 1, MenuChoices(iz, 522,-1)
END IF
NEXT j%
NEXT 12
CALL MenuPreProcess
Now it’s time to actually display the menubar. We can use MenuShow( ) to
do that,
MouseShow( ) to display the mouse cursor, and then print out the prompt “Make a menu
selection:”
SINCLUDE: 'C:\BC7\GENERAL.BI' ;
SINCLUDE: 'C:\BC7\MOUSE.BI*
SINCLUDE: 'C:\BC7\MENU.BI' :
SINCLUDE: 'C:\BC7\WINDOW.BI'
> Pull-down Menus’ 191
MenuNames(1) = "Fruits"
MenuChoices(1, 1) = "Apples"
MenuChoices(1, 2) = "Bananas"
MenuChoices(1, 3) = "Grapes"
MenuChoices(1, 4) = “Peaches”
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = “Veggies”
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) “Corn”
MenuChoices(2, 3) "ou “Broccoli”
MenuNames(3) = “Meats”
MenuChoices(3, 1) = “Chicken”
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) = “Beef”
MenuChoices(3, 4) = "Fish"
FoR i= 1 TO 3
MenuAttrbs(i) = &H61
NEXT i
BarAttrb% = &H24
CALL MenulInit
FOR 4% = 1 1053
CALL MenuSet(ix, 0, 1, MenuNames(i2), a)
FOR 3% -= 1°705
IFMenuChoices(iz%, j%) <> "" THEN
j%, 1, MenuChoices(iz, j%), 1)
CALL MenuSet(i%,
END IF
‘NEXT j%
NEXT i%
CALL MenuPreProcess
~ CALL MenuShow
CALL MouseShow
LOCATE 2, 1
PRINT "Make a menu selection.”
ae
os
192 b Advanced BASIC
Now the menubar is on the screen; we're ready to receive input. The BASIC PDS
system uses a method similar to the one we've used for our menu system to read input:
there is a function named MenulInkey$() that operates much like our MenuCheck-
Event$( ). To get menu or keyboard (but not mouse) input, we loop over Menulnkey$( )
until it returns a menu string. Then we have to interrogate the menu system to find out
what choice was actually made:
MenuNames(1) = "Fruits"
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) “Bananas“
MenuChoices(1, 3) “Grapes”
MenuChoices(1, 4) “Peaches”
MenuChoices(1, 5) W
oH
Hou“Oranges”
MenuNames(2) = “Veggies”
MenuChoices(2, 1) i= "Peas"
MenuChoices(2, 2) “Corn”
MenuChoices(2, 3) HUW "Broccoli"
MenuNames(3) = “Meats”
MenuChoices(3, 1) “Chicken”
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) "Beef"
MenuChoices(3, 4) "Fish"
FOR i = 1 TO 3
MenuAttrbs(i) = &H61
NEXT i
BarAttrb% = &H24
CALL Menultnit
FOR i% = 1 TO 3
CALL MenuSet(iz, 0, 1, MenuNames(iZ%), 1) 1
FOR j% = 1 TO 5
IF MenuChoices(ix, j%) <> "" THEN
CALL MenuSet(iz, j%, 1, MenuChoices(iz, jx), 1)
> Pull-down Menus 193
END IF
NEXT j%
NEXT i2%
CALL MenuPreProcess
CALL MenuShow
CALL MouseShow
LOCATE 2, 1
PRINT “Make a menu selection.”
> DO
Instring$S = MenulInkey$
LOOP WHILE Instring$ <> “menu"
.=
When we leave this loop, a menu selection has been made; the PDS function Menu-
Check(0) will return the number of the menu that the selection was made in, and Menu-
Check(1) will return the choice number in that menu. This means that we'll be able to
print out the selection made as shown in Listing 4-9.
MenuNames(1) = “Fruits”
MenuChoices(1, 1) = “Apples”
MenuChoices(1, 2) = “Bananas
MenuChoices(1, 3) = “Grapes”
MenuChoices(1, 4) = "Peaches"
MenuChoices(1, 5) = “Oranges”
MenuNames(2) = “Veggies”
MenuChoices(2, 1) = "Peas"
MenuChoices(2, 2) = “Corn”
194 Pb Advanced BASIC
MenuNames(3) = “Meats”
MenuChoices(3, 1) = "Chicken"
MenuChoices(3, 2) = “Pork”
MenuChoices(3, 3) = "Beef”
MenuChoices(3, 4) = "Fish"
FOR i = 1 TO 3
MenuAttrbs(i) = &H61
NEXT i
BarAttrb% = &H24
CALL MenulInit
FOR 1% = 1 T0 3
CALL MenuSet(iz, 0, 1, MenuNames(i%), 1)
FOR j% = 1 T0 5
IF MenuChoices(izZ, j%) <> “" THEN
CALL MenuSet(iz, j%, 1, MenuChoices(iz, j%), 1)
END IF
NEXT 4%
NEXT i%
CALL MenuShow —
CALL MouseShow t
LOCATE 2, 1
PRINT “Make a menu selection."
bo
Instring$ = Menulnkey$
LOOP WHILE Instring$ <> “menu”
MenuNo% = MenuCheck(0)
ChoiceNo% = MenuCheck(1)
ay
oe
LOCATE 3, 1
PRINT “That selection was ", MenuChoices(MenuNo%, ChoiceNo%)
And that’s it. At this point, we've made a menu selection, and our example program
has deciphered what it was and reported it to us. You can see that, fundamen
tally, the
PDS menu system is very similar to the one we’ve developed. The primary
restriction is
that you have to use it to read all input to your program. In practice,
however, it usually
works quite well.
> Pull-down Menus 195
Conclusion
That’s the end of the menu system we'll develop, and the end of our survey of the PDS
menu tools. With these systems, you can add a powerful set of pull-down menus to your
programs with a few simple calls.
But we have much more power to add to your programs; as a case in point, let’s turn to
Chapter 5 right now and see what we can do with graphics.
Graphics
197
i ee
adiriasin)
> Graphics 199
IN THIS CHAPTER we're going to examine the graphics resources in BASIC, and we'll
see just how advanced we can get. As it turns out, BASIC is thoroughly stocked with
graphics tools, enough for us to put together our own fully functional paint program
(which is even going to save and load images to and from disk). We'll be able to draw
circles, boxes, lines — even color them in. After the paint program, we'll spend a little
time with the DRAW statement to produce some line graphics, and then we'll plunge into
animation.
Animation is a hot graphics topic that is supported very well in BASIC. Computer
animation revolves around the use of screen sprites, graphic images that you can move
around and display rapidly on the screen. We'll design our own sprite — a small, colorful
rocketship — and we'll also animate it, sending it blasting up the screen. After animation,
we'll look into the PDS’ Presentation Graphics tools, drawing pie, bar, and line charts
with ease.
|NOTE: | If you do any business graphics, be sure to read about presentation graphics.
We'll end the chapter with a couple of low-level functions to determine what video card
is in the computer you're working with, and what the current horizontal and vertical pixel
ranges on the screen are. This exploration of the graphics environment can give your
programs the power they need to make sure they’re using the resources available fully.
Graphics is an exciting topic — and this is going to be an exciting chapter. Let’s start
with our paint program.
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
200 & Advanced BASIC
bp — AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
DIM InRegs AS RegType, OutRegs AS RegType
ry
os
*
Our paint program has to handle many different types of actions: drawing pixels on
the screen, drawing lines, boxes, circles, and even saving or loading images on disk. For
that reason, organization is going to be important here, and we should make the main
body of the program as simple as we can, breaking the rest of the code up into
subroutines.
We choose the subroutine structure (i.e., GOSUB...RETURN) here so that all variables
will be shared, and there’s no advantage to using subprograms in this case. For example,
we can start this way:
TYPE RegTtype :
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
PS In early versions of BASIC, all variables used to be shared (that is, global) and
there were no functions or subprograms — only subroutines (which you use
with GOSUB). Now, however, you can use functions and subprograms, mak-
ing your variables private (that is, local). To share variables between them, you
can use a SHARED COMMON.
eee
In the Initialize subroutine, we can set the screen mode, print the menubar, and
initialize the mouse. When the program actually starts, the first thing we'll do is to wait
for the left mouse button to be pressed — you make all menu selections with the left
mouse button, or begin drawing with the left mouse button. We can set up a perpetual
loop, waiting for the left mouse button to be pressed:
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
GOSUB Initialize
— DO
GOSUB GetLeftButtonPress
LOOP WHILE 1
END
We can set up the subroutine named GetLeftButtonPress to wait until the left button
has been pressed. After that button has been pressed, we’ll want to know is whether a
menu selection was made; let’s design GetLeftButtonPress to set a variable named Men-
uSelectionMade% to 1, if the mousedown occurred in the menubar, and 0, if otherwise.
This will allow us to check for a menu selection this way:
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
=
(GOSUB Initialize
If a menu selection was made, then we can handle it in a new subroutine we can name
MenuChoice. There, we'll have to toggle internal flags to turn various drawing actions on
or off. For example, if the user selected the Line option, then we can toggle a flag called
LineFlag% to 1 in MenuChoice. With that option selected, we'll be ready to draw lines
the next time the left mouse button goes down.
If the button wasn‘ pressed in row 1, MenuSelectionMade% will not be set — maybe
we're supposed to start a drawing action, such as drawing lines or boxes. We can check
what we're supposed to do (if anything) by checking the flags we set in MenuChoice, like
this:
> Graphics 203
END IF
LOOP WHILE 1
END
As you can see, the main procedure of PAINT.BAS has been made as modu-
lar as possible. The result is that it is easy to understand and easy to debug.
Dividing the overall basics into components like that is called top-down pro-
gramming, and most professional programmers work this way.
In other words, the action goes like this: the user can start drawing lines by selecting
Line in the menubar. When a menu selection like that is made, we go to the subroutine
MenuChoice and toggle the correct flag, LineFlag% to 1.
Then the user releases the mouse button. Internally, we return from MenuChoice, loop
back to the top of our main loop, and wait for the next left button press in GetLeftBut-
tonPress. The user moves the mouse to the drawing area below the menubar and presses
the left mouse button. Internally, we return from GetLeftButtonPress with MenuSelec-
tionMade% not set this time, so we check which flag is set (LineFlag%) and enter the
correct subroutine (DrawLine). We stay in that subroutine, drawing the line, until the
user releases the button.
From the user’s point of view, the line appears with one end anchored where they've
pressed the mouse button. The user can move the mouse cursor around the screen,
pulling their end of the line as they require. When they release the button, the line
becomes fixed (and we return from DrawLine to wait for the next left button press, which
might be either a new line or a new menu selection).
That’s all there is to it from a programming standpoint. The main procedure in our
program is complete (note that we've added an array called Storage( ) above to hold the
screen image for disk transfers). All that remains is to write the subroutines.
Subroutine Initialize
In the initialization subroutine, we have to set the screen mode and print the menubar.
We can do that like this:
204 P& Advanced BASIC
Initialize:
SCREEN 8
LOCATE 1, 1
Fore% = 1
Back% = 2
COLOR Forex, Back%
PRINT “Exit Color Bkgrnd Draw Lane cts
"Box Circle Paint Save Load";
We also have to initialize the mouse and show the mouse cursor:
Initialize:
SCREEN 8
LOCATE 1, 1
Fore% = 1
Back% = 2
COLOR Fore%, Back%
PRINT “Exit Color Bkgrnd Draw Eine 7 4
“Box Circle Paint Save Load”;
Finally, we can set all our drawing flags to 0, and we're done:
eee & Graphics 205
Initialize:
SCREEN 8
LOCATE 4,4
Forex = 1
Back% = 2
COLOR Forex, Back%
PRINT “Exit Color Bkgrnd Draw Line " +
“Box Circle Paint Save Load"; 2
Subroutine GetLeftButtonPress
This subroutine is easy: All we have to do is to wait for the left mouse button to be
pressed, which we can check with interrupt @H33 service 5 like this:
GetLeftButtonPress:
DO
InRegs.bx = 0 ‘Wait for left button press
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
*
We have to set the variable MenuSelectionMade% to report whether or not this was a
menubar event. We can do that by simply checking whether the mouse cursor was in row
1, the menubar row:
GetLeftButtonPress:
BDO
InRegs.bx = 0 "Wait for left button press
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)
206 P Advanced BASIC
Subroutine MenuChoice
In the MenuChoice subroutine, we can keep track of what menu choice or drawing
option was selected. The menubar looks like Figure 5-1.
Exit Color Bkgrnd Draw Line Box Circle Paint Save Load
Figure 5-1
Let’s work through the options one by one. First, we have to reset all the drawing flags
and figure out what choice was made; we can do that like this (each menu choice takes
up eight screen columns, and each column is eight pixels wide):
MenuChoice:
DrawFlag% 0
LineFlag% it 0
BoxFlag% = 0
CircleFlag% = 0
PaintFlag% = 0
Choice% = OutRegs.cx \ 64 + 1
Next, we can do what is required by the menu choice made with a SELECT CASE
statement. For example, for choice 1, Exit, we just end the program:
MenuChoice:
DrawFlag% 0
LineFlag% oi
it 0
BoxFlag% = 0
CircleFlag% = 0
PaintFlag% = 0
Choice% = OutRegs.cx \ 64 + 1
> SELECT CASE Choice%
CASE 1
END
Oe
a a ee a > Graphics
il | aad 207
ela
Choice 2 lets us set the foreground color; each time the user clicks on this selection, we
should increment the foreground color through the 16 options available in this mode. We
can indicate those colors by reprinting the word Color (which the user is clicking on) in
the new foreground color. Note that we turn the mouse cursor off and on before working
with the screen:
MenuChoice:
DrawFlag% = 0
LinefFlag% = 0
BoxFlag% = 0
CirclefFlagx = 0
PaintFlag% = 0
Choicex = OutRegs.cx \ 64 + 1
SELECT CASE Choice%
CASE 1
END
CASE 2
Fores’ = Fore% + 1
cde
“eo IF Forex > 15 THEN Fore% = 0
COLOR Fore%, Back%
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOCATE 1, 9
PRINT “Color ";
InRegs.ax = 1 ‘Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
The next option changes the background color, which we can do with the BASIC
COLOR statement. Each time we change the background color, the background of the
whole screen changes, so the user can easily make a selection:
MenuChoice:
DrawFlag% 6
LineFlag% Nod 0
BoxFlag% = 0
Circleflag% = 0
PaintFlag% = 0
Choice% = OutRegs.cx \ 64 + 1
SELECT CASE Choice%
CASE 1
END
CASE 2
Fore% = Fore% + 1
If Fore’ > 15 THEN Fore% = 0
COLOR Fore%’, BackzZ
InRegs.ax = 2 "Hide mouse cursor
208 & Advanced BASIC
Now we come to the drawing options in the menubar (see Figure 5-2). Each of these
options are going to be handled in their own subroutines, but we have to toggle the
appropriate flags here. That looks like this:
A J L L i}
Exit Color Bkgrnd Draw Line Box Circle Paint Save Load
Figure 5-2
MenuChoice:
DrawFlag% 0
LineFlag% ft
ott 9
BoxFlag% = 0
CirclefFlagz% = 0
PaintFlag% = 0
Choicez = OutRegs.cx \ 64 + 1
SELECT CASE Choice%
CASE 1
END
CASE 2
Fore% = Fore% + 1
IF Fores > 15 THEN Fore% = 0
COLOR Forez, Back%
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOCATE 1, 9
PRINT "Color “;
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPTC(&H33, InRegs, OutRegs)
CASE 3
Backz = Backx + 1
IF Back% > 7 THEN BackZ% = 0
COLOR Fore%, Back%
CASE 4
sa ; DrawFlag% = 1
CASE 5
LineFlag% = 1
CASE 6
BoxFlag% = 1
CASE 7
CirclefFlag% = 1
CASE 8
PaintFlag% = 1
> Graphics 209
The following option, Save, lets us save the image to the disk. In particular, we can
save it as a file named PAINT.DAT. Since we should save the file when the option is
selected, we should handle this option here in MenuChoice.
|NOTE: | It would be more professional to allow the user to select the name of the disk
file.
To save the screen image, we can use the BASIC GET statement to load it into an array.
We'll need to set up an array, and we can use an array of integers named, say, Storage( ).
We want to save the drawing area of the screen, whose top left corner will be (X1, Y1) =
(0, 8) (omitting the menubar) and whose bottom left corner is (X2, Y2) = (639, 199). The
number of bytes needed to store this image is given by the formula:
Bytes = 4 + INT(((X2-X1+1)*(Bits/pixel/plane)+7)/8)*planes*((Y2-Y1)+1)
where Bits/pixel/plane is the number of bits per pixel per plane and planes is the number
of planes in the present screen mode. To determine these values for a specific screen
mode, see the BASIC documentation (or look at our subroutine SaveSprite coming up
later). For mode 8 and the size of the image we want to save, we'll need 62,532 bytes, or
31,266 integers.
At this point, all we need to do is to use GET like this:
This will load the screen image into the array Storage( ). (Note that we should turn the
mouse cursor off first to avoid saving it too.)
Next, we can dump the array Storage( ) directly to a file on disk using BSAVE, BASIC’s
binary save routine. To do that, we have to give BSAVE the address of the data we want to
save, and the number of bytes to save.
As you may already know, addresses in memory are made up of two words, a segment
address and an offset address. Briefly, a segment is a 64K section of memory, and an offset
is the location of a particular byte in that segment, as measured from the beginning of the
segment (see Figure 5-3).
Segment Begins:
64K
| Offset
Segment Ends:
Figure 5-3
We will work with actual segment and offset values when we cover memory addressing
thoroughly in Chapter 10. All we have to know now is that we have to supply BSAVE
with both the segment and offset address of the array Storage( ). To get the segment
address, BASIC provides the VARSEG( ) function. To get the offset in that segment,
BASIC provides VARPTR( ).
We start by setting the segment address BASIC will use equal to the segment address of
Storage( ) this way:
where Storage(1) is the first element in the array. Next, we just pass the name of the file
we want to create on disk, PAINT.DAT, followed by the offset address of the first byte to
write, which is VARPTR(Storage(1)), and the number of bytes to write, 62,532:
At the end, we reset the segment address BASIC will use for data
back to its original
value with DEF SEG (no argument). Here’s how it looks:
MenuChoice:
DrawFlag% = Q
LineFlagX% = 0
BoxFlagz = 0
CircleFlag% = 0
PaintFlag% = 0
Choice% = OutRegs.cx \ 64 + 1
SELECT CASE Choice%
CASE 1
END
CASE 2
Forex’ = Fore% + 1
IF Fore% > 15 THEN Fore% = 0
COLOR Fore%, Back%
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPTC(&H33, InRegs, OutRegs)
LOCATE 1, 9
PRINT “Cotor ";
InRegs.ax = 1 ‘Show mouse cursor :
CALL INTERRUPT(&H33, InRegs, OutRegs)
CASE 3
Back% = Back% + 1
IF Back% > 7 THEN Back% = 0
COLOR Fore%, Back%
CASE 4
DrawFflag% = 1
CASE 5
LinefFlag% = 1
CASE 6
BoxFlag% = 1
CASE 7
CircleFlag% = 1
CASE 8
PaintFlag% = 1
CASE 9
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPTC(&H33, InRegs, OutRegs)
GET (0, 8)-(639, 199), Storage
DEF SEG = VARSEG(Storage(1))
~~ BSAVE "PAINT.DAT", VARPTR(Storage(1)), 62532
DEF SEG
InRegs.ax = 1 ‘Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
And that’s it — we've been able to use BSAVE to save a graphics image on the disk.
The last option, Load, lets us read the image from the file and place it on the screen
again. We can do that with BLOAD, BASIC’s binary load routine, and PUT. Again, we
have to supply BLOAD with the address of the beginning of Storage( ).
212 & Advanced BASIC
We start by hiding the mouse cursor and loading PAINT.DAT back into memory like
this:
MenuChoice:
DrawFlag% = 0
LineFlag% = 0
Boxflagz = 0
Circleflagz = 0
PaintFlagz% = 0
Choicex = OutRegs.cx \ 64 + 1
SELECT CASE Choice%
CASE 1
END
CASE 2
Fore% = Fore% + 1
IF Fore% > 15 THEN Forex% = 0
COLOR Forex, Back%
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPTC&H33, InRegs, OutRegs)
LOCATE 1, 9
PRINT "Color “;
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPTC8H33, InRegs, OutRegs)
CASE 3
Back% = Back% + 1
IF Back% > 7 THEN Back% = 0
COLOR Fore%, Back%
CASE 4
DrawFlag% = 1
CASE 5
LineFlag% = 1
CASE 6
BoxFlag% = 1
CASE 7
CircleFlag% = 1
CASE 8
PaintFlag% = 1
CASE 9
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
GET (0, 8)-(639, 199), Storage
DEF SEG = VARSEG(Storage(1))
BSAVE "PAINT.DAT", VARPTR(Storage(1)), 62532
DEF SEG :
InRegs.ax = 1 *Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
CASE 10
InRegs.ax = 2 'Hide mouse cursor
CALL INTERRUPTC&H33, InRegs, OutRegs)
DEF SEG = VARSEG(Storage(1))
> BLOAD "“PAINT.DAT", VARPTR(Storage(1))
> Graphics 213
Next, we can place the image back on the screen, with the BASIC PUT statement, like
this: PUT (0, 8), Storage, PSET. The GET and PUT statements transfer data to and from
the screen rapidly, and both are cornerstones of graphics routines in BASIC. The PSET
option here indicates that each bit that we’re putting on the screen should overwrite
what's already there. The default, if you do not specify any option such as PSET, is XOR
— the image is XOR’d with whatever is already there. We'll use that option when working
with screen sprites.
XOR has a special property: If you XOR A with B, and then XOR the result with
B again, you'll get A back. This is why screen cursors are often XORed with
screen characters — when you XOR the character with the cursor again, the
original character reappears and the cursor can be moved elsewhere.
Finally, we just restore the mouse cursor, and we’re done. Here’s the whole subroutine
MenuChoice:
MenuChoice:
DrawFlag% 0
LineFlag% 0
BoxFlag% = 0
CircleFlag% = 0
PaintFlag% = 0
Choice% = OutRegs.cx \ 64 + 1
SELECT CASE Choicez
CASE 7
END
CASE 2
Fore% = Fore% + 1
IF Fore% > 15 THEN Fore’ = 0
COLOR Fore%, Back%
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOCATE 1, 9
PRINT "Color "; ;
InRegs.ax = 1 'Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
CASE 3
Back% = Back% + 1
IF Back% > 7 THEN Backs = 0
COLOR Fore%, Backs
214 P Advanced BASIC
CASE 4
DrawFlag% = 1
CASE 5
LineFlag% = 1
CASE
BoxFlag% = 1
CASE
CircleFlag% = 1
CASE
PaintFlagz = 1
CASE ee
GO
o
N
Subroutine DrawPixel
So far, we've handled everything but the drawing subroutines in our main procedure:
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
GOSUB Initialize
DO ;
GOSUB GetLeftButtonPress
IF MenuSelectionMade% THEN
Graphics 215
GOSUB MenuChoice
ELSE
> If DrawFlag% THEN GOSUB DrawPixel
: IF LineFlag% THEN GOSUB DrawLine
IF BoxFlag% THEN GOSUB DrawBox
IF CircleFlag% THEN GOSUB DrawCircle
IF PaintFlag% THEN GOSUB DrawPaint
LOOP WHILE 1
END
Let’s handle them now. The first one is DrawPixel. When we enter this subroutine,
we're supposed to keep setting the pixel at the current mouse cursor position until the
mouse button is released. This way, the user will be able to draw by pressing the left
mouse button and moving the mouse cursor around the screen.
We should start by clearing the mouse button release queue so we’re not responding to
a button release that occurred some time ago:
DrawPixel: :
InRegs.bx = 0 "Get teft button releases to clear queue
InRegs.ax = 6 : . :
CALL INTERRUPT(&H33, InRegs, OutRegs)
Now we should keep looping until the mouse button is released (by checking interrupt
&H33 service 6):
DrawPixel:
InRegs.bx = 0 "Get Left button releases to clear queue
InRegs.ax = 6 : ) :
CALL INTERRUPT(&H33, InRegs, OutRegs)
~ pO
RETURN
Every time through this loop while we're waiting for the mouse button to be released, we
have to get the mouse cursor’s current position and set the pixel there to the foreground
216 & Advanced BASIC
color. We get the current position with interrupt Q@H33 service 3, hide the mouse cursor
with service 2, set the pixel with a BASIC PSET( ) statement, and then show the mouse
cursor again:
DrawPixel:
InRegs.bx = 0 "Get left button releases to clear queue
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
dO
InRegs.ax = 3
CALL INTERRUPT(&H33, InRegs, OutRegs)
X% = OutRegs.cx
Y% = OutRegs.dx
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPTC&H33, InRegs, OutRegs)
PSET (X%, YX)
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = 0 "Left Button Releases
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
RETURN
And that’s all there is to it. The real work is done by PSET(X%, Y%), which sets the
pixel at the current location to the foreground color. We've finished the first of the
drawing subroutines.
Subroutine DrawLine
The next subroutine is DrawLine:
TYPE Regtype
ax INTEGER
bx INTEGER
cx INTEGER
dx INTEGER
bp INTEGER
si INTEGER
di INTEGER
flags INTEGER
END TYPE
GOSUB Initialize
> Graphics 217
DO
GOSUB GetLeftButtonPress
IF MenuSelectionMadeX% THEN
GOSUB MenuChoice
ELSE
IF DrawFlagX% THEN GOSUB DrawPixel
> IF LineFlag% THEN GOSUB DrawLine
IF BoxFlag% THEN GOSUB DrawBox
IF CircleFlag% THEN GOSUB DrawCircle
IF PaintFlag% THEN GOSUB DrawPaint
END IF
LOOP WHILE 1
END
We can handle this task with the BASIC LINE statement. You use it like this to draw a
line from screen point (X1, Y1) to (X2, Y2):
Here, Color% is an optional argument that specifies the line’s color. We'll make use of this
argument to both draw and erase lines. When we arrive in DrawLine, the user has already
clicked the mouse in the drawing area to establish the “anchor” point of one end of the
line. Since all variables are global between subroutines, we can read that location from
OutRegs.cx and OutRegs.dx, which are still with the location the user clicked at (in
GetLeftButtonPress):
DrawLine:
X% = OutRegs.cx
Y% = OutRegs.dx
.
This provides us with the anchor point of the line, (X%, Y%). When the user moves
the mouse cursor to a new location (XNew%, YNew%), we’ll draw:a line from (X%, Y%)
to (XNew%, YNew%); see Figure 5-4.
x Xx
(X%, Y%) (XNew%, YNew%)
Figure 5-4
218 & Advanced BASIC
When the user moves to a new position after this, we'll set XNew% and YNew% to
XOld% and YOld%, and erase that line by drawing over it in the background color (see
Figure 5-5).
Xx Xx
(X%, Y%) (XOld%, YOId%)
Figure 5-5
And draw a new one to the new (XNew%, YNew%) (see Figure 5-6).
Xx Xx
(X%, Y%) (XNew%, YNew%)
Figure 5-6
In this way, one end of the line will always be anchored at the original click position,
and the other will follow the mouse cursor around. When the user releases the left
button, we’ll make the line permanent. To do this, we first clear the left mouse button
release queue, and loop, waiting for the left button to be released:
DrawLine: :
X% = OutRegs.cx |
Y% = OutRegs.dx
— KOLdK = X%
YOtd% = Y%
DO
Each time we loop, we have to check the new mouse cursor position, erasing
the old
line and drawing the new one (after hiding the mouse cursor). That looks
like this:
DrawLine:
: X% = OutRegs.cx
Y% = OutRegs.dx
XOLdZ% = XX
YOLd% = Y%
Sasa
erersseeneeeamemsnrinmeecentiireenipemests
comenuirrarniesseeaces a > EOPINCS
Graphics 219
219
DO
=> InRegs.ax = 3
: CALL INTERRUPT(&H33, InRegs, OutRegs)
YNew% = OutRegs.dx
XNewx = OutRegs.cx
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
~ LINE (X%, Y%)-CXOLd%, YOLd%), Back% "Erase old tine
LINE (X%, YX)-(XNew%, YNew%), Forex "Draw new Line
XOLdX% = XNew%
YOldX = YNew%
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPTC&H33, InRegs, OutRegs)
InRegs.bx = 0 "Left Button Releases
InRegs.ax = 6
CALL INTERRUPTC(&H33, InRegs, Outhega?
LOOP WHILE OutRegs.bx = 0
RETURN
And that’s it. When the user releases the left mouse button, the line becomes final.
Subroutine DrawBox
The next subroutine is DrawBox. In fact, this subroutine is extremely simple, now that
we've developed DrawLine. If we just add a “B” parameter to the end of the LINE
statement in BASIC:
Adding the F option after the B option will make BASIC fill the box in with the
same colors used to draw the sides.
Then we'll get a box with its upper left corner at (X1, Y1) and lower right corner at (X2,
Y2). This is perfect for us: just copying over DrawLine and adding the B parameter means
that the anchor point will now anchor one corner of the box, and the mouse cursor will
drag the other corner around the screen. When the user releases the left mouse button,
the box will become final:
220 » Advanced BASIC
DrawBox:
X% = OutRegs.cx
Y% = OutRegs.dx
XOLdX = X%
YOld% = Y%
bo
InRegs.ax = 3
CALL INTERRUPT(&H33, baa OutRegs)
YNew% = OutRegs.dx
XNew% = OutRegs.cx
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs>)
LINE (X%, Y%)-CXOLd%, YOLd%), Back%, B ra
LINE (X%, Y%)-(XNew%, YNew%), Fore%, B
XOLd% = XNew%
YOLd% = YNew%
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs?
InRegs.bx = 0 "Left Button Releases
InRegs.ax = 6
CALL INTERRUPT(&H33, Inkegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
Subroutine DrawCircle
You can even use the CIRCLE statement to draw arcs or ellipses in BASIC like
this: CIRCLE (x, y), Radius%, Color%, START!, END!, ASPECT!
START! and END! indicate the starting and ending angles, respectively, for
an arc (use radians). ASPECT! sets the aspect ratio for an ellipse.
Here we are drawing a circle centered at (X%, Y%), with radius Radius!, and with a
color specified by the value in Color%. We can adapt DrawLine here too; we’ll make the
anchor point the center of the circle, and the mouse cursor location the edge of the circle
$e > Graphics
phic 221
221
(i.e., the radius will stretch from (X%, Y%) to (XNew%, YNew%)). We can do that
by
changing the LINE statements in DrawLine to CIRCLE statements like this:
DrawCircle:
X% = OutRegs.cx
Y% = OutRegs.dx
XOLdG% = XX
YOld% = Y%
dO
InRegs.ax = 3
CALL INTERRUPT(&H33, InRegs, OutRegs)
YNewX = OutRegs.dx
XNew% = OutRegs.cx — :
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs) ‘
CIRCLE (X%, YX), SQRCCX% - XOLdX) * 2 + (YX = YOLdX) * ra
Ld Backs
CIRCLE (X%, Y4), SQRCCXX - XNew%) ~ 2 + CYX - YNew%) * 2)
XOldxX = XNew% -
YOld% = YNew% oS -
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPT(8H33, InRegs, OutRegs)
InRegs.bx = 0 ‘Left Button Releases
InRegs.ax = 6 :
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
RETURN
Subroutine DrawPaint
The last subroutine we'll develop is DrawPaint. This subroutine will let us fill hollow
figures with solid color. In this case, we can use the BASIC PAINT statement, like this:
This statement fills an area in with the current foreground color. The problem is that
this area must also be bounded by the current foreground color or by another color,
which we can specify like this:
222 & Advanced BASIC
This is a problem for us since our paint program is not sophisticated enough to figure
out what the border color of the figure surrounding us at some given point is. Instead,
we'll have to settle for painting in the current foreground color, which means that you can
only paint figures that were drawn in the current foreground color (or you can set the
foreground color to the color of the figure you want to fill in).
With that restriction, we can fill figures with color like this:
DrawPaint:
X% = OutRegs.cx
Y% = OutRegs.dx :
‘InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPTC&H33, InRegs, OutRegs)
= PAINT (X%, Y%)
InRegs.ax = 1 ‘Show mouse cursor
CALL INTERRUPT(8&H33, InRegs, OutRegs)
RETURN ~
GOSUB Initialize
DO
GOSUB GetLeftButtonPress
IF MenuSelectionMade% THEN
> Graphics 223
LOOP WHILE 1
END
Initialize:
SCREEN 8
LOCATE 1, 1
Forex = 1
Back% = 2
COLOR Fore%, Back%
PRINT “Exit Color Bkgrnd Draw Line oe
“Box Circle Paint Save Load";
GetLeftButtonPress:
DO
= 0 "Yait for left button press
InRegs.bx
InRegs.ax = 5
CALL INTERRUPT(&H33, canecs, OutRegs)
LOOP WHILE OutRegs.bx = 0
Row% = OutRegs.dx \ 8 +1
IF Row% = 1 THEN
MenuSelectionMade% = 1
ELSE
MenuSelectionMade% = 0
END IF
RETURN
MenuChoice:
DrawFflag% = 0
LineFlag% = 0
BoxFlag% = 0
Circleflag% = 0
PaintFlag% = 0
Choice% = OutRegs.cx \ 644 + 1
224 }& Advanced BASIC
DrawPixel:
InRegs.bx = 0 "Get Left button releases to clear queue
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
dO
InRegs.ax = 3
CALL INTERRUPT(8H33, InRegs, OutRegs)
> Graphics 225
RETURN
DrawLine:
X% = OutRegs.cx
Y% = OutRegs.dx
XOLdX% = X%
YOld% = Y%
dO
InRegs.ax = 3
CALL INTERRUPT(&H33, InRegs, OutRegs)
YNew% = OutRegs.dx
XNew% = OutRegs.cx
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
LINE (X%, Y%)-C(XO0Ld%, YOLd%), Backs
LINE (X%, Y%)-(XNew%, YNew%), Forez
XOlLd% = XNews
YOld% = YNew%
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = 0 ‘Left Button Releases
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
RETURN
DrawBox:
X% = OutRegs.cx
Y% = OutRegs.dx
XOLdU% = X%
YOld% = Y%
po
InRegs.ax = 3
CALL INTERRUPT(&H33, InRegs, OutRegs)
226 & Advanced BASIC
DrawCircle:
X% OutRegs.cx
¥% wot OutRegs.dx
XOlLdZ% X%
YOld% = Y%
DO
InRegs.ax = 3
CALL INTERRUPTC&H33, InRegs, OutRegs)
YNews% = OutRegs.dx
XNew% = OutRegs.cx
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
CIRCLE (X%, Y%), SQRCCX% - XOLd%) * 2 + CYX% - Yoltd%) + C272
Backz
CIRCLE (X%, Y%2), SQRC (XZ ~ XNew%) * 2 + CYY = YNew%) ~ 2)
XOlLd% = XNew%
YOLdZ% YNews
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = 0 "Left Button Releases
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
RETURN
DrawPaint:
X% = OutRegs.cx
Y% = OutRegs.dx
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
PAINT (X%,. ¥%)
InRegs.ax = 1 "Show mouse cursor J
CALL INTERRUPT(&H33, InRegs, OutRegs)
RETURN
> Graphics 227
Give it a try; it works well, and creating your own figures on the screen can be a lot of
fun. Now, however, we're going to press on with our exploration of graphics, but before
getting into sprites and animation, there’s a popular way of drawing on the screen that we
should look at: the DRAW statement.
Drawing a Clock
You can use DRAW to produce “vector” graphics — that is, line graphics — on the
screen. DRAW takes a string as its argument. For example, this statement draws a hex-
agon on the screen:
DRAW “BUSO NL25 F12 D20 G12 L50 H12 U20 E12 R25 BD22"
Tip - The DRAW instruction is BASIC’s most compact way to produce graphics.
Each letter is a directive to DRAW, and each number is a length in pixels. Here’s what
the letters mean:
Up
oC Down
Left
Right
Up and right
Down and right
Down and left
Up and left
Pen up (don't draw)
Pen down (draw)
Zworatmar
This table allows us to decipher our DRAW statement. When we first use DRAW, its
“pen” is at the center of the screen. Here, we pick up the pen (so we don’t draw on the
screen) and move up 50 pixels, then put the pen down and move to the left 25 pixels:
Next, we move diagonally down and to the right by 12 pixels, and then straight down by
20:
Then we move diagonally left and down by 12, to the left 20 units, and so on, until our
hexagon is finished:
DRAW “BUSO NL25 F12 D20 G12 L50 H12 U2O E12 R25 BD22"
As it turns out, you can draw at angles other than just 0, 45, and 90 degrees. You can
use the “TA=” (Tilt Angle) argument to rotate the drawing actions that follow. For exam-
ple, this draws a line of 50 pixels straight up; that is, at 90 degrees:
DRAW “NUSO”
However, we might want to draw this line at 95 degrees instead. To do that, we tilt our
axes by 5 degrees counterclockwise:
ANG
=5
DRAW “TA="+VARPTRSCANG)+" NUSO”
This strange form, where we pass a string pointer to a variable holding the tilt angle, is
required when you want to use the TA= argument. In this case, we tilt the vertical 5
degrees counterclockwise, so a line that was formerly straight up is now at 95 degrees. A
negative angle would have tilted the axes clockwise. Keep in mind that the tilt remains in
effect until you change it.
Let’s put this to work by drawing a clock that displays the current time. We start by
drawing our hexagon and then moving the pen back to it center:
SCREEN 8 o
DRAW “BU5O0 NL25 F12 D20 G12 L50 H12 U20 E12 R25 BD22"
«
AsO > Graphics
Hc 229
229
Now we calculate the angle from vertical of both the minute and hour hands using
the
BASIC TIMER function (notice that we make the angles negative, since we want to treat
them as clockwise angles):
SCREEN 8
DRAW “BUSO NL25 F12 D20 G12 L50 H12 U20 E12 R25 BD22"
TimeMark! = TIMER
Hours! = INTCTimeMark! / 3600)
Remainder! = TimeMark! - 3600 * Hours!
IF Hours! > 12 THEN Hours! = Hours! - 12
> HourAngle! = -Hours! / 12 * 360
Minutes! = INTCRemainder! / 60)
MinuteAngle! = -Minutes! / 60 * 360
Finally, we can just draw the hour and minute hands like this:
SCREEN 8 :
DRAW “BU50 NL25 F12 D20 612 L50 H12 U20 £12 R25 BD22"
TimeMark! = TIMER oo :
Hours! = INTCTimeMark! / 3600) —
Remainder! = TimeMark! - 3600 * Hours! ©
IF Hours! > 12 THEN Hours! = Hours! - 12.
HourAngle! = -Hours! / 12 * 360
Minutes! = INT(Remainder! / 60).
MinuteAngle! = -Minutes! / 60 * 360.
> DRAW “TA=" + VARPTRS(CHourAngle!) + “ NUS
DRAW "TA=" + VARPTRS(MinuteAngle!) + “ NU12"
If you prefer, you can revise this program so that it keeps updating the figure on the
screen. As it stands, it only draws the clock once, reflecting the current time.
Now that we’ve gained some familiarity with the DRAW statement, let’s move on to
screen sprites and animation.
SpriteArray(1) "0000000010000000"
SpriteArray(2) = "0000000010000000"
SpriteArray(3) = "0000000111000000"
SpriteArray(4) = "0000001131100000"
SpriteArray(5) = "0000001131100000"
SpriteArray(6) = "0000011333110000"
SpriteArray(7) = "0000011333110000"
SpriteArray(8) = "0010011333110010"
SpriteArray(9) = %0011001131100110"
SpriteArray(10) = "0011101131101110"
SpriteArray(11) = "0011111131111110"
SpriteArray(12) = "0011100131001110"
SpriteArray(13) = "0011001111100110”
SpriteArray(14) = "0010000222000010"
SpriteArray(15) = "“0000000222000000"
SpriteArray(16) = "0000000020000000"
Now we can place the screen into a graphics mode and use a subroutine to draw this
sprite on the screen, converting string after string into pixels on the screen:
SpriteArray(1) = "0000000010000000"
SpriteArray(2) = “0000000010000000"
SpriteArray(3) = "0000000111000000"
SpriteArray(4) = "0000001131100000"
SpriteArray(5) = “0000001131100000"
SpriteArray(6) = "0000011333110000"
SpriteArray(7) = %0000011333110000"
SpriteArray(8) = "0010011333110010" |
SpriteArray(9) = "0011001131100110" |
SpriteArray(10) = "0011101131101110"
SpriteArray(11) = "0011111131111110"
SpriteArray(i2) = "0011106131001110"
eee phics
> Gra il 231
SpriteArray(13) = "0011001111100110"
SpriteArray(14) = "0010000222000010"
SpriteArray(15) = "0000000222000000"
SpriteArray(16) = “0000000020000000"
— ScreenMode% = 1
GOSUB DrawSprite
*
»
Now that we've placed the sprite on the screen, we can store it in an array with a
subroutine named GetSprite. To do that, we have to dimension an array to hold the sprite
data, which we can call GetArray( ). Since we might want to change our sprite size as we
design it, let’s make this a dynamic array, which means that we'll be able to adjust it to the
size we need with a REDIM statement in GetSprite:
* SDYNAMIC <_
SpriteArray(1) = “0000000010000000"
SpriteArray(2) = "“0000000010000000"
SpriteArray(3) = "000000G111000000"
SpriteArray(4) = "0000001131100000"
SpriteArray(5) = "9000001131100000"
SpriteArray(6) = “0000011333110000"
SpriteArray(7) = “0000011333110000"
SpriteArray(8) = "0010011333110010"
SpriteArray(9) = "0011001131100110" .
SpriteArray(10) = "0011101131101110"
SpriteArray(11) = "0011111131111110"
SpriteArray(12) = "0011100131001110"
SpriteArray(13) = "0011001111100110"
SpriteArray(14) = "0010000222000010"
SpriteArray(15) = "0000000222000000"
SpriteArray(16) = “O0000000020000000"
ScreenMode% = 1
GOSUB DrawSprite
GOSUB GetSprite ~
As in our paint program, we may want to store the sprite to disk and then retrieve it, so
let’s add two subroutines named SaveSprite and LoadSprite which will do that:
" SDYNAMIC
SpriteArray(1) = "0000000010000000"
SpriteArray(2) = "0000000010000000"
SpriteArray(3) = "0000000111000000"
SpriteArray(4) = "0000001131100000"
SpriteArray(5) = “9000001131100000"
SpriteArray(6) = "0000011333110000"
SpriteArray(7) = "0000011333110000"
SpriteArray(8) = "0010011333110010"
SpriteArray(9) = "0011001131100110"
SpriteArray(10) = "0011101131101110"
SpriteArray(11) = "0011111131111110"
SpriteArray(12) = "0011100131001110"
SpriteArray(13) = "0011001111100110"
SpriteArray(14) = “0010000222000010"
SpriteArray(15) = “0000000222000000"
SpriteArray(16) = "0000000020000000"
ScreenModex = 1
GOSUB DrawSprite
GOSUB GetSprite
GOSUB SaveSprite
GOSUB LoadSprite
To make sure the sprite made it back from the disk intact, we can display it on the
screen with the BASIC PUT statement, like this, which puts the sprite at
location (50, 50):
* SDYNAMIC
SpriteArray(1) “0000000010000000"
SpriteArray(2) “0000000010000000"
SpriteArray(3) “0000000111000000"
SpriteArray(4) “0000001131100000"
SpriteArray(5) "0000001131100000"
SpriteArray(6) “0000011333110000"
SpriteArray(7) "0000011333110000"
SpriteArray(8) "0010011333110010"
SpriteArray(9) "0011001131100110"
tenant
ten
a
> Graphics 233
SpriteArray(10) = "0011101131101110"
SpriteArray(11) = "0011111131111110"
SpriteArray(12) = "0011100131001110"
SpriteArray(13) = "0011001111100110"
SpriteArray(14) = "0010000222000010"
SpriteArray(15) = "0000000222000000"
SpriteArray(16) = "0000000020000000"
ScreenMode% = 1
GOSUB DrawSprite
GOSUB GetSprite
GOSUB SaveSprite
GOSUB LoadSprite
Now we're ready for some animation. We can put together a loop, putting the sprite on
the screen, erasing it, then moving it up a little and repeating the action so that it appears
to be flying upwards. As mentioned earlier, PUT’s default method is to XOR the bits in a
screen image with what’s already there. That means that when we PUT the same image in
the same place for a second time, it disappears (any number XOR’d with itself is 0). Then
we're free to draw the spaceship a little higher up the screen, erase it, and so on. At the
very end, we have to draw the rocket ship one last time (since every previous image of it
was erased). Listing 5-2 shows how the animation loop looks.
SpriteArray(1) = "0000000010000000"
SpriteArray(2) = “0000000010000000"
SpriteArray(3) = "0000000111000000"
SpriteArray(4) = "0000001131100000"
SpriteArray(5) = "0000001131100000"
SpriteArray(6) = "0000011333110000"
SpriteArray(7) = "0000011333110000"
SpriteArray(8) = "0010011333110010"
SpriteArray(9) = "0011001131100110"
SpriteArray(10) = "0011101131101110"
SpriteArray(11) = "0011111131111110"
SpriteArray(12) = "9011100131001110"
SpriteArray(13) = "0011001111100110"
SpriteArray(14) = "0010000222000010"
SpriteArray(15) = “0000000222000000"
234 P» Advanced BASIC
And that’s it, we’ve completed SPRITE.BAS, including animation. All that remains is to
write the subroutines. Let’s turn to those next.
Subroutine DrawSprite
We're supposed to decode the sprite as defined in the STRING array SpriteArray( ) in
this subroutine. We can start by placing the screen into the graphics mode we’ve defined,
ScreenMode%, and picking a location to display the sprite on the screen. Let’s choose
Oe Oye
Then we decipher the strings in the array SpriteArray( ) and use the BASIC PSET
statement to turn pixels on. We decode each pixel’s color value by looping over each
string in the array — and looping over each character in the string — to place the color
value of the current pixel into the variable CurrentColor%:
a & Graphics
pic 235
295
DrawSprite:
SCREEN ScreenMode%
XStart = 10
YStart = 10
FOR i = 1 TO UBOUND(A, 1)
FOR j = 1 TO LEN(SpriteArray(i))
> CurrentColor% = ASCCUCASES(MIDS(SpriteArray(i), j, 1)))
me
oe
NEXT j
NEXT i
Now that we have the character (a hex digit) in CurrentColor%, we have to decode it
into a color value from 0 to 15. We do that this way, then use PSET() to turn the
appropriate pixel on:
OrawSprite:
SCREEN ScreenMode%
XStart 10
YStart 10
FOR i = 1 TO UBOUND(CA, 1)
FOR j = 7 TO LEN(SpriteArray(i))
CurrentColor% = ASCCUCASES(MIDS(SpriteArray(i), j, 1)))
= IF CurrentColor% <= ASC("9") THEN
CurrentCotor% = CurrentColor% = ASCC"0")
ELSE
CurrentColor% = CurrentColor% - ASC("A") + 10
END IF
PSET (XStart + j - 1, YStart + i - 1), CurrentCotor%
NEXT j
NEXT i
At this point, the sprite is on the screen, and we’re done. Listing 5-3 shows the whole
subroutine.
The next job is loading the sprite into an array where we can work with it.
Subroutine GetSprite
This subroutine is supposed to read the sprite from the screen and store it in the array
GetArray( ). There are, it turns out, standard formulas to determine the memory storage
needed for a sprite of a given size, depending on screen mode. All we need is the
dimension of the sprite in pixels, which we can get from the dimensions of the array
SpriteArray( ), and the screen mode, which is already in the variable ScreenMode%. We
determine the number of 16-bit words needed to store the sprite and place that value in
SpriteSize, like this:
GetSprite:
Cols = LENCSpriteArray(1))
Rows = UBOUND (SpriteArray, 1)
Now we've got to redimension GetArray( ) to match since we know how many 16-bit
words — that is, INTEGERs — we'll need, we can use REDIM to redimension GetAr-
& Graphics 237
ray( ) on the fly (note that we have to use the directive ’ $DYNAMIC at the beginning of
SPRITE.BAS to make the arrays dynamic):
GetSprite:
Cols = LENC(SpriteArray(1))
Rows = UBOUND (SpriteArray, 1)
Now we just have use GET to store the sprite, and we're done. Listing 5-4 shows the
completed subroutine.
Cols = LEN(SpriteArray(1))
Rows = UBOUND (SpriteArray, 1)
That’s it. We've stored the sprite in GetArray( ). The next two subroutines let us store
the sprite on disk, and load it back into memory.
Subroutine SaveSprite
We'll just use BSAVE to save the sprite to disk, as shown is Listing 5-5.
And that’s all there is to it: we have created a file named SPRITE.DAT and saved the
sprite in it. Now we have to read it from the disk again.
Subroutine LoadSprite
This final sprite subroutine just loads the sprite back into GetArray( ) with BLOAD,
much as we've already seen in our paint program. That process works as shown in Listing
5-6.
Listing 5-6.
| leedSorite:
~——sé* Er SEG
> BLOAD “SPR
—sSs=DEF SEG)
RETURN
SpriteArray(1) = "0000000010000000"
SpriteArray(2) = "0000000010000000"
SpriteArray(3) = "0000000111000000"
SpriteArray(4) = "0000001131100000"
SpriteArray(5) = “0000001131100000"
SpriteArray(6) = "0000011333110000"
SpriteArray(7) = “0000011333110000"
SpriteArray(8) = "0010011333110010"
SpriteArray(9) = "0011001131100110"
SpriteArray(10) = “0011101131101110"
SpriteArray(11) = "0011111131111110"
SpriteArray(12) = "0011100131001110"
SpriteArray(13) = "0011001111100110"
SpriteArray(14) = "0010000222000010"
SpriteArray(15) = "“0000000222000000"
SpriteArray(16) = "0000000020000000"
ScreenMode% = 1
GOSUB DrawSprite
GOSUB GetSprite
GOSUB SaveSprite
GOSUB LoadSprite
FOR i = 1 TO 100
PuT (100, 100 - i), GetArray
PuT (100, 100 - i), GetArray
NEXT i
END
DrawSprite:
SCREEN ScreenMode%
XStart = 10
YStart = 10
FOR i = 1 TO UBOUND(SpriteArray, 1)
FOR j = 71 TO LENCSpriteArray(i))
= ASCCUCASES(MIDS$(SpriteArray(i), 45. 4202
CurrentColor%
IF CurrentColor% <= ASC("9") THEN
CurrentCotor% = CurrentColor% - Asc¢("0")
ELSE
CurrentColor’ CurrentColor% — ASCC("A") + 10
3
END IF
PSET (XStart + j - 1, YStart + i - 1), CurrentColor’
NEXT j
240 P& Advanced BASIC
GetSprite:
Cols LEN(SpriteArray(1))
Rows Ro UBOUND (SpriteArray, 1)
SaveSprite:
DEF SEG = VARSEG(GetArray(1))
BSAVE “SPRITE.DAT", VARPTR(GetArray(1)), 2 * UBOUND(GetArray, 1)
DEF SEG
RETURN
LoadSprite:
In this program, we first design and draw the sprite, then we load it into GetArra
y( ).
Using SaveSprite, we’re able to save it on disk, and LoadSprite loads it back
in again.
Finally, we’ve seen how to animate the rocketship by looping over PUT(
) statements.
Give this program a try — it draws a credible rocket ship zooming up the
screen. Now,
however, we’re ready to press on, and we can turn to the Present
ation Graphics resources
offered in the BASIC PDS.
age of routines can present data in a variety of chart formats. For example, let’s say that
we wanted to track peanut butter consumption by region, where these were our figures:
Using the PDS Presentation Graphics packages, we could display our data in a pie
chart, a bar chart, or a line chart — and we'll take a look at all three here.
Pie Charts
To start, we should note that if you’re using QuickBasic, QBX.EXE, you'll need to load
the special Presentation Graphics Quick library named CHRTBEFR.QLB like this (for a
program name PIE.BAS):
On the other hand, if you’re using BC.EXE (version 7.0 or later), you would link to the
library CHRTBEFR.LIB like this:
LINK PIE,,,CHRTBEFR;
Now let’s develop our pie chart program, PIE.BAS. We have to include two .BI files
that come with the PDS first:
* SINCLUDE: 'C:\BC7\FONTB.BI'
* $INCLUDE: 'C:\BC7\CHRTB.BI'
Next, we'll need to load the name of each of our four regions into an array (which we'll
name Regions( )), the peanut butter consumption in each region into another array
(named Consumption( )), and create one last array named Exploded ).
242 }& Advanced BASIC
* SINCLUDE: 'C:\BC7\FONTB.BI'
" SINCLUDE: ‘C:\BC7\CHRTB.BI'
This final array is only necessary for pie charts. If the value in Exploded( ) correspond-
ing to a particular section of the pie is nonzero, that section is treated as “exploded,”
meaning that it is drawn as though it were being separated from the rest of the pie. We'll
leave the values in Exploded( ) at 0, meaning that none of our pie slices will be exploded
from the rest of the pie. Let’s fill the other arrays now:
Regions(1) = “North”
Regions(2) = "East"
Regions(3) = “South”
Regions(4) = “West”
Consumption(1) = 219
Consumption(2) = 19
Consumption(3) = 119
Consumption(4) = 319
«.
Now we have to select a graphics mode, but we can’t just use SCREEN( ). Instead,
we
have to use the new PDS statement ChartScreen( ),
The argument we pass to ChartScreen( ) has to be a valid BASIC screen mode;
in this
case, let’s use (VGA-only) mode 12:
* SINCLUDE: 'C:\BC7\FONTB.BI'
' SINCLUDE: 'C:\BC7\CHRTB.BI'
Regions(1) = "North"
Regions(2) = "East"
Secdl > Graphics
i aie243
Regions(3) = "South"
Regions(4) = "West"
Consumption(1) 219
Consumption(2) 19
Consumption(3) 119
Consumption(4) dé 319
fiudw
CALL ChartScreen(12) +
Now we're ready to specify what type of graph we want. We do that by setting up a
variable of type ChartEnvironment, which is defined like this in CHRTB.BI:
TYPE ChartEnvironment
- ChartType AS INTEGER 1=Bar, 2=Column, 3=Line, 4=Scatter, 5=Pie
ChartStyle AS INTEGER Depends on type
DataFont AS INTEGER Font to use for plot characters
ChartWindow AS RegionType Overall chart window
DataWindow AS RegionType Data portion of chart
MainTitle AS TitleType Main title options
SubTitle AS TitleType Second Line title options
XAxis AS AxisType X-axis options
YAxis AS AxisType Y-axis options
oe
ws
ewe
«=
se
es
=
Legend AS LegendType Legend options
END TYPE
We have to pass a variable of this type to the PDS graphing tools, and setting the fields
in this data structure allows us to specify the titles we want in our chart. Let’s call our
ChartEnvironment variable OurChart:
* SINCLUDE: ‘C:\BC7\FONTB.BI'
" SINCLUDE: ‘C:\BC7\CHRTB.BI'
Regions(1) “North”
Regions(2) "East"
Regions(3) "South"
Regions(4) un "West"
not
Consumption(1) 219
Consumption(2) 19
Consumption(3) 119
Consumption (4) w 319
ue
Hb
244 & Advanced BASIC
CALL ChartScreen(12)
«»
Now we have to indicate the type and style of chart we want. We can use one of these
(predefined) constants to indicate the type of chart we want — cBar, cColumn, cLine,
cScatter, or cPie. We'll choose cPie to ask for a pie chart. We also have to select the style of
the chart from these options for each type:
Type Style
Bar cPlain or cStacked
Column cPlain or cStacked
Line cLines or cNoLines
Scatter cLines or cNoLines
Pie cPercent or cNoPercent
For our example, let’s choose the pie chart type (cPie) drawn in a style that marks each
slice with the percentage it represents of the whole (cPercent). We use the PDS subpro-
gram DefaultChart( ) to let the PDS Presentation Graphics package know what our selec-
tions are:
Regions(1) = “North”
Regions(2) = “East”
Regions(3) = “South”
Regions(4) = “West”
Consumption(1) = 219
Consumption(2) 19
Consumption(3) = 119
Consumption(4) = 319
CALL ChartScreen(12)
“ SINCLUDE: "Cs\BC7\FONTB.BI'
* SINCLUDE: "C:\BC7\CHRTB.BI'
Regions(1) “North”
Regions(2) "East"
Regions(3) "South"
Regions(4) uw “West”
Huu
Consumption(1) 219
Consumption(2) +9
Consumption (3) 119
Consumption(4) tt 319
“one
CALL ChartScreen(12)
Here, OurChart is the ChartEnvironment variable we set up, Regions( ) holds the titles
of each pie section; Consumption( ) holds the actual numerica | data to plot; Exploded( )
indicates which pie slice(s) should be exploded; and the final integer, in this case 4,
indicates how many data points there are. We can call ChartPie( ) and then finish up as
shown in Listing 5-8.
246 }& Advanced BASIC
Regions(1) = “North”
Regions(2) “East”
Regions(3) “South”
Regions(4) ion
u "West"
Consumption(1) 219
Consumption(2) 19
Consumption(3) aq?
Consumption(4) tft 319
Wout
CALL ChartScreen(12) _
CALL DefaultChart(OurChart, cPie, cPercent)
Notice that we placed a BASIC SLEEP statement at the end of the code to hold the pie
chart on the screen until we press a key.
TIP: | The BASIC SLEEP statement replaces the older DO: LOOP WHILE
INKEY$ =
“”. If you use it with a number, like: sleep 5, BASIC will pause for five seconds
.
EEE
All this may seem like a lot of work to display a simple graph, but now that we’ve
got
the skeleton of the program working, it will be easy to adapt for other chart
types.
Bar Charts
For example, we can put together a bar chart example very easily from
what we already
have. We just need to omit the definition of Exploded( ), which has no
use in a bar chart,
and specify a plain style bar chart to DefaultChart( ):
> Graphics 247
Regions(1) “North”
Regions(2) "East"
Regions (3) “South”
Regions(4) fiwoaon
“West”
Consumption(1) 219
Consumption(2) 19
Consumption(3) 119
Consumption(4) tt 319
“oun
CALL ChartScreen(12)
Next, we add titles for the X and Y axes (which had no meaning and, therefore, were
not used when we drew a pie chart), and call Chart( ) — not ChartPie( ) — to display the
bar graph. (See Listing 5-9.)
Regions(1) “North”
"East”
Regions(2)
Regions(3) “South”
Regions(4) it "West"
hou
Consumption(1) 219
Consumption(2) 19
Consumption(3) 119
Consumption(4) uu
w 319
CALL ChartScreen(12)
OurChart.XAxis.AxisTitle.Title *Consumption"
OurChart.YAxis.AxisTitle.Title "Region"
And that’s it, the bar chart is now on the screen. Let’s work through one more type of
Presentation Graphics chart — line charts.
Line Charts
It’s easy to adapt our example to produce a line chart where all the displayed points are
connected by a line. We just specify what we want to DefaultChart( ):
SINCLUDE: 'C:\BC7\FONTB.BI'
* SINCLUDE: *C:\BC7\CHRTB.BI'
Regions(1) "North"
Regions(2) “East”
Regions(3) "South"
Regions(4) hu
tt “West”
Consumption(1) 219
Consumption(2) 19
Consumption(3) 119
Consumption(4) fi 319
Hon
CALL ChartScreen(12)
Regions(1) "North"
Regions(2) “East"
Regions(3) "South"
Regions(4) “West"
Consumption(1) 219
Consumption(2) 19
Consumption(3) 119
Consumption(4) it 319
iow
CALL ChartScreen(12)
OurChart.XAxis.AxisTitle.Title = “Region"
OurChart.YAxis.AxisTitle.Title = “Number”
thing to do, and after you take a look at the listing we’re going to develop, you'll see why.
However, knowing what video adapter card is available means that you know what screen
modes you can use, an indispensible piece of information if your program uses graphics.
|NOTE: With this function, you won't need to ask the user about installed graphics
equipment.
Let’s call our function GetVideoCard$( ). To make this function useful, we can have it
return these outputs (as STRINGs):
The MDA is a specific type of monitor, a monochrome monitor, and does not
refer to a screen that can display graphics but only in black and white. The
MDA can only support BIOS mode 7.
This function is going to take a little work. We'll have to check if the machine’s BIOS
can support a VGA, then an EGA, then work down to CGAs or MDAs. For each new
monitor type, a new level of BIOS was produced, so we interrogate the computer’s BIOS
directly to see how advanced the version we’re working with is.
First, we check to see if the BIOS in the machine is advanced enough to support
interrupt @H10 service @H1A, which would tell us exactly what video card is installed.
If so, &@HI1A is returned in al, and a video code in bl. We can just check that code like
this, with a SELECT CASE statement:
FUNCTION GetVideoCard$
GetVideoCard$ = "CGA"
CASE 4 TO 5
GetVideoCard$S = "EGA"
CASE 6
GetVideoCardS = “PGA"
CASE 7 TO 8
GetVideoCard$ = "VGA"
END SELECT
EXIT FUNCTION
However, only relatively recent BIOSes (those that can handle the VGA) support this
service. If it is not supported, we have to continue by checking for an EGA with interrupt
&H10 service &H12. If we call this service with bl = @H10, and, on return, bl is no
longer equal to @H10, an EGA is installed. Note that, unlike service &H1A, this service
only indicates the presence or absence of a single type of monitor, the EGA. If there is an
EGA, we can exit:
FUNCTION GetVideoCard$
~ CASE2
tt "CGA"
GetVideoCard$
CASE 4 TO 5
i] "EGA"
GetVideoCard$
CASE 6
"BGA"
GetVideoCard$
CASE 7 TO 8
"VGA"
GetVideoCard$
END SELECT
EXIT FUNCTION
ELSE
InRegs.ax = &H1200
InRegs.bx = &H10
sed
ee CALL INTERRUPT(&H10, InRegs, OutRegs)
IF (COutRegs.bx AND &HFF) <> &H10 THEN
GetVideoCard$S = "EGA"
EXIT FUNCTION
wl
on
252 & Advanced BASIC
Otherwise, there are only two options left, CGA and MDA. Since the MDA can only
support BIOS video mode 7, we check the current video mode. Ifit’s 7, we have an MDA.
If not, a CGA:
FUNCTION GetVideoCard$
CASE 4 TO 5
vEGA" <
GetVideoCard$
CASE 6 :
GetVideoCard$ u a PGA"
CASE 7 TO 8
GetVideoCards “VGA
END SELECT
EXIT FUNCTION
ELSE
InRegs.ax = &H1200
InRegs.bx = &H10
CALL INTERRUPT(&H10, InRegs, OutRegs)
IF COutRegs.bx AND @HFF) < > &H10 THEN
GetVideoCardS = “EGA"
EXIT FUNCTION
ELSE
InRegs.ax = &HFOO
CALL INTERRUPT(&H10, InRegs, OutRegs)
|
ee IF COutRegs.ax AND &HFF) = 7 THEN
GetVideoCard$ = "MDA"
EXIT FUNCTION
ELSE
GetVideoCard$ = "CGA"
EXIT FUNCTION
END IF
END IF
END IF
And we're done. Listing 5-11 shows the whole function, GetVideoCard$(
).
> Graphics 253
FUNCTION GetVideoCard$
REM This function returns a 3-letter string: "MDA" (monochrome adapter card)
REM "CGA" (color graphics adapter), "EGA" (enhanced graphics adapter),
REM "PGA" (professional graphics adapter), "VGA" (variable graphics array)
END FUNCTION
254 }& Advanced BASIC
This example program will tell you what the video card in your machine is. Let’s go on to
find another important piece of information about the graphics environment — the
horizontal and vertical pixel ranges on the screen.
Note that we allow for the possibility that the screen is not in a graphics mode, in
which case the pixel count is meaningless and GetXYRanges%( ) should return a 0.
The function shown in Listing 5-12 is just a single SELECT CASE statement, based on
the BIOS screen mode.
> Graphics 255
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
END TYPE
GetXYRanges% = 1
END FUNCTION
SCREEN 2
Check% = GetXYRanges%(XRange%, YRange%)
LOCATE 1, 1
IF Check% < > O THEN
PRINT "X, Y Ranges are: “,XRange%, YRangez
END IF
Note that we check the value of GetXYRanges% — i.e., make sure that it is not 0 — to
make sure XRange% and YRange% hold valid values.
Conclusion
And that’s it for graphics. We’ve worked through a lot in this chapter: We've developed
our own paint program, a screen clock, animated a small rocketship, plotted peanut
butter consumption by geographical region, and finished up with some low-level infor-
mation about the graphics environment. Try the paint program or design your own sprite
— it can be a lot of fun. Graphics can add a very professional feel to a program if it’s
handled correctly.
Now we're ready to move on, however, so let’s turn to a topic of interest to many
programmers: database programming. We'll see what the PDS offers us here with ISAM
database programming in Chapter 6.
Databasing
257
Ay é
» Ay
, ‘J
a
| | <i
a4 vanes |
_ ontesdeist-
) anWhPrisco
> Databasing 259
|NOTE: | The ISAM system is one of the major reasons programmers upgrade to the
PDS.
So what is ISAM? Briefly, it’s a method of quickly changing the apparent order of
records in a file. For example, you may be a careful person who prefers to order your
friends by height or age. Towards that end, you may have devised a new data TYPE to
hold information about each friend:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE :
Each variable in this type is a field in database terminology, and, together, all these
fields make up a record. After defining a data type, you can set up a variable of this type
named MyFriend like this:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
And then fill all the fields in this record with values like this:
260 P& Advanced BASIC
TYPE Friend.
Name -AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
~ MyFriend.Name = “Doug”
MyFriend.Age = 33
MyFriend.Height = 70 "Inches
MyFriend.HomeTown = “Redlands”
You can even make a file of such records, like the one shown in Figure 6-1. This file is a
database. The records in it are in the order in which we inserted them into the file, one
after the other, which is called insertion order.
Record 1
“Redlands”
44
69 Record 2
“San Pedro”
“Margie”
67
64 Record 3
“Los Angeles”
Record 4
HomeTown: “Morgantown”
Figure 6-1 4
If we were to make this an ISAM file (which we would have to specify when we first
create the file), the ISAM system would automatically begin the file with an index of the
records, as shown in Figure 6-2.
> Databasing 261
Name:
Age:
Height: Record 1
HomeTown:
69 Record 2
“San Pedro”
“Margie”
67
64 Record 3
HomeTown: “Los Angeles”
Age:
Height: Record 4
HomeTown: “Morgantown”
Figure 6-2
This index just lists the record numbers in the order in which they were inserted into
the file, and, in the ISAM system, this index is referred to as the NULL index (see Figure
6-3). The NULL index is the default index of the records in the file, and it’s always created
when you open a file for ISAM output and put records into it.
NULL Index
70 Record 1
“Redlands”
Pe Record 2
HomeTown: “San Pedro”
262 }& Advanced BASIC
“Margie”
67
64 Record 3
“Los Angeles”
“Adam”
2
a Record 4
HomeTown: “Morgantown”
Figure 6-3
However, the crux of the ISAM system is that you can build other indices as well. For
example, we can (and will) instruct the ISAM system to create an index of the records in
your file by age. For easy reference, we can even give this index a name; e.g., Friends-
ByAge (see Figure 6-4). Note that the order in this index is different from that in the
NULL index. Here, the records are listed in terms of increasing age.
NULL Index
FriendsByAge Index
Record 1
69 Record 2
“San Pedro”
“Margie”
67
64 Record 3
HomeTown: “Los Angeles”
to
pee
4
ele
> Databasing 263
Record 4
HomeTown: “Morgantown”
Figure 6-4
The way we'll create indices with ISAM is through the new BASIC statement, CREATE-
INDEX. Similarly, we can create another index on the height of your friends, which we
can call FriendsByHeight (see Figure 6-5).
NULL Index
FriendsByAge Index
FriendsByHeight Index
Record 1
Age:
Height: 69 Record 2
HomeTown: “San Pedro”
“Margie”
oF Record 3
64
HomeTown: “Los Angeles”
|)
fore
ge
oe)
AN)
Be
pepe)
264 }& Advanced BASIC
e : Record 4
HomeTown: “Morgantown”
Figure 6-5
LON
Figure 6-6
On the other hand, you can select which index is the current index with the SET-
INDEX statement. For example, you may make the FriendsByAge index the current index
with SETINDEX (see Figure 6-7).
4
1
2
3
Figure 6-7
Now, when you ask for the first record from this file, you'll get the entry that’s really
record 4, not record 1 (Adam has the lowest age of all the friends). The next record after
that will really be record 1, followed by record 2 and then 3. In other words, the file now
appears to be sorted by age. (Note that the file has not really been resorted. The ISAM
system uses pointers to point to each record, and it is these pointers that are actually
sorted.)
> Databasing 265
Sorting a file by sorting pointers to each record, and not each record itself,
saves ISAM the trouble of moving each record's data. It’s a trick all profes-
sional database systems use, and the result is that ISAM can manage records
considerably faster than any other method in BASIC.
Similarly, if you made FriendsByHeight the current index, the file would appear to be
sorted by height. When you retrieve the first record from the file, it will be the record of
the person with the lowest height. The next record will be that of the person with the
next lowest height, and so on up to the tallest person.
Ordering records like this can be very useful. For example, if you wanted to exclude all
people below the age of thirty, you'd order your file by age, giving you this ordering of
ages, record by record:
25 (Adam)
33 (Doug)
44 (Ed)
67 (Margie)
You would then scan up the file for the first age above 30 (this can be done with the SEEK
statement, as we'll see), which is Doug:
25 (Adam)
—33 (Doug)
44 (Ed)
67 (Margie)
Since the records are ordered, you know that all records before this one are of people
under thirty, and can be discarded. All records after this point can be preserved; handling
data like this is the fundamental part of a database program.
Now let’s put this all into code.
This time, you don’t need to create or load new Quick libraries. Instead, you have to
load a TSR (Terminate and Stay Resident) program that holds the ISAM code. This
program is named PROISAMD.EXE; it contains all the code needed to run all database
programs. Just run it once before using any ISAM routines and then start QBX as usual. If
your database program is DB.BAS, you’d type QBX DB. If you’re using BC.EXE (version
7.0 or later), just compile and link the program into DB.EXE, run PROISAMD.EXE, and
then run DB.EXE.
Now we're free to do some programming. Our database program will do some of the
things we've been talking about, and we can even use the list of friends we’ve developed.
We can start by structuring our data records with TYPE and creating a variable named
MyFriend of that type:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
Now we can create our ISAM file. To do that, we simply say OPEN “FRIENDS.DAT”
FOR ISAM (as opposed to opening it under other BASIC options such as APPEND), like
this:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
Let’s take a look at this OPEN statement. We start with OPEN “FRIENDS.DAT” FOR
ISAM, which is clear enough, since we want this file to be an ISAM file. The next
keyword, Friend, indicates the TYPE of the records we'll be using so that ISAM can
structure each record in the file. The following keyword, Pals, is the table name, and the
number (#1) is the file number as is normal in BASIC.
All the records of a particular data type (such as TYPE Friend) make up a table in the
database file. For example, when we've filled FRIENDS.DAT with all our friends, all the
records will be in a single table, which we’re naming Pals (see Figure 6-8).
“Pals”
NULL Index
“Pals”
FriendsByAge Index
“Pals”
| FriendshipByHeight
Index
pas
ge:
Height: 70 Record 1
HomeTown: “Redlands”
“Pals”
, Table
Age
Height: 69 Record 2
HomeTown: “San Pedro”
Lo]
268 Pb Advanced BASIC
“Margie”
67
64 Record 3
“Los Angeles”
Record 4
HomeTown: “Morgantown”
Figure 6-8
But we might also have another table with a different record type — say TYPE Enemy
— in the same file, and this would make up a new table that we can call NotPals (see
Figure 6-9).
“Pals”
NULL Index
“Pals”
FriendsByAge Index
“Pals”
| FriendshipByHeight Index
70 Record 1
“Redlands”
g an 7 Record 2
HomeTown: “San Pedro” etsy
ta > Databasing
ING 269
=U
“Pals”
“Margie” Table
67
64 Record 3
HomeTown: “Los Angeles”
2
71 5 Record 4
HomeTown: “Morgantown”
“NotPals”
NULL Index
Record 1
“Not Pals”
Table
Record 2
Figure 6-9
If we wanted to work with the NotPals table, we’d open up the same file again, but this
time we’d specify a different record TYPE (Enemy), and a different file number:
What's referred to in the rest of BASIC as a file number (i.e., 1 and 2 as we’ve opened
them so far) are really table numbers in ISAM. When you create indices, search for
matches, or perform other operations in ISAM, you have to specify the table number as
well.
The reason you can have multiple tables in ISAM is to avoid the need for
relational databases, where you have to tie records from different files
together. Here, you can put all your data, even data that would normally go into
different files, into the same file. And ISAM files can be huge; the upper limit is
128 megabytes.
270 }& Advanced BASIC
We're going to limit ourselves to one table (Pals) in this chapter. And our table number
is going to be 1:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
Now we have to insert the data we want, record by record, into FRIENDS.DAT. We can
do that with the INSERT statement, like this:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE :
MyFriend.Name = “Doug”
MyFriend.Age = 33
MyFriend.Height = 70 "Inches
MyFriend.HomeTown = “Redlands”
— INSERT 1, MyFriend
MyFriend.Name = "Eq"
MyFriend.Age = 44
MyFriend.Height = 69 ‘Inches
MyFriend.HomeTown = “San Pedro”
— INSERT 1, MyFriend
MyFriend.Name = “Margie"
MyFriend.Age = 67
MyFriend.Height = 64 ‘Inches
MyFriend.HomeTown = “Los Angeles"
— INSERT 1, MyFriend
MyFriend.Name = “Adam”
MyFriend.Age = 25
MyFriend.Height = 71 "Inches
er
a > Databasing 271
ING «271
MyFriend.HomeTown = "Morgantown"
— INSERT 1, MyFriend
*
.
»
It's an easy process; we just fill the fields in MyFriend with the data we want, and then
INSERT MyFriend into the file FRIENDS.DAT (which we've opened as #1).
So far, then the database file FRIENDS.DAT has all four records loaded, and the NULL
index has been automatically set up by ISAM (see Figure 6-10).
NULL Index
Record 1
Record 2
“Margie”
ay Record 3
“Los Angeles”
71 Record 4
HomeTown: “Morgantown”
Figure 6-10
Now we can go on to create the other two indices, FriendsByAge and FriendsByHeight.
To do that we use CREATEINDEX, passing it the file number, the name we want to give
to this index, a parameter named unique%, and the name of the field we want to sort on
(Age):
272 +} Advanced BASIC
TYPE Friend ~
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = “Doug"
MyFriend.Age = 33
MyFriend.Height = 70 ‘Inches
MyFriend.HomeTown = “Redlands”
INSERT 1, MyFriend
MyFriend.Name = “Ed"
MyFriend.Age = 44
MyFriend.Height = 69 "Inches
MyFriend.HomeTown = "San Pedro"
INSERT 1, MyFriend
MyFriend.Name = “Margie”
MyFriend.Age = 67
MyFriend.Height = 64 ‘Inches
MyFriend.HomeTown = “Los Angeles”
INSERT 1, MyFriend
MyFriend.Name = “Adam"
MyFriend.Age = 25
MyFriend.Height = 71 *Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
The unique% field indicates whether or not ISAM will tolerate the insertion of records
with the same value in this field as one that already exists in the database. For example, if
we set unique% to 1, demanding unique entries only, and then tried to enter a record for
Pete, whose age is 33 (the same as Doug’s), a trapable error would be generated (i.e., use
ON ERROR GOTO...). In this chapter we are not going to worry about the uniqueness of
records, so we'll set this value to 0. Now the database file looks like this Figure
6-11.
NULL Index
-ONM—
> Databasing 273
| FriendsByAge Index
Record 1
HomeTown:
Age:
Height: Record 2
HomeTown: “San Pedro”
“Margie”
67
64 Record 3
“Los Angeles”
Record 4
HomeTown: “Morgantown”
Figure 6-11
We know what order we’ve inserted the records into the file. Now we can sort them
according to age by making the FriendsByAge index the current index and printing out
each friend’s name. We start off with SETINDEX:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = “Doug"
MyFriend.Age = 33
MyFriend.Height = 70 ‘Inches
MyFriend.HomeTown = “Redlands”
INSERT 1, MyFriend
MyFriend.Name = "Ed"
274
T
li \& Advanced
ehh BASIC
lhc R
MyFriend.Age = 44
MyFriend.Height = 69 "Inches
MyFriend.HomeTown = "San Pedro”
INSERT 1, MyFriend
MyFriend.Name = “Margie”
MyFriend.Age = 67
MyFriend.Height = 64 "Inches
MyFriend.HomeTown = “Los Angeles”
INSERT 1, MyFriend
MyFriend.Name = “Adam”
MyFriend.Age = 25
MyFriend.Height = 71 "Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
— SETINDEX 1, "FriendsByAge”
»
»
«
This is easy enough — we're just making the FriendsByAge index the current index for
file 1 (actually table 1). Now we can print the names out in this new order.
The ISAM system supports a number of statements for moving around in a database
file. Here’s a list of the ones we'll find useful:
MOVEFIRST FileNumber
MOVELAST FileNumber
MOVENEXT FileNumber
MOVEPREVIOUS FileNumber
EQOFCFileNumber)
BOFCFileNumber)
Keep in mind that what’s listed in the ISAM documentation as FileNumber, as above, is
really the table number. In other words, confusing as it may be, you can actually have as
many file numbers for a specific file as there are tables in that file.
The MOVE statements are self-explanatory. EOF(FileNumber) returns TRUE (ie., all
bits set: @HFFFF) if we’re at the end of the table, and BOF(FileNumber) returns TRUE if
we're at the beginning. Otherwise, they return FALSE (0).
With all this in mind, here’s how we print out the new ordering of the records in
FRIENDS.DAT, now sorted by age:
> Databasing 275
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = "Doug"
MyFriend.Age = 33
MyFriend.Height = 70 ‘Inches
MyFriend.HomeTown = "Redlands"
INSERT 1, MyFriend
MyFriend.Name = “Ed"
MyFriend.Age = 44
MyFriend.Height = 69 fInches
MyFriend.HomeTown = “San Pedro”
INSERT 1, MyFriend
MyFriend.Name = “Margie"
MyFriend.Age = 67
MyFriend.Height = 64 ‘Inches
MyFriend.HomeTown = "Los Angeles"
INSERT 1, MyFriend
MyFriend.Name = “Adam”
MyFriend.Age = 25
MyFriend.Height = 71 "Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
SETINDEX 1, “FriendsByAge”
— MOVEFIRST 1
DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
MOVEFIRST 1
276 & Advanced BASIC
And then we enter a DO loop that loops over each record in the file by continually using
MOVENEXT until EOF(1) becomes true:
MOVEFIRST 1
DO
MOVENEXT 1
LOOP WHILE NOT EOF(1)
When we're at the beginning of the file, the first record in the current index is the
current record. When we use MOVENEXT 1, the next record in that index becomes the
current record. We can look at the current record by retrieving it, using RETRIEVE, into a
variable of type Friend, such as MyFriend:
MOVEFIRST 1
bO
RETRIEVE 1, Myfriend
MOVENEXT 1
LOOP WHILE NOT EOF(1)
RETRIEVE 1, MyFriend reads the current record from the databse file and places it
into MyFriend. That means that we can print out the name of the person in the current
record with PRINT like this:
MOVEFIRST 1
DO
RETRIEVE 1, MyFriend
> PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
And that’s the way to loop over the ordered records of an ISAM file.
We can also create our second index, FriendsByHeight, like
this:
> Databasing 277
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = “Doug”
MyFriend.Age = 33
MyFriend.Height = 70 ‘Inches
MyFriend.HomeTown = "Redlands”
INSERT 1, MyFriend
MyFriend.Name = “Ed"
MyFriend.Age = 44
MyFriend.Height = 69 "Inches
MyFriend.HomeTown = “San Pedro”
INSERT 1, MyFriend
MyFriend.Name = “Margie"
MyFriend.Age = 67
MyFriend.Height = 64 "Inches
MyFriend.HomeTown = "Los Angeles"
INSERT 1, MyFriend
MyFriend.Name = "Adam”
MyFriend.Age = 25
MyFriend.Height = 71 "Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
SETINDEX 1, “FriendsByAge”
MOVEFIRST 1
DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHELE NOT EOF(1)
*
rf
NULL Index
FriendsByAge Index
FriendsByHeight Index
Name:
Age:
Height: Record 1
HomeTown: “Redlands”
“Ed”
44
69 Record 2
“San Pedro”
“Margie”
67
64 Record 3
“Los Angeles”
Record 4
Homerown: “Morgantown”
Ee
ee
ee
eee
ee
Figure 6-12
We can make FriendsByHeight the current index and print the records out in that
order just as we did for FriendsByAge:
> Databasing 279
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = "Doug"
MyFriend.Age = 33
MyFriend.Height = 70 "Inches
MyFriend.HomeTown = "Redlands”
INSERT 1, MyFriend
MyFriend.Name = “Ed"
MyFriend.Age = 44
MyFriend.Height = 69 "Inches
MyFriend.HomeTown = “San Pedro”
INSERT 1, MyFriend
MyFriend.Name = “Margie"
MyFriend.Age = 67
MyFriend.Height = 64 "Inches
MyFriend.HomeTown = "Los Angeles“
INSERT 1, MyFriend
MyFriend.Name = "Adam"
MyFriend.Age = 25
MyFriend.Height = 71 ‘Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
SETINDEX 1, “FriendsByAge"
MOVEFIRST 1
dO
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
— SETINDEX 1, “*FriendsByHeight"
280 P& Advanced BASIC
MOVEFIRST 1.
dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
That’s it for reordering the whole file. Now let’s start searching through our database
for specific values.
Seeking Records
A big part of working with a database is being able to find records that meet a certain
criterion quickly. For example, we may want to search for a person that is 33 years old.
To perform this and other operations, ISAM provides the SEEK statements: SEEKGT,
SEEKGE, and SEEKEQ. Using them is simple. If you wanted to find a person with age 33
in our table, table 1, you would make FriendsByAge the current index (so ISAM knows
what field you’re interested in examining), and then use the statement SEEKEQ 1, 33. In
code, that looks like this:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = "Doug”
MyFriend.Age = 33
MyFriend.Height = 70 "Inches
MyFriend.HomeTown = “Redlands”
INSERT 1, MyFriend
MyFriend.Name = “Ed"
MyFriend.Age = 44
MyFriend.Height = 69 "Inches
MyFriend.HomeTown = "San Pedro”
rr > Databasing
tabasing 281
281
INSERT 1, MyFriend
MyFriend.Name = "Margie"
MyFriend.Age = 67
MyFriend.Height = 64 ‘Inches
MyFriend.HomeTown = “Los Angeles"
INSERT 1, MyFriend
MyFriend.Name = “Adam"
MyFriend.Age = 25
MyFriend.Height = 71 "Inches
MyFriend.HomeTown = "Morgantown"
INSERT 1, MyFriend
SETINDEX 1, “FriendsByAge"
MOVEFIRST 1
BO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, “FriendsByHeight"
MOVEFIRST 1
dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
— SETINDEX 1, “FriendsByAge"
MOVEFIRST 1
SEEKEQ 1, 33
iiPe You can make your own ISAM masters from MOVE, SEEK, and EOF.
282 & Advanced BASIC
If there is no match, EOF(1) will be set true. If EOF(1) is not true, however, there was
a match and it will be the current record, which we can print out with RETRIEVE:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = “Doug"
MyFriend.Age = 33
MyFriend. Height = 70 "Inches
MyFriend.HomeTown = “Redlands”
INSERT 1, MyFriend
MyFriend.Name = "Ed"
MyFriend.Age = 44
MyFriend.Height = 69 "Inches
MyFriend.HomeTown = “San Pedro"
INSERT 1, MyFriend
MyFriend.Name = “Margie”
MyFriend.Age = 67
MyFriend.Height = 64 "Inches
MyFriend.HomeTown = “Los Angeles”
INSERT 1, Myfriend
MyFriend.Name = “Adam"
MyFriend.Age = 25
MyFriend.Height = 71 ‘Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
PRINT “Sorting by age..."
SETINDEX 1, "FriendsByAge"
MOVEFIRST 1
DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, “FriendsByHeight"
MOVEFIRST 1
dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, “FriendsByAge”
MOVEFIRST 1
SEEKEQ 1, 33
—+ IF EOF(1) THEN
PRINT "No Match."
ELSE
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
END IF
In a similar way, we can search for a person with a height over seven feet (that is, 84
inches) with SEEKGT:
TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Neme = “Doug”
MyFriend.Age = 33
MyFriend.Height = 70 ‘Inches
MyFriend.HomeTown = "Redlands"
INSERT 1, MyFriend
MyFriend.Name = "Ed”
MyFriend.Age = 44
MyFriend.Height = 69 ‘Inches
MyFriend.HomeTown = "San Pedro"
INSERT 1, MyFriend
284 -& Advanced BASIC
MyFriend.Name = “Margie"
MyFriend.Age = 67
MyFriend.Height = 64 "Inches
MyFriend.HomeTown = “Los Angeles”
INSERT 1, MyFriend
MyFriend.Name = "Adam"
MyFriend.Age = 25
MyFriend.Height = 71 "Inches
MyFriend.HomeTown = "Morgantown”
INSERT 1, MyFriend
SETINDEX 1, "FriendsByAge”
MOVEFIRST 1
DO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, “FriendsByHeight”
MOVEFIRST 1
dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, “FriendsByAge"
MOVEFIRST 1
SEEKEQ 1, 33
IF EOFC(1) THEN
PRINT “No Match."
ELSE
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
END IF
SETINDEX 1, “FriendsByHeight.”
> Databasing 285
MOVEFIRST 1
SEEKGT 1, 84
IF EOQFC1) THEN
PRINT “No Match."
ELSE
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
END IF
Since there is no match, EOF(1) is true, and “No Match.” is printed out.
ISAM provides other ways of managing records besides inserting them as we’ve seen.
You can also delete them or change the values in them. Let’s start by deleting some
records.
Deleting Records
Let’s say that we wanted to delete all records of friends whose age was over 40. We
could seek these records out by making FriendsByAge the current index (with SET-
INDEX), and then using a SEEKGT 1, 40 statement.
If we find any matches (i.e., EOF(1) is FALSE), we can simply delete them with the
statement DELETE 1, which deletes the current record in table 1. After removing all
records meeting this criterion, we can print the remainder out:
TYPE Friend :
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE
MyFriend.Name = “Doug”
MyFriend.Age = 33
MyFriend.Height = 70 "Inches
MyFriend.HomeTown = “Redlands”
INSERT 1, MyFriend
MyFriend.Name = “Ed"
MyFriend.Age = 44
MyFriend.Height = 69 "Inches
286 P Advanced BASIC
MyFriend.Name = “Margie"
MyFriend.Age = 67
MyFriend.Height = 64 ‘Inches
MyFriend.HomeTown = “Los Angeles"
INSERT 1, MyFriend
MyFriend.Name = “Adam”
MyFriend.Age = 25
MyFriend.Height = 71 ‘Inches
MyFriend.HomeTown = "Morgantown"
INSERT 1, MyFriend
SETINDEX 1, “FriendsByAge”
MOVEFIRST 1
dO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, "FriendsByHeight"
MOVEFIRST 1
DO
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
MOVEFIRST 1
SEEKEQ 1, 33
If EOQFC1) THEN
PRINT “No Match.”
ELSE
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
END IF
> Databasing 287
SETINDEX 1, “FriendsByHeight”
MOVEFIRST 1
SEEKGT 1, 84
IF EOFC(1) THEN
PRINT "No Match."
ELSE
RETRIEVE 1, MyFriend
PRINT Myfriend.Name
END IF
ve].
ae SETINDEX 1, “FriendsByAge"
dO
SEEKGT 1, 40
MOVEFIRST 1
dO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
«
ry
As you can see, deleting records is easy, but be careful, since it’s impossible to get them
back. Also, you shouldn’t expect your ISAM file (in this case, FRIENDS.DAT) to get any
smaller when you delete records. In the first place, the minimum size of an ISAM file is
64K, which includes 32K of overhead and 32K of space for records.
When the data space is used up, the ISAM system adds more space in 32K chunks (it
does this because its internal searching algorithms work better with data chunks of this
size). When you delete a record, the file is not compacted; instead, that record’s space is
just made available. A new record may be written there in the future.
288 > Advanced BASIC
If you have deleted many records and really want to compact an ISAM file, use
the utility program ISAMPACK, but keep in mind that it works in 32K chunks,
and, therefore, can only compact a file if more than 32K has been deleted.
Besides deleting records, we can change the individual fields inside them, as you’d
expect. You do that with UPDATE.
TYPE Friend | :
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE :
DIM MyFriend AS Friend
MyFriend.Name = “Doug”
MyFriend.Age = 33.
MyFriend.Height = 70 "Inches
MyFriend.WomeTown = "Redlands"
INSERT 1, MyFriend
MyFriend.Name = "Eq"
MyFriend.Age = 44
MyFriend.Height = 69 ‘Inches
MyFriend.HomeTown = “San Pedro”
INSERT 1, MyFriend © :
MyFriend.Name = “Margie"
MyFriend.Age = 67
MyFriend.Height = 64 "Inches
MyFriend.HomeTown = “Los Angeles”
INSERT 1, MyFriend
> Databasing 289
MyFriend.Name = “Adam"
MyFriend.Age = 25
MyFriend.Height = 71 ‘Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
SETINDEX 1, “FriendsByAge"
MOVEFIRST 1
DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, "“FriendsByHeight"
MOVEFIRST 1
DO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, "FriendsByAge”
MOVEFIRST 1
SEEKEQ 1, 33
IF EOFC1) THEN
PRINT “No Match.”
ELSE
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
END IF
SETINDEX 1, "FriendsByHeight”
MOVEFIRST 1
SEEKGT 1, 84
290 }& Advanced BASIC
IF EQF(1) THEN
PRINT “No Match."
ELSE
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
END IF
SETINDEX 1, “FriendsByAge”
bO
SEEKGT 1, 40
MOVEFIRST 1
dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
eek
ae CREATEINDEX 1, “FriendsByName”, 0, "Name"
SETINDEX 1, “FriendsByName”
SEEKE@ 1, “Doug"
RETRIEVE 1, MyFfriend
MyFriend.Age = 34
UPDATE 1, MyFriend
«
es
Then we can print everyone’s name and ages out to make sure the change was effec-
tive. Here’s the final program.
> Databasing 291
MyFriend.Name = "Doug"
MyFriend.Age = 33
MyFriend.Height = 70 ‘Inches
MyFriend.HomeTown = “Redlands"
INSERT 1, MyFriend
MyFriend.Name = “Ed"
MyFriend.Age = 44
MyFriend.Height = 69 ‘Inches
MyFriend.HomeTown = "San Pedro”
INSERT 1, MyFriend
MyFriend.Name = “Margie”
MyFriend.Age = 67
MyFriend.Height = 64 ‘Inches
MyFriend.HomeTown = “Los Angeles"
INSERT 1, MyFriend
MyFriend.Name = “Adam
MyFriend.Age = 25
MyFriend.Height = 71 'Inches
MyFriend.HomeTown = “Morgantown”
INSERT 1, MyFriend
SETINDEX 1, “FriendsByAge”
MOVEFIRST 1
DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)
SETINDEX 1, "“FriendsByHeight”
MOVEFIRST 1
DO
RETRIEVE 1, MyFriend
292 & Advanced BASIC
SETINDEX 1, "*FriendsByAge”
MOVEFIRST 1
SEEKEQ 1, 33
IF EOFC1) THEN
PRINT “No Match."
ELSE
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
END IF
SETINDEX 1, “FriendsByHeight”
MOVEFIRST 1
SEEKGT 1, 84
IF EOFC(1) THEN
PRINT "No Match."
ELSE
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
END IF
SETINDEX 1, “FriendsByAge"”
dO
SEEKGT 1, 40
MOVEFIRST 1
DO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
> Databasing 293
SETINDEX 1, “FriendsByName"
SEEKEQ 1, "Doug"
RETRIEVE 1, MyFriend
MyFriend.Age = 34
UPDATE 1, Myfriend
Setindex 1, "FriendsByAge”
dO
RETRIEVE 1, Myfriend : :
PRINT MyFriend.Name;"“'s age is: ";Myfriend.Age
MOVENEXT 1
LOOP WHILE NOT EOF(1)
CLOSE #1
At the end, note that we close this table (and therefore the file, since there’s only one
table in it) with CLOSE 1.
That’s it for our database: it’s not exactly a general purpose database (because it has no
user interface) but it has let us explore the ISAM system. Now, however, it’s time to move
on and take a look at the other ways we have of handling data in BASIC.
GAS
ees yrdemsipiecns
, | ee aed
a
q ;
¢. 5
’S i a;
: 3 a
>™ . - a=:
ae *
\ ¥
* "wr
7 4 rs
@& - =
a pA ie
( ~ -@
Ms ) “.
_ - bs
i = > * cea
oe fl
: Paes
be - ~ | ~ ee
E_ : 4 ; =ae 7 7
oe : opea >: i.
os s ® > we Os : =
= a : af - 7
— Py a we ey
. =.
Zz a a? Q od
7
b
, $ a — es Ba Nee
gee ral ee" Cee
p BOT
PFPaar ;
oe
‘ iT IES
ae
see cs
fats 210%; cil
Ale 4 *
py est ah dg)
“ss
,
Advanced Data
Handling and Sorting
eel
: sisd beonsvbA
~
IN THIS CHAPTER we'll work through just about all the ways there are of organizing
data in BASIC (and then we'll add a few of our own). Organizing your data for easy access
can be crucial in program development for speed in both program coding and execution.
In fact, organizing your data at the beginning may win you more than half the battle of
writing a program.
We'll work through the most helpful methods of arranging data, including arrays, data
structures, linked lists, circular buffers, and binary trees. Programmers — especially
advanced programmers — should be familiar with these common methods of organizing
data and not have to continually reinvent the wheel. And, at the end of the chapter, we'll
examine two fast sorting methods to get the most out of our data, as well as a fast
searching algorithm to search through sorted arrays.
BASIC Variables
The most elementary method of organizing data is by storing it in simple variables.
Here are the standard types used in BASIC:
We're familiar with all of these, except perhaps the new CURRENCY type available in
the BASIC PDS. You use that type to store amounts of money. Here’s an example (results
are printed out to the nearest cent):
Savingsa@ = 6000.00
Renta = 775.00
Fooda = 124.50
Bilisa = 513.72
This example prints out how much of your savings are left after paying rent and the
bills. For most purposes, you can think of a CURRENCY variable as a very long integer
with four decimal places added on to it as well (although the last two decimal places are
for internal accuracy only). We'll see more of the CURRENCY type in Chapter 11.
Note that the currency type can hold numbers over 400,000 times as large as
LONG integers with complete integer accuracy. This has led some program-
mers to supplant the LONG type with the CURRENCY type.
Sum& = 0
Product& = 1
FOR i = 1 TO 10
> READ Number&
Sum& = Sum& + Number&
NEXT i
PRINT "The sum of your data is:"; Sum&
RESTORE "Start data over
FOR 4 = 4 TO 416
READ Number&
Product& = Product& * Number&
NEXT i
PRINT “The product of your data is:"; Product&
DATA 1, 25°35, 4, 5,°6; 7,/8,°9; 10
Read it with READ (which reads a number from the DATA statement and moves on to the
next number for the next READ operation):
FOR i = 1 TO 10
> READ Number&
Sum& = Sum& + Number&
NEXT i
And then we can start the whole data list over again with RESTORE:
FOR i = 1 TO 10
READ Number&
Product& = Product& * Number&
NEXT i
This is one way of handling data. Of course, one of the purposes of putting numeric
data into a computer is to organize it, and the most common way of doing that is with the
next step towards higher data organization — arrays.
Arrays
We're all familiar with arrays, such as the one in this Listing 7.1.
Array(1, 1) = 10.00
Array(2, 1) = 53.00
Array(3, 1) = 7.17
Array(4, 1) = 9.67
Array(5, 1) = 87.99
Array(6, 1) = 14.00
Array(7, 1) = 91.19
Array(8, 1) = 12.73
Array(9, 1) = 1.03
Array(10, 1) = 5.04
Array(1, 2) = 9.67
300 P& Advanced BASIC
Sum1a@ = 0
Sum2a = 0
FOR i = 170 10
Sum1a = Sum1a + ArrayCi, 1)
Sum2@ = Sum2@ + Array(i, 2)
NEXT i
In this program, we're setting up an array of 10 rows and 2 columns to hold sales
values for the last two days:
Array(1, 1) = 10.00
Array(2, 1) = 53.00
Array(3, 1) = 7.17
Array(4, 1) = 9.67
Array(5, 1) = 87.99
Array(6, 1) = 14.00
Array(7, 1) = 91.19
Array(8, 1) = 12.73
Array(9, 1) = 1.03
Array(10, 1) = 5.04
Array(1, 2) = 9.67
Array(2, 2) = 3.5
Array(3, 2) = 8.97
Array(4, 2) = 10.00
Array(5,; 2) = 78.33
Array(6, 2) = 17.00
> Advanced Data Handling and Sorting 301
Array(7, 2) = 91.36
Array(8, 2) = 12.73
Array(9, 2) = 16.12
Array(10, 2) = 7.98
Array(1, 1) = 10.00
Array(2, 1) = 53.00
Array(3, 1) = 7.17
Array(4, 1) = 9.67
Array(5, 1) = 87.99
Array(6, 1) = 14.00
Array(7, 1) = 91.19
Array(8, 1) = 12.73
Array(9, 1) = 1.03
Array(10, 1) = 5.04
Array(1, 2) = 9.67
Array(2, 2) = 3.5
Array(3, 2) = 8.97
Array(4, 2) = 10.00
Array(5, 2) = 78.33
302 » Advanced BASIC
Array(6, 2) 17.00
Array(7, 2) 91.36
Array(8, 2) 12.73
Array(9, 2) wan
16.12
Array(10, 2) = 7.98
Sumia = 0
Sum2a = 0
FOR i = 1 TO 10 -
$um1a = Sumia + Array(i, 1)
Sum2@ = Sum2@ + Array(i, 2)
NEXT i
PRINT Sum18, Sum2a; " = Total”
Arrays don’t get very complex in BASIC, unlike in C, where array names are just
pointers (and two-dimensional array names are just pointers to pointers). There you can
really go wild, saving both time and memory by converting all array references to pointer
references — and making your code extremely hard to read at the same time.
As it is, we’ve gotten about as complex as arrays get in BASIC, so let’s move on to the
next most advanced way of organizing data after arrays: data structures.
Data Structures
We can group the standard data types together and come up with a whole new type of
our own. In fact, we’ve already used TYPE frequently throughout the book to define our
data structures InRegs and OutRegs which we use with INTERRUPT( ) or INTER-
RUPTX( ). We've also defined data records with TYPE in Chapter 6 on ISAM databases.
To define a TYPE named Person, we can do this:
TYPE Person
FirstName AS STRING * 20
LastName AS STRING * 20
END TYPE
*
2
This data structure just stores a person’s first and last names. We can set up a variable
of this type, or, even more powerfully, an array of variables of this type:
> Advanced Data Handling and Sorting 303
TYPE Person
FirstName AS STRING * 20
LastName AS STRING * 20
END TYPE
TYPE Person
FirstName AS STRING * 20
LastName AS STRING * 20
END TYPE
People(1).FirstName = "AL"
People(1).LastName = “Einstein”
People(2).FirstName = “Frank”
People(2).LastName = “Roosevelt”
People(2).FirstName = "Charlie”
People(2).LastName = “DeGaulle"
PRINT People(1).FirstName
One of the most common uses for TYPE in BASIC is to create structured
records that can be written to or read from a sequential access file (using write
# or input #). Giving all the variables in each record one uniform TYPE name
makes them much easier to handle (as we did in the previous chapter) and you
can reach each field with . operator.
But that’s not the end to working with data structures. If there is some connection
between the elements of our array, we can connect them into a linked list.
Linked Lists
Linked lists are good for organizing data items into sequential chains, especially if you
have a number of such chains to manage and want to use space efficiently.
They work this way: For each data item, there is also a pointer pointing to the next
data item. The last pointer in the chain is a null pointer with a value of 0 (so you know
the list is done when you reach it). (See Figure 7-2.)
304 » Advanced BASIC
To see what clusters a file is stored in, you get its first cluster number from the internal
data in its directory entry — let’s say that number is 2. That means that the first section of
the file is stored in cluster 2 on the disk. This number is also the key to theFAT for us. We
can find the next cluster occupied by the file by looking in cluster 2’s entry in the FAT (see
Figure 7-3).
FAT L
Entry# 2 3 4 = 6 7 8 Qn Oot 7 isan 2 13
3 4 6) G27 7>cEND 29"10 PEND? 90 0 0
Figure 7-3
That cluster’s entry in the FAT holds 3, which is the number of the next cluster that the
file occupies on the disk. To find the cluster after 3, check the entry in the File Allocation
Table for that cluster (See Figure 7-4).
FAT L
Entry# 2 3 4 5 6 if 8 9. Oa Laat
3 4 6. 392 J END (29 9s10 SEND 0 0
Figure 7-4
> Advanced Data Handling and Sorting 305
That holds 4, so the next section of the file is in cluster 4. To continue from there,
check the number in the FAT entry for cluster 4 (Figure 7-5).
FAT fe
ear
peewee AL 5) 68 7. 8 OO 11s fom
feces 6 52 007 END, 26° 10 wEND <Omemr0ee 0
Figure 7-5
That number is 6, and you continue on until you come to the end-of-file mark in the
FAT (Figure 7-6).
FAT L e
Entry# 2 3 4 5 6 7 8 SEOOREOE a GEE
3 & Bie fae ND 29 2 OL ENDO 0 0
Do eee
Figure 7-6
In other words, this file is stored in clusters 2, 3, 4, 6, and 7. Notice that 5 was already
taken by another file, which is also weaving its own thread of clusters through the FAT at
the same time.
Linked lists are used when you want to use memory or disk space efficiently
and have to keep track of a number of sequential chains of data.
When this file is deleted, its entries in the FAT can be written over, and those clusters
can be taken by another file.
Let’s see an example of a linked list in BASIC. For example, we might have the two
distinct career paths shown in Figure 7-7 to keep track of:
President Colonel
Supervisor Lieutenant
Figure 7-7
306 > Advanced BASIC
We can connect the various levels with a linked list. We start by setting up a variable of
type Person as follows:
— TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE
Now we fill the Rank fields — we can fill them in any order; the pointers will keep
them straight:
TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE
— People(1).Rank = “Supervisor”
People(2).Rank = "Major"
People(3).Rank = "“Director®
People(4).Rank = "President"
People(5).Rank = "Captain"
People(6).Rank = “Vice President"
People(7).Rank = "Colonel"
Peopte(8).Rank = “Lieutenant”
*
For each entry in People( ), there’s also a superior position. For example, the superior
of the entry in People(1), Supervisor, is in People(3), Director. To link the entries in each
of the two chains, we have to point to the superior rank by filling the pointers
Person( ).SuperiorPointer:
TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE
People(1).Rank = "Supervisor"
People(2).Rank = "Major"
People(3).Rank = “Director"
People(4).Rank = "President"
People(5).Rank = “Captain”
People(6).Rank = “Vice President”
People (7).Rank = "Colonel":
People(8).Rank = “Lieutenant"
People(1).SuperiorPointer
People(2).SuperiorPointer
cae
oe People(3).SuperiorPointer
People(4).SuperiorPointer
People(5).SuperiorPointer
People(6).SuperiorPointer
People(7).SuperiorPointer
People(8).SuperiorPointer wt Ww
Hniuwtniun
MOFNOAN
Now that all the items in the two lists are linked, we can accept a number, 1 or 2, and
work our way through the first or second linked list, printing out the various Rank names
as we go. (See Listing 7-2.)
TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE
People(1).Rank “Supervisor”
“Major”
People(2).Rank
People(3).Rank “Director”
People(4).Rank "President"
People(5).Rank “Captain”
People(6).Rank "Vice President”
People(7).Rank “Colonel”
People(8).Rank “Lieutenant”
tHeunradunnu
People(1).SuperiorPointer
People(2).SuperiorPointer
People(3).SuperiorPointer
People(4).SuperiorPointer
People(5).SuperiorPointer
People(6).SuperiorPointer
People(7).SuperiorPointer
People(8).SuperiorPointer Hnhuinonun
NOANW
MOF
. He
PRINT “Choose a life track, — ° 3 nN = tA
DO
308 Pb Advanced BASIC
InString$ = INKEYS
LOOP WHILE InString$S = ""
PRINT
— DO
PRINT People(Index).Rank ‘Print out results.
Index = People(Index).SuperiorPointer
LOOP WHILE Index <> 0
For example, asking this program to print out life track 1 results in this list:
Supervisor
Director
Vice President
President
Note that we must know in advance that chain 1 starts with entry 1 and chain 2 with
entry 8. We always need the first entry to use as a key to the first position in the linked list.
After that we can work our way up either chain of command; as we do so, we print out the
current Rank and get a pointer to (that is, the array index number of) the next Rank at the
same time.
Circular Buffers
There is another type of linked list that programmers often use — a list where the last
item points to the first one so the whole thing forms a circle. This is called a circular buffer.
The most well-known circular buffer in your computer is the keyboard buffer.
What happens there is that while one part of the operating system is putting key codes
into the keyboard buffer, another part of the operating system is taking them out. The
location in the buffer where the next key code will be placed is called the tail, and the
location where the next key code is to be read from is called the head. :
When keys are typed in, the tail advances. When they are read, the head does. As you
write to and read from the keyboard buffer, the head and tail march around (each data
> Advanced Data Handling and Sorting 309
location can be either the head or the tail). When the buffer is filled, the tail comes up
behind the head, and the buffer-full warning beeps.
You use circular buffers when some part of your program is writing data and
some other part is reading it, but at different rates. Store the location of the
head and tail positions, and, after you put data into the buffer, advance the tail.
When you take data out, advance the head. This way, you can use the same
memory space for both reading and writing.
The primary problem with linked lists, however, is that all access to their data is
sequential access. To find the last entry in a linked list, for example, you have to start at
the very first one and work your way back. That’s ok for files tracked through the FAT
(where you need every FAT entry before you can read the whole file), but it’s a terrible
method if you're only looking for a specific record. A better way is to make what’s called a
binary tree.
Binary Trees
Binary trees differ from linked lists in that, for the first time, we're going to start to
order our data. We can start with a linked list as shown in Figure 7-8. And then make it a
doubly linked list as shown in Figure 7-9.
es Oia sea i ee
| Pointer _| aero aa
Figure 7-8
| |
Left
Figure 7-9
310 P» Advanced BASIC
Now there are two pointers in each record: one scans up the chain, the other down.
Doubly linked lists have many uses in themselves, but this is still not a binary tree.
Instead, let’s put in some values for the data fields, -5, 0, and 2 (see Figure 7-10).
Notice that we’ve constructed a hierarchy based on data values here, arranging them
from left to right in increasing order (-5, 0, 2). The record with the data value closest to
the median data value becomes the root of our binary tree (see Figure 7-11).
Record 1 Record 3
Left
Increasing data values >
Figure 7-11
Because it has the data value closest to the middle of all three records, record 2 is the
root of our binary tree. If we wanted to find a record with a data value of, say, -5, we’d
start at the root, record 2, whose data value is 0. Since -5 is less than O, we would next
search the record to its left (since data values decrease to the left). This record is record 1
with a data value of -5, which means that we’ve found our target value.
This might seem like a small gain here, but imagine having a list like this:
> Advanced Data Handling and Sorting 311
Let's say that it is your job to coordinate this list and find a person with a specified age.
To construct a binary tree, you’d pick the person with an age as close to the median as
possible (Doug), and put the tree together like Figure 7-12.
Doug (33)
Cheryl (28) John (41)
Margo (27) Dennis (42)
Denise (23) Ed (46)
Nick (47)
Figure 7-12
Now you can start with Doug and just keep working until you find the age you
require. For example, to find the person who is 46 years old, start at Doug, who is 33.
Since 46 is greater than 33, continue moving to the right through John and Dennis to Ed,
who is the person we're searching for.
312 P Advanced BASIC
Binary trees are good for so-called “expert systems,” where each node corres-
ponds to a question and the two branches correspond to the answers yes and
no.
We should note that this is an extremely simple binary tree since, with the exception of
Doug, each node only has one way to go. In general, each node can go both ways. Let’s
put this example into code. We start off by defining a new Person TYPE which has two
pointers, one to the next older person, and one to the next younger:
+ TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE
TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE
~ People(1).fFirstName = “Denise”
People(1).Age = 23
os
ae People(1).NextYoungerPerson = 0
People(1).NextOlderPerson = 6
People(2).FirstName = “Ed”
People(2).Age = 46 :
People(2).NextYoungerPerson &
People(2).NextOlderPerson = Wt
People(3).FirstName = “Nick”
People(3).Age = 47
People(3).NextYoungerPerson = 2
> Advanced Data Handling and Sorting 313
People(3).NextOlderPerson 0
People(4).FirstName = "Dennis"
People(4).Age = 42
People(4).NextYoungerPerso re
People(4).NextOlderPerson 2
Peopte(5).FirstName = “Doug”
People(5).Age = 33
People(5).NextYoungerPerson 8
People(5).NextOlderPerson 7
People(6).FirstName “Margo”
People(6).Age = 27
4
People(6).NextYoungerPerson
People(6).NextOlderPerson 8
People(7).FirstName
People(7).Age = 41
People(7).NextYoungerPerson 5
People(7).NextOlderPerson = 4
People(8).FirstName “Cheryl”
People(8).Age = 28
Peopte(8).NextYoungerPerson 6
People(8).NextOlderPerson = 5
Now we can search for the first person who is 46 years old. First, we start off at the
root:
TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE
People(1).FirstName = "Denise"
People(1).Age = 23
People(1).NextYoungerPerson 0
People(1).NextOlderPerson
People(2).FirstName = "Ed"
People(2).Age = 46
People(2).NextYoungerPerson
People(2).NextOlderPerson
People(3).FirstName
People(3).Age = 47
314 P» Advanced BASIC
People(3).NextYoungerPerson 2
People(3).NextOlderPerson = 0
People(4).FirstName = “Dennis”
People(4).Age = 42
People(4).NextYoungerPerson = 7
People(4).NextOlderPerson = 2
People(5).FirstName = “Doug"
People(5).Age = 33
People(5).NextYoungerPerson = 8
People(5).NextOlderPerson = 7
People(6).FirstName = “Margo”
People(6).Age = 27
People(6).NextYoungerPerson = 1
People(6).NextOlderPerson = 8
People(7).FirstName = "John"
People(7).Age = 41
People(7).NextYoungerPerson = 5
People(7).NextOlderPerson = 4
People(8).FirstName = "Cheryl"
People(8).Age = 28
People(8).NextYoungerPerson = 6
People(8).NextOlderPerson = 5
CurrentRecord%’ = BinaryTreeRoot%
TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE
People(1).FirstName = “Denise”
People(1).Age = 23
People(1).NextYoungerPerson = 0
Peopie(1).NextOlderPerson = 6
People(2).firstName = "Eq"
People(2).Age = 66
> Advanced Data Handling and Sorting 315
People(2).NextYoungerPerson = 4
People(2).NextOlderPerson = 3
People(3).FirstName = "Nick"
People(3).Age = 47
People(3).NextYoungerPerson = 2
People(3).NextOlderPerson = 0
People(4).FirstName = "Dennis"
People(4).Age = 42
People(4).NextYoungerPerson = 7
People(4).NextOlderPerson = 2
People(5).FirstName = “Doug”
People(5).Age = 33
People(5).NextYoungerPerson = 8
People(5).NextOlderPerson = 7
People(6).FirstName = “Margo”
People(6).Age = 27
People(6).NextYoungerPerson = 1
People(6).NextOlderPerson = 8
People(7).FirstName = “John"
People(7).Age = 41
People(7).NextYoungerPerson = 5
People(7).NextOlderPerson = 4
People(8).FirstName = "Cheryl"
People(8).Age = 28
People(8).NextYoungerPerson = 6
People(8).NextOlderPerson = 5
CurrentRecord% = BinaryTreeRoot%
— BO
z IF People(CurrentRecord%).Age = 46 THEN
PRINT "That person is: "; People(CurrentRecord’). FirstName
:
EXIT DO
END IF
want
If not, then we have to compare the current person's age to 46. If it’s less, then we
looks like this:
the NextOlderPerson. If greater, then we want the NextYoungerPerson. That
316 P Advanced BASIC
TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE
People(1).FirstName = "Denise"
People(1).Age = 23
People(1).NextYoungerPerson = 0
People(1).NextOlderPerson = 6
People(2).FirstName = "Ed"
People(2).Age = 46
People(2).NextYoungerPerson = 4
People(2).NextOlderPerson = 3
People(3).FirstName = "Nick"
People(3).Age = 47
People(3).NextYoungerPerson 2
People (3).NextOlderPerson = 0
People(5).FirstName = "Doug"
People(5).Age = 33
People(5).NextYoungerPerson = 8
People(5).NextOlderPerson = 7
People(6).FirstName = “Margo”
People(6).Age = 27
People(6).NextYoungerPerson 1
People(6).NextOlderPerson = 8
People(8).FirstName = “Cheryl
People(8).Age = 28
People(8).NextYoungerPerson = 6
People(8).NextOlderPerson = 5
CurrentRecord% = BinaryTreeRoot% D0
IF People(CurrentRecord%).Age = 46 THEN
PRINT “That person is: "; People(CurrentRecord%). FirstName
EXIT DO
END IF
~ IF People(CurrentRecord%) .Age > 46
THEN
CurrentRecord% = People(CurrentRecord%) .NextYounger
Person
> Advanced Data Handling and Sorting 317
ELSE
CurrentRecord%’ = People(CurrentRecord%) .NextOlderPerson
END IF
LOOP WHILE CurrentRecord% <> 0
And that’s how to search through a binary tree — we just keep going until we find what
we're looking for (or we run out of branches).
With binary trees, we've started ordering our data. That is, we've established the relative
position of a record with respect to its two neighbors. But what if we wanted to sort all the
data? Sorting data is, of course, a very common thing to do; it’s the next, more advanced
step in organizing our data. And, because it’s so common, we should explore it in some
detail. To do that, we'll work through two of the fastest algorithms available, shell sorts and
the Quicksort. And we'll put them to work.
It's hard to know which sorting routine will work best for your application before
you see it in action. For that reason, you should test both of the following
methods on your data.
Shell Sorts
The standard shell sort is always popular among programmers. It works like this: say
you had a one-dimensional array with these values in it:
DD nee Re Br ae|
To sort this list into ascending order, divide it into two partitions as shown in Figure
7-13.
8765 4321
eee, ee Se
Figure 7-13
Then compare the first element of the first partition with the first element of the second (see
Figure 7-14).
Figure 7-14
318 > Advanced BASIC
In this case, 8 is greater than 4, so we switch the elements, and go on to compare the
next pair (Figure 7-15).
L L
4765 8321
a [ears aie
Figure 7-15
Again, 7 is greater than 3, so we switch and go on (Figure 7-16).
L L
4365 8721
Figure 7-16
We also switch 6 and 2 and then look at the last pair as shown in (Figure 7-17).
1 L
4325 87 64
Lae
Figure 7-17
After we switch them too, we get this as the new list:
While this is somewhat better than before, we’re still not done. The next step is to divide
each partition itself into two partitions, and repeat the process, comparing 4 with 2 and 8
with 6 (see Figure 7-18).
Re ye oO
Lo aa =e Bae
Figure 7-18
We switch both pairs and go on, comparing 3 with 1 and 7 with 5 (Figure 7-19).
ee A het
23416785
ee nl a
Figure 7-19
Again, we switch the second set of two pairs, leaving us with this:
27047 3,6-9 8-7
> Advanced Data Handling and Sorting 319
This looks even closer. Now the partition size is down to one element, which means that
this is the last time we'll need to sort the list. We need to compare the first, and only,
element in each partition with the first, and only, element in the next partition. Here that
means that we compare elements 2, 4, 6, and 8 with elements 1, 3, 5, and 7. When we
swap them all, we get:
i 2a a OL. 8
And that is how the standard shell sort works — at least if there’s an even number of
items to sort (in which case breaking them up into balanced partitions is easy). The case
where we have an odd number of elements is slightly more difficult. For example, if we had
a list of nine elements to sort, we would start by breaking them up into two partitions as
shown in Figure 7-20 (note that there is no last element in the second partition).
98765 4-32 1x
| | Ls Sa
Figure 7-20
Now we'd compare as before, switching as necessary, until we try to compare a value in
the first partition to a value in the second partition that isn’t there (see Figure 7-21).
J J
432 1-5 9876x
| ae
Figure 7-21
In this case, we just don’t perform any comparison (i.e., there is no value in the x
position that might have to be placed earlier in the array). Instead, we just continue on to
the next smaller partition size. We keep going as before, working until the partition size
becomes 1, perform the final switches, and then we're done.
Now let’s see this in code. We start off by dimensioning an array and filling it with values
(which are as out of order as they can be):
Array(1)
Array(2)
Array(3)
Array(4)
Array(5)
Array(6)
Array(7)
Array(8)
Array(9) nh
nnn
HH
aneDO
©PNW
EUAN
320 P& Advanced BASIC
We can also print those values out so they can be compared with the sorted list later:
Array(1)
Array(2)
Array(3)
Array(4)
Array(5)
Array(6)
Array(7)
Array(8)
Array(9) vw
tf
ak
nw
wf UDAN
O&O
ANUS
Now we have to implement our shell sort. In this type of sorting routine, we loop over
partition size (PartitionSize% below), so let’s set that loop up first:
Array(1) = 9
Array(2) = 8
Array(3) = 7
Array(4) = 6
Array(5) = 5
Array(6) = 4
Array(7) = 3
Array(8) = 2
Array(9) = 1
NumItems% = UBOUND(Array, 1)
PartitionSize% = INTC(NumItems% + 1) / 2)
> Advanced Data Handling and Sorting 321
= bo
In this loop, we loop over partition size, cutting it in half each time through (see Figure
7-22).
4325 8.7641 Current Partition Size
Anoreet 8765
eg ean Next Partition Size
Figure 7-22
For every partition size, however, the list is broken up into a different number of
partitions, and we have to loop over those partitions so we can compare elements in the
current partition to the elements of the next one (Figure 7-23).
4321 8765
ler Eee Next Partition Size
Figure 7-23
The loop over partitions looks like this (note that when we're done with each partition,
we cut the partition size in half):
Array(1) = 9
Array(2) = 8
Array(3) = 7
Array(4) = 6
Array(5) = 5
Array(6) = 4
Array(7) = 3
Array(8) = 2
Array(9) = 1
PRINT " i Array(id"
PRINT Sees oS 8 eerane .
FOR i = 170 9
PRINT i, Array(id
322 » Advanced BASIC
NEXT i
PRINT
PRINT “Sorting...”
Numitems% = UBOUND(Array, 1)
PartitionSize% = INTC(NumItems% + 1) / 2)
dO
_ NumPartitions% = (NumItems% + 1) / PartitionSize%
Low% = 1
FOR i = 1 TO NumPartitions% - 1
en
se
NEXT i
PartitionSize% = PartitionSize% \ 2
LOOP WHILE PartitionSize% > 0
e
«
*
Finally, we have to loop over each element in the current partition, comparing it to the
corresponding element in the next partition (see Figure 7-24).
J
he3258761
[oe ebb
Figure 7-24
Array(1)
Array(2)
Array(3)
Array(4)
Array (5)
Array(6)
Array(7)
Array(8)
Array(9) HH
UR VION
Hu OOO
NUE
—
PRINT “ i Array¢i)"
PRINT "——— eee *
> Advanced Data Handling and Sorting 323
FOR i = 1 T0 9
PRINT i, Array(i)
NEXT i
PRINT
PRINT “Sorting...”
NumItems% = UBOUND(Array, 1)
PartitionSize% = INTC (NumItems% + 1) / 2)
BO
NumPartitions% = (NumItems% + 1) / PartitionSizez
Low% = 1
FOR i = 1 TO NumPartitions% - 1
HighX = Low% + PartitionSize% - 1
IF HighX% > NumItems% - PartitionSize% THEN High% = _
NumItems% - PartitionSizez
> FOR j = Low% TO High%
IF Array(j) > Array(j + PartitionSize%) THEN
°
.
> END IF
NEXT j
Low% = Low% + PartitionSizez
NEXT i
PartitionSize% = PartitionSize% \ 2
LOOP WHILE PartitionSize% > 0
If it turns out that the element in the later partition is smaller than the element in the
current one, we have to swap them, which we can do handily with the BASIC SWAP
statement:
Array(1) = 9
Array(2) = 8
Array(3) = 7
Array(4) = 6
Array(5) = 5
Array(6) = 4
Array(7) = 3
Array(8) = 2
Array(9) = 1
NumItems% = UBOUND(Array, 1)
PartitionSize% = INTC (NumItems% + 1) / 2)
dO
NumPartitions% = (NumItems% + 1) / PartitionSize%
Low% = 1
FOR i = 1 TO NumPartitions% - 1
High% = Low% + PartitionSize% - 1
If High% > NumItems% - PartitionSize% THEN High% = _
NumItems% ~ PartitionSize%
FOR j = Low% TO High%
IF Array(j) > Array(j + PartitionSizez) THEN
> SWAP Array(j), Array(j + PartitionSize%)
END IF
NEXT j
Low% = Low% + PartitionSizes
NEXT i
PartitionSize% = PartitionSize% \ 2
LOOP WHILE PartitionSize% > 0
And that’s it. We loop over partition sizes, over each partition, and over each element in
the current partition, swapping it with its counterpart in the next partition if necessary. At
the end, we can print out the newly sorted array. Listing 7-3 shows the whole program.
Array 1)
Array(2)
Array(3)
Array(4)
Array(5)
Array(6)
Array(7)
Array(8)
Array (9) nuh
tt O&O
Hw
eu EUAN
ANU
FORi= 4.10 9
PRINT i, Array(i)d
NEXT i
PRINT
PRINT “Sorting...”
NumItems% = UBOUND(Array, 1)
PartitionSize% = INTC (NumItems% + 1) / 2) '
> Advanced Data Handling and Sorting 325
PRINT
PRINT “ i Array(i>"
PRINT 9s atten nal
FOR i = 1 T0 9
PRINT i, ArrayCi)
NEXT i
We can even do the same thing for two-dimensional arrays. In that case, we simply sort
the array on one of its columns. For example, we can adapt the above program to handle a
two-dimensional array by adding a column index (Col%) to Array( ) (see Listing 7-4).
Array(1, 1)
Array(2, 1)
Array(3, 1)
Array(4, 1)
Array(5, 1)
Array(6, 1)
Array(7, 1)
Array(8, 1)
Array(9, 1) inner
nunt
un PFBANWEUAN@O
PRINT “ 7 Array(i,1)"
PRINT eee Dap cn oa ee x
FOR i a 10.9
PRINT i, Array(i, 1)
NEXT i
PRINT
326 P& Advanced BASIC
PRINT :
PRINT " i Array(i,1)"
PRINT “=—— eee eee x
FOR i = 1 TO 9
PRINT i, Arrayfi, 1)
NEXT i ‘
Quicksorts
Besides shell sorts, another popular sorting algorithm is the Quicksort. That sorting
routine works like this: first we find a key, or test, value to compare values to. The best
value here would be the median value of the elements of the array, but in practice a random
entry is usually chosen. Here, we'll choose a value from the center of the array.
Then we divide the array into two partitions: those less than the test value, and those
greater. We move upwards in the array until we come to the first value that is greater than
the test value, and down the array (starting from the end) until we find a number less than
the test value. Then we swap them. We keep going until all the numbers in the first
partition are less than the test value, and all the numbers in the second partition are greater.
Next, we do the same thing to each partition: we select a new test value from each
partition and break that partition into two new partitions. One of those new partitions
> Advanced Data Handling and Sorting 327
holds the numbers less than that test value, while the other holds those ugreater. We keep
going in that way, splitting partitions continuously until there are just two numbers in a
partition, at which point we compare and switch them if necessary.
You may have noticed that each subsequent step is itself a QuickSort. That is, to start,
we divide the array into two partitions less than and greater than the test value, then take
each partition and break it into two partitions depending on a new test value, and so on.
In this way, QuickSorts lend themselves easily to recursion, and that’s the way they’re
usually coded. And the QuickSort we'll develop here is no exception.
If the term recursion is new to you, you should know that it just refers to a routine that
calls itself. If a programming task can be divided into a number of identical levels, it can
be dealt with recursively. Each time the routine calls itself, it deals with a deeper level.
After the final level is reached, control returns through each successive level back to the
beginning. Let’s see how this looks in code.
Since this routine is recursive, we will set up a subprogram called SortQuick( ) to call
from the main program (this subprogram will call itself repeatedly):
We just pass the array name to sort, the index to start sorting from (SortFrom%) and
the index to sort to (SortTo%). Working this way will be useful when we have to sort a
particular partition in the array.
In SortQuick( ), we first handle the final case; that is, a partition of only two elements:
In this case, we just compare each element to its neighbor (the only other element in
this partition) and swap them if needed. That’s all there is to the final case in the Quick-
sort algorithm.
If the partition size is greater than two, however, we have to sort the values from
Array(SortFrom%) to Array(SortTo%) according to a test value, dividing the elements
into two new partitions, and then call SortQuick( ) again on each new partition. Let’s see
how that works. First, we pick a test value, then we divide the present partition into two
partitions on the basis of it.
We start by moving up from the bottom of the partition, swapping any values that we
find are greater than the test value:
~ bO
REM Split into two partitions
And we also scan from the top of the partition down in the same loop, looking for
the
first value that’s smaller than the test value:
DO
REM Split into two partitions
We keep going until i and j meet, at which time we’ve created our two new partitions.
Next we can call SortQuick( ) again for each of the resulting partitions (which may be of
unequal size):
dO
And that’s all there is to it; the sort will continue recursively until we get down to the
final case of a partition size of 1, the final swaps will be done if necessary, and then we’re
finished. Listing 7-5 shows the whole thing.
Array(1) = 9
Array(2) = 8
Array(3) = 7
Array(4) = 6
Array(5) = 5
Array(6) = 4
Array (7) = 3
Array(8) = 2
Array(9) = 1
PRINT “ 4 Array(i)"
PRINT "%--~— Sost tty
FOR i = 1 TO 9
PRINT i, ArrayCi)
NEXT i
PRINT
PRINT “Sorting...”
PRINT
PRINT ” j Array(i)"
PRENT mm tt ss 5
FOR i=1 TO 9
PRINT i, Array(i)d
NEXT i
dO
END IF
END SUB
Just as with out shell sort program, we can adapt the Quicksort program to work with
a two-dimensional array, sorting on the values in one of its columns (the column number
is given in Col%):
Array(1, 1)
Array(2, 1)
Array(3, 1)
Array(4, 1)
Array(5, 1)
Array(6, 1)
Array(7, 1)
Array(8, 1)
Array(9, 1) unnuvnrkhunun
DO
NWEUAN
=
PRINT “ i Array(i,1)"
PRINT “=== 0 wee nnna=- -
FOR i = 170 9
332 P» Advanced BASIC
PRINT
PRINT “Sorting..."
PRINT
PRINT “ ji Array(i,1)"
PRINT "==~ aaaa
FOR i = 1 TO 9
PRINT i, ArrayCi, 1)
NEXT i
DO
END IF
END SUB
That's it for sorting. Both the shell sort and the Quicksort are pretty fast. The one you
should use will depend on your application. You might want to try them both and
use the
faster of the two.
> Advanced Data Handling and Sorting 333
Array(1) 9
Array(2)
Array(3)
Array(4)
Array(5)
Array(6)
Array(7)
Array(8)
Array (9) innnrnnunne
ON
UMW
ANAK
FOR i = 1 TO UBOUND(CArray,1)
IF ArrayCi) = 1 THEN
PRINT "Value of 1 in element";i
END IF
NEXT i
We just keep scanning up the list of values until we find what we’re looking for. On the
other hand, we can be more intelligent when searching a sorted list. For example, if our
sorted array had these values in it:
Pee eC tO 2°13: 14 15
and we were searching for the entry with 10 in it, we could start off in the center of the
list as shown in Figure 7-25.
| |
1234567891011 12 13 14 15
Figure 7-25
Since 10 is greater than 8, we divide the upper half of the array in two and check the
midpoint again (see Figure 7-26).
334 P Advanced BASIC
1234567891011 lent
Figure 7-26
The value we’re looking for, 10, is less than 12, so we move down and cut the
remaining distance in half (Figure 7-27).
Cat ae
1.234567 89 10 11 1213 14 15
Figure 7-27
In this way, we've zeroed in on our number, cutting down the number of values we
have to check. Let’s see how this looks in a program.
First, we set up our array. In this example, let’s search an array of 9 elements for the
entry with 8 in it:
Array(1)
Array(2)
Array(3)
Array(4)
Array(5)
Array(6)
Array(7)
Array(8)
Array(9) Hit
not
how
W
ot =OCGCNAURWNH
— SearchValue% = 8
PRINT “Searching the ordered list for the value
*
8."
.
Now we cut the array into two partitions and check the test value that is right
between
them, at position TestIndex%:
Array(1)
Array(2)
Array(3)
Array(4)
Array(5) oH
Wow
ow
ot WN
Vt
> Advanced Data Handling and Sorting 335
Array(6)
Array(7)
Array(8)
Array(9) HUH
OON
SearchValuex = 8
PRINT "Searching the ordered list for the value 8."
—+ Partition% = CUBOUND(Array, 1) + 1) \ 2
Testindex% = Partition%
*.
Then we need to start searching. We will keep looping over partition size: if the
partition becomes 0 without any success, then the value we're looking for isn’t in the
array:
Array(1) 1
Array(2) 2
Array(3) 3
Array(4) 4
Array(5) 5
Array(6) 6
Array(7) 7
Array(8) 8
Array(9) 9
SearchValuez = 8
PRINT "Searching the ordered List for the value 8.”
Partition% = (UBOUND(Array, 1) + 1) \ 2
TestIindex% = Partition%
— BO
Partition% = Partition% \ 2
Let’s first check to see if we’ve found our value, and, if so, we can quit:
336
el Pb Advanced
ha hhh BASIC eens setsceo
Array(1) = 1
Array (2) = 2
Array(3) = 3
Array(4) = 4
Array(5) = 5
Array(6) = 6
Array(7) = 7
Array(8) = 8
Array(9) = 9
SearchValue% = 8
PRINT “Searching the ordered list for the value 8."
Partition% = CUBOUND(Array, 1) + 1) \ 2
TestiIndex% = Partitions
DO
Partition% = Partitions 2
> IF Array(TestIndex%) = SearchValtue% THEN
PRINT “Value of"; SearchValues; “in element"; Testindex%
EXIT bo
END IF
.
«
If we haven't found our value, we have to go on to the next iteration of the loop, setting
TestIndex% to the middle of either the higher or lower partition, and then dividing that
partition into two new partitions. If the search value is bigger than the value at our
current location in the array, we want to move to the partition at higher values:
Array (1)
Array(2)
Array(3)
Array(4)
Array(5)
Array(6)
Array(7)
Array(8)
Array (9) hunni
ti
uanun
OOnNAUPWN
SearchValueZ% = 8
PRINT “Searching the ordered list for the value 8."
> Advanced Data Handling and Sorting 337
Partitiong = CUBOUND(Array, 1) + 1) \ 2
Testindex% = Partition%
dO
Partition% = Partition% \ 2
IF Array(TestIndex%) = SearchValue% THEN
PRINT “Value of"; SearchValuex; “in element"; TestIndex%
EXIT DO
END IF
_) IF Array(TestIndex%) < SearchValue% THEN
TestIndex% = TestIndex% + Partition%
But if the search value is smaller, on the other hand, we want to move to the lower
partition (which holds lower values):
Array(1)
Array(2)
Array(3)
Array(4)
Array(5)
Array (6)
Array(7)
Array(8)
Array(9) ttn
nnnnnn
WO
=
ONAURWN
SearchValuez% = 8
PRINT “Searching the ordered list for the value &.°
Partition% = (UBOUND(Array, 1) + 1) \ 2
Testindex% = Partition%
dO
Partition% = Partition% \ 2
IF Array(Testindex%) = SearchValue% THEN
“Value of"; SearchVatue%; “in element"; TestIndex%
PRINT
EXIT bO
END IF
IF Array(TestIndex%) < SearchValue% THEN
Testindex% = TestIndex% + Partition%
~ ELSE
TestIndex% = TestIndex% ~ Partition%
END IF
LOOP WHILE Partition% > 0
*
338 P Advanced BASIC
And that’s almost all there is. We just keep going until we find what we’re looking for, or
the partition size becomes 0, in which case it’s not there.
If we were unsuccessful, however, there are two last remaining tests that we should
apply: we should check the value we're searching for against the very first and last entries in
the array. That is, our algorithm demands that all numbers that it checks be straddled by
two other values, and that’s true of every element in the array except for the first and last
ones. That means that if we didn’t find what we were looking for, we have to check these
last two values explicitly (see Listing 7-8).
Array(1) = 1
Array(2) = 2
Array(3) = 3
Array(4) = 4
Array(5) = 5
Array(6) = 6
Array(7) = 7
Array(8) = 8
Array(9) = 9
SearchValue% = 8
PRINT “Searching the ordered list for the value 8."
Partitions = CUBOUND(Array, 1) + 1) \ 2
Testindex% = Partition%
dO
Partition% = Partition% \ 2
IF Array(TestIndex%) = SearchValue% THEN
PRINT “Value of"; SearchValue%; “in element"; Testindex%
EXIT po
END IF
IF Array(Testindex%) < SearchValue% THEN
Testindex% = TestIndex% + Partition%
ELSE
Testindex% = TestIndex% - Partition%
END IF
LOOP WHILE Partition% > 0
Conclusion
That’s it for data handling and sorting. We’ve seen most of the popular ways of han-
dling numeric data in this chapter. When we're designing code, it’s always important to
organize our data correctly. As mentioned earlier, that can be half the battle of writing a
program.
Soon we'll be ready to start our look at low-level programming and assembly language,
but before we do, let’s see how to debug our programs in the next chapter.
7
7 Po _
Heat " -b a
\7 A there “x ' ao ¥
1} rt ' or & ‘
a
Pees
>
Pas ’
* ,«
h
- ie - St “coer gol eben
<, ¥ e hy TS 4 és gt pote ae is
rc)
~~
~*
¢
a
=
e a
ns
'
o es 45
4 »
’
tars - e 4
a 6 we =.
‘ . 7
@
o Fa 7 »'
ade ®
[i Yow
a
o~
a i. 43
7 r, /% , ‘
a
, yr
bye. +"
@
o -
ar fd ?
7
>
»
yo" iad
— vew
F .) a sy
ere
7
Dy
:
ya
o>
a
al
Debugging
341
> Debugging 343
For debugging the assembly language programs in the next chapter, a good
choice is DEBUG.COM, which came free with DOS.
QuickBASIC Debugging
For the purposes of this chapter, let’s set ourselves the task of alphabetizing 10 or so
names, like these:
John
Tim
Edward
Samuel
Frank
Todd
George
Ralph
Leonard
Thomas
We can start by setting up an array to hold all the names:
344 Pb Advanced BASIC
DIM Names(10)
AS STRING
Names(1) = “John"
Names(2) = "Tim"
Names(3) = “Edward"
Names(4) = "Samuel"
Names(5) = “Frank"
Names(6) = “Todd”
Names(7) = "George"
Names(8) = “Ralph”
Names(9) = "Leonard"
Names(10) = “Thomas”
«*
Then we arrange them in alphabetical order with these BASIC instructions (this part of
the code has two bugs in it):
Names(1) = “John"
Names(2) = “Tim”
Names(3) = "Edward"
Names(4) = “Samuel"
Names(5) = “Frank"
Names(6) = “Todd"
Names(7) = “George"
Names(8) = "Ralph"
Names(9) = “Leonard”
Names(10) = “Thomas"
— FOR i = i TO 10
; FOR j = i TO 10
; IF Names(i) > Names(j) THEN
Temp$S = Names(i)
Names(j) = Names(j)
Names(j) = Temps
END IF
NEXT j
NEXT i
Names(1) = “John"
Names(2) = "Tim"
Names(3) = “Edward"
Names(4) = "Samuel"
Names(5) = "Frank"
Names(6) = “Todd"
Names(7) = "George"
Names(8) = “Ralph"
Names(9) = “Leonard"
Names(10) = "Thomas"
ee > Debugging
ica lk 345
did
FOR i = 1 TO 10
FOR j = 4 TO 10
IF Names(i) > Names(j) THEN
Temp$ = Names(i)
Names(j) = Names(j)
Names(j) = Temps
END IF :
NEXT j
NEXT i
—> FOR k = 1 TO 10
PRINT Names(k)
NEXTk
John
Tim
Tim
Tim
Tim
Todd
Todd
Todd
Todd
Todd
It’s time to debug. If we were using QuickBASIC, that process is easy to start. Figure
8-1 shows what the QuickBASIC window looks like with our program (called, say,
ALPHA.BAS) already loaded:
File Edit View Search Run Debug Calls Options Help
Names(5) = “Frank”
Names(6) = “Todd”
Names(7) = “George”
Names(8) = “Ralph”
Names(9) = “Leonard”
Names(10) = “Thomas”
FOR i =i TO 10
FOR j =iTO 10
IF Names(i) > Names(j) THEN
Temp$ = Names(i)
Names(j) = Names(j)
7 pera) = Temp$
F
346 b> Advanced BASIC
NEXT j
NEXT i
tmmediate—_ i——@7--__—_
Figure 8-1
All we have to do is to move up to the menu bar at the top of the screen and select the
Debug option. A menu like the one in Figure 8-2 opens.
File Edit View Search Run Debug Calls Options Help
Add Watch...
Names(4) = “Samuel” Instant Watch...
Names(5) = “Frank” Watchpoint...
Names(6) =“Todd” Delete Watch...
Names(7) = “George” Delete All Watch
Names(8) = “Ralph”
Names(9) = “Leonard” Trace On
Names(10) = “Thomas” History On
END IF
NEXTj
Figure 8-2
The obvious problem in our program is that the entries in the Names( ) array are being
filled incorrectly. We can watch the array being filled with the Add Watch... option. We
select that option and a window opens, asking for the expression to be watched (see
Figure 8-3).
Names(j) =Temp$
END IF
NEXTj
NEXT i
Immediate
Figure 8-3
The two Names( ) references in our program are Names(i) and Names(j), and we can
watch both of them. We just type “Names(i)” in the Add Watch dialog box to watch
Names(i) and then repeat the process for Names(j). The display changes, as shown in
Figure 8-4, with the current value of Names(i) and Names(j) in the top of the window.
FOR i =i TO 10
FOR j =i TO 10
If Names() > Names(j) THEN
Temp$ = Names(i)
Names(j) =Names(j)
Names(j) =Temp$
ND IF
Immediate
Figure 8-4
Now we'll be able to track the values in Names(i) and Names(j) when we stop the
program midway through its execution. And we can do that by setting breakpoint. A
348 > Advanced BASIC
breakpoint halts program execution when it is reached. For example, we can set a breakpoint
by moving the cursor on the screen down to the line that reads IF Names(i) > Names(j)
THEN and pressing F9 (or selecting Toggle Breakpoint in the Debug menu). A line appears
on the screen to indicate that a breakpoint has been set (see Figure 8-5).
A program is not limited to one breakpoint — you can put as many in as you
like. For example, if you’re having trouble with the value of some variable, one
powerful technique is to break at every place that value is changed so you can
see what's going on.
FOR i =i TO 10
FOR j=i TO 10
IF Names(i) > Names(j) THEN
Temp$ = Names(i)
Names(j) = Names(j)
Names(j) = Temp$
END IF
Figure 8-5
Now we run the program by selecting Start in the Run menu. Program execution con-
‘Unues until we reach the breakpoint and then stops. We look at the displayed values of
Names(i) and Names(j), only to find that they are unchanged (in the Watch window at the
top). (See Figure 8-6.)
> Debugging 349
FOR i =i TO 10
FOR j =i TO 10
IF Names(i) > Names(j) THEN
Temp$ = Names(i)
Names(j) = Names(j)
Names(j) = Temp$
END IF
Figure 8-6
In other words, the line IF Names(i) > Names(j) THEN is comparing nothing. The
values in Names(i) and Names(j) are not valid. At this point in the program, the begin-
ning, both i and j are supposed to point to the first element in the array. That is, both i
and j should be 1. We can check the value of i simply by placing the cursor on it and
selecting the Instant Watch... option in the Debug menu (see Figure 8-7).
END IF
NEXT j
NEXT i
Immediate—£$—@£—M———————
Figure 8-7
350 P& Advanced BASIC
And a window shown in Figure 8-8 appears, displaying the current value of i.
File Edit View Search Run Debug Calls Options Help
ALPHABAS Names
ALPHABAS Names(j):
Names(8) = “Ralph”
Neiest? = a Boner
Names(10)
- Expression
FOR i= 1 |I |
FO
Valu
Ucaeepaeney|
<Add Watch> <Cancel> <Help>
NEXTj
NEXT i
Immediat
Figure 8-8
We see that i= 0, which is a problem. This line in the code must be changed to FORi =
1 to 10 because we need to initialize i before using it:
— FOR i = 4 TO 40 :
FOR j = i To 10 oo
IF Names(i) > anes THEN
: Temp$ = Names(i)
Names(j) = Names(j)
Names(j) = Temp$ i
END IF
NEXT j
EE ee Lae >lahat
Debugging 351
| lt Maal
NEXT i
FOR k = 1 TO 10
PRINT Names(k)
NEXT k
However, when we make the change and run the program, we still see:
John
Tim
Tim
Tim
Tim
Todd
Todd
Todd
Todd
Todd
Obviously there is still a problem. Let’s examine the part of the program where the
actual elements are switched, the only other part of the program. We can put a breakpoint
in at the end of the element switching section as shown in Figure 8-9.
File Edit View Search Run Debug Calls Options Help
ALPHABAS Namesth.
ALPHABAS Names(j
Names(8) = “Ralph”
Names(9) = “Leonard”
Names(10) = “Thomas”
FOR i=i TO 10
FOR j=i TO 10
IF Names(i) > Names(j) THEN
Temp$ = Names(i)
sredlees|| = Names(j)
eo —— Nameoni] =Temp$—@_@__—____———_—<_ cm ——
END IF
NEXT j
NEXT i
—$ | mm edit A —_—_ i —__—_
Figure 8-9
352 b& Advanced BASIC
And then we run the program. Execution halts at the breakpoint, and we examine
Names(i) and Names(j) (see Figure 8-10).
File Edit View Search Run Debug Calls Options Help
Names(8) = “Ralph”
Names(9) = “Leonard”
Names(10) = “Thomas”
FOR i =i TO 10
FOR j =i TO 10
IF Names(i) >Names(j) THEN
Temp$ = Names(i)
Names(j) = Names(j)
haiti j) = Temp $A o—§—$€~@_OAO§«A=—@Ar_O__@|_ iiix§_c§_
NEXT i
ince. ———
Figure 8-10
The exchange of array elements is supposed to go like this: we take the value in
Names(i) and place it in Temp$. Then we copy the element in Names(j) and place it into
Names(i). The final step is to move Temp$ into Names(j). At this breakpoint, all but the
final step has been taken: We are about to move the value in Temp$ into Names(j).
In other words, we’d expect Names(i) and Names(j) to hold the same value, but they
do not. Names(i) holds John and Names(j) holds Edward. Something is wrong. If we look
back one line in our code, we see this line:
Names(2) = "Tim"
Names(3) = “Edward”
Names(4) = “Samuel”
Names(5) = "Frank"
Names(6) = "Todd"
Names(7) = "George"
Names(8) = "Ralph"
Names(9) = “Leonard”
_ Names(10) = “Thomas”
FOR i = 1 TO 10
> Debugging 353
FOR j = i TO 10
IF Names(€i) Names(j) THEN
Temp$S = Names(i)
~ Names(j) = Names(j)
Names(j) = Temp$
END IF
NEXT j
NEXT i
FOR k = 1 TO 10
PRINT Names(k)
NEXT k
It is apparent that this line should be Names(i) = Names(j). We make the change,
yielding the debugged program (shown in Listing 8-1).
Names(1) = “John"
Names(2) = “Tim”
Names(3) = "Edward”
Names(4) = “Samuel”
Names(5) = “Frank”
Names(6) = “Todd"
Names(7) = “George”
Names(8> = "Ralph"
Names(9) = “Leonard”
Names(10) = “Thomas”
FOR i = 1 TO 10
FOR j = i TO 10
IF Names(i) > Names(j) THEN
Temp$S = Names(i)
Names(i) = Names(j)
Names(j) bee Temps
END IF
NEXT j
NEXT i
FOR k = 1 TO 10
PRINT Names(k)
NEXT k
Edward
Frank
George
John
354 P& Advanced BASIC
Leonard
Ralph
Samuel
Thomas
Tim
Todd
The program has been debugged. Now let’s take a look at the same process under
CodeView.
CodeView
CodeView can debug all Microsoft languages, not just BASIC. This can come
in handy if you interface BASIC to assembly language, as we’ll do later.
Once again, let’s start with the same program (with the two bugs in it):
Names(1) = "John"
Names(2) = “Tim"
Names(3) = "Edward"
Names(4) = “Samuel”
Names(5) = “Frank”
Names(6) = "Todd"
Names(7) = "George"
Names(8) = “Ralph”
Names(9) = “Leonard”
Names(10) = "Thomas"
FOR i = i TO 10
FOR j = 1 To 10
IF Names(i) > Names(j) THEN
Temp$S = Names(i)
Names(j) = Names(j)
Names(j) = Temp$
END IF
NEXT j
NEXT i
FOR k = 1 TO 10
PRINT Names(k)
NEXT k
> Debugging 355
We cannot use the QB (or QBX) environment with CodeView, however. Instead, we
must compile our program, ALPHA.BAS, like this with BC.EXE:
0 Warning Error(s)
O Severe Error(s)
The /Zi switch is a necessary step in preparing for CodeView; it instructs the compiler
to retain the line number and symbol information that CodeView will need. Next we link,
setting up for CodeView with the /Co switch to the linker like this:
And that’s it, ALPHA.EXE has been produced and we're ready to run our program.
When we do, however, we get this result, as before:
John
Tim
Tim
Tim
Tim
Todd
Todd
Todd
Todd
Todd
356 & Advanced BASIC
CodeView can also be started in sequential mode (without windows) if you use
the IT switch. In this mode, you can redirect its output to a file — which gives
you a complete record of your debugging session for later reference.
Figure 8-11
CodeView is the premier debugger for the PC; the abilities of this package are prac-
tically unlimited, but we'll only get a chance to scratch the surface in this chapter. We can
start as we did with QuickBASIC, by watching both Names(i) and Names(j). We do that
by pulling down the Watch menu and selecting Add Watch... (see Figure 8-12).
File Edit View Search Run Watch Options Calls
|
Add Watch... Ctril+W
Delete Watch... Ctrl+U
source1 CS:IP AL} Set Breakpoint... FQ
Nemesis = “Ralph” Edit Breakpoints...
Names(9) = “Leonard” Quick Watch — Shift+F9
Names(10) = “Thomas”
FOR i =i TO 10
lize FOR j =i TO 10
18: IF Names(i) > Names(j) THEN
19: Temp$ = Names(i)
> Debugging 357
Figure 8-12
The window shown in Figure 8-13 opens up, much like the window in QuickBASIC.
We can type Names(i), and then repeat the process for Names(j):
File Edit View Search Run Watch Options Calls Help
068!
ource1 CS:IP ALPHABAS (ACTIVE)
12: Names(8) = “Ralph”
13: Names(9) Add Watc!
14: Names(10
Expression: [Names(i) <—
16: FOR i =
17:
18: <Ok><CANCEL> <Help>
19:
20: Naresh = Names(j)
ah Names(j) = Temp$
22: END IF
23: NEXTj
ee ee ret er as a ec ee
Figure 8-13
The top window splits in two, and a watch window appears. Figure 8-14 shows what
the Watch window looks like:
File Edit View Search Run Watch Options Calls Help
teh—A$
}___ pears Namesti=
.
=;
Names(j)="” <
ource1 CS:IP ALPHABAS (ACTIVE)
= Names(8) = “Ralph”
13: Names(9) = “Leonard”
14: Names(10) = “Thomas”
15:
16: FOR i=i TO 10
AZ FOR j =i TO 10
18: IF Names(i) > Names(j) THEN
19: Temp$ = Names(i)
358 P& Advanced BASIC
We can put a breakpoint in, just as we did with QuickBASIC, at the line IF Names(i) >
Names(j) THEN with F9 (or by using Set Breakpoint in the Watch window). CodeView is
a command-driven debugger; to execute the program up to the breakpoint, we can just
type g for go (see Figure 8-15.)
File Edit View Search Run Watch Options Calls Help
local wateh—£a
|Names(i)=“ ”
Names(j)=“ ”
ource1 CS:IP ALPHABAS (ACTIVE)——____
12: Names(8) = “Ralph”
13: Names(9) = “Leonard”
14: Names(10) = “Thomas”
lo:
16: FOR i =i TO 10
as FOR j =iTO 10
18: —____-___—_—|F Namesa(i) > Names(j) THEN ——__________
19: Temp$ = Names(i)
20: Names(j) = Names(j)
21: Names(j) = Temp$
22: END IF
23: NEXTj
aL COMM AN eee
———
>ge
16: FORi =i TO 10
17: FOR j =i TO 10
18: —__————————_-FF Names(i) > Names(j) THEN
19: Temp$ = Names(i)
20: Names) = Names(j)
2t: Names(j) = Temp$
22: END
23: N j
Figure 8-16
We can check the value of the indices i and j with the ? command. In this case, the ?i
command tells CodeView to display the current value of i (see Figure 8-17).
File Edit View Search Run Watch Options Calls Help
eee cs Sewer |aineae => wate)
Namesttz
Names(j)=“ ”
ource1 CS:IP ALPHABAS (ACTIVE):
13: Names(9) = “Leonard”
14: Names(10) = “Thomas”
15:
16: FOR i=i TO 10
lr FOR j =i TO 10
18: ——_—_—_——————_IF Names(i) > Names(j) THEN
19: Temp$ = Names(i)
20: Names(j) = Names(j)
21: Names(j) = Temp$
22: END IF
23 NEXT j
a nO al
< F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>
Figure 8-17
And we see that it is 0. Again, we see that this line in the program is causing problems:
360 P& Advanced BASIC
Names(1) = “John”
Names(2) = "Tim"
Names(3) = “Edward"
Names(4) = "Samuel”
Names(5) = “Frank"
Names(6) = “Todd"
Names(7) = “George”
Names(8) = “Ralph”
Names(9) = "Leonard"
Names(10) = “Thomas”
~ FOR i = i TO 10
FOR j = i TO 10
IF Names(i) > Names(€j) THEN
TempS = Names(i)
Names(j) = Names (j>
Names(j) = Temp$
END IF
NEXT j
NEXT i :
FOR k = 1 TO 10
PRINT Names(k)
NEXT k —
CodeView can even evaluate BASIC expressions for you. For example, 7ASC
(“A”) + 5 will give you a result of 70. This is a good way of checking your code if
you’re unsure about the effect of some statements — and you can do it without
leaving CodeView.
John
Tim
Tim
Tim
Tim
Todd
Todd
Todd
Todd
Todd
> Debugging 361
We can turn back to CodeView. This time, we put a breakpoint farther down in the
code at the end of the section that switches the elements in the array (see Figure 8-18.)
File Edit View Search Run Watch Options — Calls Help
local watch
ae |Nenee=t*
Namestho" :
<8 ource1 CS:IP ALPHABAS (ACTIVE)
Ss
12: Names ye “Ralph”
13: Names(9) = “Leonard”
bes Names(10)= “Thomas”
Figure 8-18
Once again, we type g for go, and the program executes until it reaches the breakpoint.
As before, the section of code that switches array elements works in three steps. First, it
movesNames(i) into Temp$, then moves Names(j) into Names(i), and ends up by moving
Temp$ into Names(j). At this breakpoint, all but the last step has been completed, which
means that Names(i) and Names(j) should hold the same values. In fact, Names(i) =
“John” and Names(j) = “Edward” (see Figure 8-19.)
File Edit View Search Run Watch Options Calls Help
—_———__—_—local ___—_—_
wateh—— A —_—
|Names(i)=“John” <<
Names(j)=“Edward” <—
ource1 CS:IP ALPHABAS (ACTIVE)
12: Norestove “Ralph”
13: Names(9) = “Leonard”
14: Names(10) = “Thomas”
15:
16: FOR i =i TO 10
17: FOR j =i TO 10
18: IF Names(i) > Names(j) THEN
19: Temp$ = Names(i)
20: naieeye Names(j)
——_
24: __——r ames Temp$—@_ _—_—_—_ cm “|
22: END IF
362 P Advanced BASIC
23: NEXT j
Figure 8-19
That indicates that there is a problem with the way the array elements are loaded
during the switching process. We check the code and find that the line immediately
before the breakpoint is in error:
Names(1) = “John"
Names(2) = “Tim"
Names(3) = "Edward"
Names(4) = “Samuet"
Names(5) = "Frank"
Names(6) = “Todd"
Names(7) = “George”
Names(8) = “Ralph”
Names(9) = “Leonard”
Names (10) = “Thomas”
FOR i = 1 TO 10
; FOR j = 4 TO 10
IF Names (i) > vanee (iy THEN
— TempS = Names(i) |
> Names(j) = Names(j)
Names(j) = Temps
END IF
NEXT j
NEXT ji
FOR k = 1 TO 10
ee PRINT Names (k)
NEXT k
We switch this line to Names(i) =Names(j), yielding the debugged program (shown in
Listing 8-2).
i > Debugging
TING: 363
SES
FOR i = 1 TO 10
FOR j = i TO 10
IF Names(i) > Names (j) THEN
Temp$S = Names(i)
Names(i) = Names(j)
Names(j) = Temps
END IF
NEXT j
NEXT i
FOR k = 1 TO 10
PRINT Names(k)
NEXT k
Edward
Frank
George
John
Leonard
Ralph
Samuel
Thomas
Tim
Todd
The program has been debugged. CodeView is a powerful debugger, and it provides
many, many tools for the programmer. It is worth noticing that, besides breakpoints,
CodeView also has tracepoints and watchpoints. A tracepoint is variable breakpoint that
is taken when a specified value changes. The value can be either the value of a variable or
the value of some expression. For example, when the value in a certain variable changes,
the tracepoint is activated. |
A watchpoint is a variable breakpoint that is taken when a specified expression
becomes true (that is, nonzero). In other words, you can give CodeView a BASIC
364 & Advanced BASIC
expression to test as it steps through a program; when that expression becomes true, the
watchpoint is activated. All in all, we’ve just scratched the surface here; if you run into
serious bugs, you'll often find assistance in CodeView.
CodeView will not help you debug memory-resident files, however; nor can it
retain symbolic information in programs that are made into .COM files.
That ends our discussion of debugging. For more information, see the QuickBASIC or
the CodeView manuals. Now, however, it is time to start moving on to assembly language
to add real speed and power to our BASIC code. We'll do that be beginning to see what
the operating system has to offer us.
A Tour of BIOS and DOS
365
. =
a
ry
F Ue eth Tr vty toon
=" 4 nic code, Will
de, Saat +. os
eat
r Wer 7
ae
aX
a)
1 us ‘ foe Ver
&
oe ir » — _ =
=4s 2 a ’ Ee
—_ 6. _ ha
_
< _
7
- 7
: 7
e -_
we
a
q
.
i ¢
_ _ _
=rs a a
. ~ 7
7 7
_
>
ee
:
-
: We snes
+ S
aS or hi
7 ris
"a
re
o. 723
‘
a te
mee
asf
ae
a
7=ee
:q :
Pe
7_7"nen
> A Tour of BIOS and DOS_ 367
ONE OF THE most powerful ways of augmenting BASIC is to use the resources
available to us in BIOS (the Basic Input/Output System that is already in the computer's
ROM) and DOS, and BASIC allows us to reach the computer’s low-level resources
directly with its INTERRUPT( ) and INTERRUPTX( ) routines.
You can often do some things that BASIC does in a fraction of the time by
interfacing to DOS and BIOS. The most common example is fast file
manipulation. *
In this chapter, we’re going to make a tour of BIOS and DOS, and we'll examine all the
services they provide (as listed in the appendix at the end of this book). These services are
built into the operating system, which means they’re available to us as BASIC program-
mers. We're not going to limit our exploration to just looking. We’re going to put together
some subprograms and functions that will let us make use of this added power.
For example, we'll write routines that can search for files matching a given file specifi-
cation (including wildcards), one that can find the amount of free space left on a target
disk, another one that can tell us what equipment is installed in the computer, and
others. To develop these and more, let’s get started with the BIOS interrupts immediately.
In particular, you might notice interrupt 5, which is the interrupt called when you
press the Print Screen key. If you execute an interrupt 5 with INTERRUPTC( ), you'll print
the screen out.
The first big BIOS interrupt is interrupt @H10, which handles the screen at the lowest
level. Here are those services:
To select a service from an interrupt, load the service’s number into the ah register.
Some of these services are pretty complex, and it’s not appropriate to spend too much
time on them in our tour of the system’s resources. However, it’s often useful to know
what the video mode is, so let’s put together a small function to find the current BIOS
screen mode.
If you know the current BIOS mode, you know the number of rows and columns (or
pixels) available on the screen, and the number of colors you can display at once. Here is
a list of BIOS modes and what they mean as far as video capability is concerned:
To get the BIOS video mode, we can look in the appendix to see that interrupt @H10
service QHF is what we want. Let’s write a function named VideoMode%( ) to return the
video mode. We start off by loading ah with QHF and calling INTERRUPTC( ):
Checking the appendix, we see that the video mode is returned in al, the bottom eight
bits of the ax register. To return that as the value of the function VideoMode%, we AND
ax with @HFF:
And that’s all there is to it. Listing 9-1 shows the whole function.
> A Tour of BIOS and DOS 371
FUNCTION VideoMode%
END FUNCTION
Mode% = VideoMode% e
When you run it, it sets the variable Mode% to the value returned by the function
VideoMode%( ) and prints it out.
The next BIOS interrupt is interrupt @H11, equipment determination: This interrupt
often
tells us what kind of hardware is installed in the computer. Since this information is
useful to the BASIC programmer, let’s take a closer look.
ax bits Indicates
15,14 Number of printers installed
1) Internal modem installed? (1 = yes)
12 Not used
11,10,9 Number of RS-232 cards attached
8 Not used
0 Number of diskette drives: 00 = 1 drive; 01 = 2 drives
5,4 Video type: 01 = 40x25 color; 10 = 80x25 color; 11 = 80x25
mono
3 Not used
2 Mouse installed? (1 = yes)
] Math coprocessor installed? (1 = yes)
0 Diskette drives installed? (1 = yes)
After we return this 16-bit word to the calling program, we can decode its bits as
shown above (or you might want to adapt this function to do that for you). Listing 9-2
shows the whole function.
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
> A Tour of BIOS and DOS: 373
FUNCTION GetEquipment%
END FUNCTION
This example program, for example, checks whether or not a math coprocessor is
installed:
Check% = GetEquipment%
BASIC
As you can see, the BIOS and DOS interrupts have some real utility to offer
put them to
programmers, and, through INTERRUPT( ) and INTERRUPTX( ), we can
work.
374 Pb Advanced BASIC
Testing bit 2 of the return value from this service tells you whether or not a
mouse is installed (1=yes). This is a good test for your program to make before
using the mouse program we developed in Chapter 3.
Now let’s continue our tour of BIOS and DOS. Interrupt @H12 was originally
designed to determine the size of installed memory, but it only returns the amount of
memory installed on the motherboard, not on add-in cards, which means that it’s no
longer reliable.
The next interrupt, @H13, is huge. This is the interrupt that handles the diskette and
disk drives at the lowest level. Because of the sensitive nature of disk data, we’re not going
to make use of interrupt @H13 in this book, but it does pack a lot of power:
The next couple of interrupts have largely to do with VO. For example, interrupt
S&H 14 is the one that handles the serial port:
Interrupt GH15 handles the (now completely obsolete) cassette port that was installed
on the original PC: Interrupt @H15 Cassette I/O.
Interrupt @H16 handles the keyboard data; this interrupt lets you read the keys that
have been struck (while interrupt 9 is the low level interrupt that reads the bits directly
from the keyboard and decodes them into characters):
Interrupt G@H18 is the resident ROM BASIC interrupt. Early PCs, including the origi-
nal PC, had a small version of BASIC in ROM so that if you turned on the machine
without a boot diskette, something still happened. In this case, this internal version of
BASIC started. If you execute interrupt @H18 in these machines, ROM BASIC will start:
Interrupt G@H18 Resident BASIC.
Interrupt @H19 is the boot interrupt. When you turn your machine on, this is the
interrupt that is executed. And, by calling this interrupt yourself, you can reboot your
machine as well (a rather dramatic way of ending a program): Interrupt @H19 Bootstrap.
Interrupt @H1A handles the time of day (this is the interrupt the DOS commands
TIME and DATE use):
The remainder of the BIOS interrupts, with the exception of interrupt @HIC, are not
software routines at all. Instead, they hold various tables and addresses. For example,
interrupt &H1B holds the address that control is transferred to when you press “Break,
and interrupt &H1D holds the current video parameters. Interrupt @H1E holds the
address of the diskette parameters, and you can change these values if you know what
you're doing (including the seek and reset times to speed things up). Interrupt @H1F can
hold the address of graphics characters used on the screen:
376 > Advanced BASIC
Interrupt SHIC is the “timer tick” interrupt, and it’s called whenever the internal
timer increments its count (18.2 times a second). Many TSRs use this interrupt to take
control of the machine temporarily.
TSRs take control by changing the internal address of this interrupt to point to
their own code in memory. That way, every time there is a timer tick, their code
is run.
These are the last of the BIOS interrupts. Let’s continue our tour by exploring DOS.
We've already seen one or two of these services in action. The next two services let you
reset a disk or change the default disk:
In the BASIC PDS, you can use the CHDRIVE statement to set the default drive.
CHDRIVE, however, is not available in QuickBASIC, so we can write a new function —
SetDefaultDisk%( ) — for QuickBASIC users. Interrupt &H21 service QHE will let us
change the default drive as we require.
Drives = “A"
+ Check% = SetDefaultDisk% (Drives)
IF Check% = 0 THEN PRINT “Error.”
378 }& Advanc ed BASIC
eee ee
We can use interrupt &{H21 service @HE here. First, we can check the passed param-
eter Drive$ to make sure it is only one character long:
SetDefaultDisk% = 0
Service @HE expects the new default drive number — 0 for A, 1 for B, etc. — in the dl
register, so we can proceed like this:
SetDefaultDisk% = 0
Afterwards, we can check the new drive number (returned in al) against the drive
number we requested (still in dl) to make sure the change was actually made:
SetDefaultDisk% = 0
SetDefaultbiskx = 4
If the present drive is not the same as the drive we requested, we exit, leaving Set-
DefaultDisk% equal to 0 and indicating failure. If it is the same, we set SetDefaultDrive%
to 1 and exit. Listing 9-3 shows the entire function.
ae > A Tour of BIOS and DOS 379
rrenre
SetDefaultDisk% = 0
InRegs.ax = &HEOO
InRegs.dx = ASCCUCASES(Drive$)) - ASCC"A")
CALL INTERRUPTC(&N21, InRegs, OutRegs)
IF (InRegs.dx AND &HFF) <> CInRegs.ax AND &HFF) THEN EXIT FUNCTION
SetDefaultDisk% = 1
END FUNCTION
Let's press on with our tour of DOS. The following group of DOS services deals mostly
with files. These services, however, are mostly obsolete, and should be avoided:
These services work with file control blocks, which cannot handle pathnames
(they were used primarily in the days before hard disks). The modern file
services start with &H3C.
Some of these services are quite useful. In fact, while we’re here, we can make use of
one of these services, service &H19, which returns the default disk.
This function will make use of interrupt @H21 service @H19 to determine what the
default disk is.
Interrupt &H21 service &H19 is useful if you’ve just been passed control in a
subprogram and want to find out if you’re dealing with a hard drive or a diskette
drive.
To request service &H19, we simply load ah with @H19 and call interrupt @H21. The
default disk number is returned in al. Here, 0 corresponds to drive A, 1 to B, and so on.
We convert the number in al to a drive letter like this:
DIM InRegs
AS RegType, OutRegs AS RegType
InRegs.ax = &H1900
CALL INTERRUPT(&H21, InRegs, OutRegs)
+ GetDefaultDisk$S = CHRS((OutRegs.ax AND &HFF) + ASC(“A"))
And that’s all there is to it. Listing 9-4 shows the complete function.
END FUNCTION
This is about as short and to the point as you could want. It simply prints out the
default disk’s drive letter, as returned by GetDefaultDisk$.
The next DOS services deal mostly with file I/O. These are part of the outmoded DOS
file services that we should avoid. These are the file control block file I/O services.
Then, in typical interrupt @H21 fashion, there’s a collection of services without any-
thing in common:
382 b& Advanced BASIC
One of these — interrupt &H21 service @H36, get free disk space — is particularly
useful to the BASIC programmer. Let’s develop a function around it.
Here Drive$ is the letter corresponding to the drive you want to check (e.g., C). Since
it’s possible that the specified drive might not exist, we should make DiskFree& return a
value of -1 if there’s been an error.
To start, we have to load &H36 into ah:
DiskFree& = -1
IF LENCDrive$) <> 1 THEN EXIT FUNCTION
— InRegs.ax = &H3600
8oo
We also have to place the drive number in dl, the low eight bits of the dx register. For
this service (and unlike service @H19), the drive number is 1 for drive A, 2 for drive B,
and so on. In other words, just ASC(UCASE$(Drive$)) - ASC(“A”) + 1. Then we call
INTERRUPTC( ):
DiskFree& = -1
IF LENCDrive$) <> 1 THEN EXIT FUNCTION
InRegs.ax = &H3600
~» InRegs.dx = ASCCUCASES(Drive$)) - ASCC"A") + 1
CALL INTERRUPT(&H21, InRegs, Outregs)
88
ae
If this service places a -1] in ax, then the target drive doesn’t exist or isn't responding. In
that case, we haveto return with DiskFree& set to -1:
DiskFree& = -1
IF LEN(Drive$S) <> 1 THEN EXIT FUNCTION
InRegs.ax = &H3600
= ASCCUCASES(Drive$)) - ASCC“A") + 1
InRegs.dx
CALL INTERRUPT(&H21, InRegs, Outregs)
«
*
384 » Advanced BASIC
DiskFree& = -1
IF LENCDrive$S) <> 1 THEN EXIT FUNCTION
InRegs.ax = &H3600
InRegs.dx = ASCCUCASES(Drive$)) ~ ASCC"A") + 1
CALL INTERRUPT(&H21, InRegs, Outregs?
—+ Temp& = OutRegs.ax
DiskFree& = Temp& * Outregs.bx * Outregs.cx
That’s it; we now return the resulting value as the value of DiskFree&. Listing 9-5
shows the whole function.
DiskFree& = -1
IF LENCDrive$S) <> 1 THEN EXIT FUNCTION
InRegs.ax = &H3600
InRegs.dx = ASCCUCASES(Drive$)) =— ASCC"A") + 4
CALL INTERRUPTC(&H21, InRegs, Outregs)
IF OutRegs.ax = -1 THEN EXIT FUNCTION
Temp& = OutRegs.ax
DiskFree& = Temp& * Outregs.bx * Outregs.cx
END FUNCTION
MyDrivesS = “C"
This program simply checks drive C and reports how many bytes are free. Don’t try
this unless you have a C drive, without an actual drive to check, the results will be
meaningless.
Continuing our DOS tour now, we come to the modern file handling services.
Starting with Version 2.0, DOS started to support pathnames, which made the old file
services obsolete. The new file services, which use file handles, start at this point. File
handles work much like the window handles we developed in Chapter 2. A file handle is
just a 16-bit word that stands for a specific open file. Here are the new file services:
The next few DOS services following these have to do with allocating or deallocating
memory, and are mostly important in multiuser environments (DOS usually gives your
program all available memory unless that memory is shared):
The following two services, however, can be very useful for BASIC programmers:
Service G@H4E searches a directory for the first filename that matches a file specifica-
tion that you’ve given. Service @H4F continues the work that service @H4E began. Using
it repeatedly finds all the remaining matches (service &CH4E performs initialization work
that only needs to be done once). Let’s add this power to BASIC.
matches your file specification, and we can use it in a function called, say, Get-
FirstMatchingFile$( ). For example, to find a match to PROGRAM. * in the subdirectory
CAARCHIVES, just use it this way:
We can design this function to return a string made up of the first matching filename
(the pathname will be omitted because service @H4E doesn’t return it).
With this function, you can find files placed anywhere on disk, check to see if a
file exists before overwriting it, or examine the contents of whole
subdirectories.
Because service QH4E only returns the name of the first matching file, we'll need to
use another function, which we can call GetNextMatchingFile$( ), to continue the
search. To search a directory for multiple files, use GetFirstMatchingFileS$( ) first. If it
returns a matching filename (and not a null string, “ ”, which indicates that there was no
match), use GetNextMatchingFile$( ) next, calling it repeatedly until you run out of
matches (i.e., it returns “ ”).
Let’s write the function GetFirstMatchingFile$( ). Interrupt QH21 service QH4E
returns information about matching files in the Disk Transfer Area, or DTA. The DTA is a
buffer put aside for low-level DOS file manipulations (and it is mostly obsolete, except for
this service). For that reason, we need to find the address of current Disk Transfer Area,
and one of the services we've already covered — interrupt @H21 service GH2F — will
give us that.
As we saw in Chapter 5, addresses in the PC are made up of two 16-bit words, a
segment address and an offset address. Service &H2F returns both the segment and offset
address of the DTA, and we place them in DTASegment% and DTAOffset%, respectively:
388 Pb Advanced BASIC
InRegs.ax = &H2F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es ¢
DTAOffset% = OutRegs.bx
Now that we have the DTA address, we're ready to find the first match. Checking the
BIOS and DOS appendix, we see that we must pass an ASCIIZ string holding the file
specification to interrupt &H21. An ASCIIZ string is just a string of characters with a
termination character of ASCII 0 — that is, CHR$(0) — at the end.
If the file specification passed to us is called FileSpec$ (e.g., this string will hold *.DAT
or *.*), we can make an ASCIIZ string out of it this way: NameFile$ = FileSpec$ +
CHR$(0). Now we have to pass the address of NameFile$ to interrupt @H21. We can use
VARSEG( ) to get the segment address of a string, and we use the BASIC function
SADD( ) (not VARPTR( )) to get its offset address.
The segment address goes into a new register called ds, which stands for data segment,
and the offset address goes into dx. The ds register is one of the four segment registers in
your PC (see Figure 9-1). These registers are designed to keep track of segment addresses.
There are four segment registers: cs, ds, es, and ss. They are all used for holding segment
addresses. The ds register holds the segment address of the current data segment; es
holds the extra segment; ss holds the stack segment; and cs holds the code segment. We'll
see them at work in Chapter 11.
4 cs ds es ss
ax bx Cx dx
CPU
bp si di flags
Figure 9-1
> A Tour of BIOS and DOS 389
InRegs.ax = &H2FO00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
Now we have to decide what type of files we want to find; that is, what the file attribute
to be used in the search should be. DOS uses file attribute values like this (and you can
use this list for reference if you want to try using any other DOS file services):
Attribute Means
0 Plain file
Read only file
Hidden file
System file
Volume label of a disk
Ovo
He
AN Subdirectory name
You can search for hidden files on your disk by using this function and setting
the file attribute to 2. You can even search for the system files IBMDOS.COM
or MSDOS.COM to see if the current disk is a boot disk.
In other words, if we passed a file attribute of 1, we would search exclusively for read-
only files. Let’s use an attribute of 0 and search for only plain files. We have to pass the file
attribute in cx, so we place 0 in it and call interrupt @H21:
390 » Advanced BASIC
InRegs.ax = &H2FO00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
When we return from interrupt @H21 service @H4E, there will be one of two possible
outcomes: we will either find a matching file, or we will not. If no matches are found, this
service sets the carry flag on return.
The carry flag is one of eight flags internal to the 80x86, and we've seen those flags as
far back as Chapter 1:
As we can see, the carry flag is in bit 0 of OutRegs.flags; if that bit is 1, the carry flag
was set. We can read the value of the carry flag after the call to INTERRUPTX( ) by
examining the value of OutRegs.flags AND 1. If there was no match, thecarry flag will be
set (OutRegs.flags AND 1 will equal 1), so we return a null string, “ ”, like this:
> A Tour of BIOS and DOS 391
TnRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
On the other hand, if there is a match, the DTA will be filled as shown in Figure 9-2
(according to the Appendix).
DTA begins:
21 bytes: Reserved.
1 byte: Found file’s attribute.
2 bytes: Found file’s time.
2 bytes: Found file’s date.
2 bytes: Low word of file’s size.
2 bytes: High word of file’s size.
DTA ends: 13 bytes: Name of found file in ASCIIZ form
Figure 9-2
Let’s suppose that there is a match. In that case the matching filename will be 29 bytes
into the DTA, stored as an ASCIIZ string. We can place its offset address into a variable
named MatchOffset% like this:
InRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
392 P Advanced BASIC
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
All that remains is to convert that ASCIIZ string into a BASIC one, and return:
InRegs.ax = &H2F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OQutRegs.bx
Match$ = ""
FOR i = 1 TO 13
NewChar$ = CHRSCPEEK(MatchOffset%
IF NewChar$S = CHRS(O)
+ 7)) eet
ae
THEN EXIT FOR
> A Tour of BIOS and DOS’) 393
DEF SEG
GetFirstMatchingFileS = Match$
And that’s all there is to it. If we’ve found a match, we return the filename in a BASIC
string. Otherwise, we return a null string. In addition, the Disk Transfer Area is now set
up with the information needed by GetNextMatchingFile$( ) to continue the search.
Listing 9-6 shows the whole function GetFirstMatchingFile$( ).
TYPE RegTypex
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
ds AS INTEGER
es AS INTEGER
END TYPE
InRegs.ax = &H2F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
Match$ = ""
FOR i = 1 TO 13
NewChar$S = CHRSCPEEK(MatchOffset% + 1))
IF NewCharS = CHRS(O) THEN EXIT FOR
Match$ = Match$ + NewChar$
NEXT i
DEF SEG
GetFirstMatchingFile$ = Matchs
END FUNCTION
FileSpecS = "*.pAT"
MatchFile$ = GetFirstMatchingFileS(FileSpec$)
IF MatchFileS <> "" THEN
PRINT “First matching file is: ", MatchFile$
ELSE
PRINT “No matches.”
END IF
This example returns the first filename in the current directory with the extension
-DAT. Keep in mind that this function only returns the name of the first matching file.
To
continue the search, use GetNextMatchingFile$( ). In addition, it only returns
files that
have attribute 0 (i.e., all files that are not hidden, system, or subdirectory
files).
Let's continue with GetNextMatchingFile$( ).
Just keep calling GetNextMatchingFile$( ) repeatedly until it returns a null string, “”, at
which point we've run out of matches.
This function is much like GetFirstMatchingFile$( ), except that it uses interrupt
&H21 service @H4F to search for the next matching file. We start off by finding the DTA
address as before, and by calling service @H4F (no input needed — everything required
is already in the DTA):
FUNCTION GetNextMatchingFiles
InRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
InRegs.ax = &H4F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
If the carry flag is set on return from INTERRUPTX( ), there was no match and we
have to indicate that by returning a null string, “”. Otherwise, we read the filename as
before from the DTA:
FUNCTION GetNextMatchingFiles
InRegs.ax = &H2FO00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
InRegs.ax = &H4FOO
CALL INTERRUPTX(&H21, InRegs, OutRegs)
Match$ = “"
FOR i = 1 TO 13
NewChar$S = CHRSCPEEK(MatchOffset% + i))
IF NewChar$ = CHRS(O) THEN EXIT FOR
Match$ = Match$ + NewChar$s
NEXT i
DEF SEG
GetNextMatchingFileS = Match$
That’s all there is to it. If there is a match, we return the filename in a BASIC string.
Otherwise, we set GetNextMatchingFile$ to a null string and exit. Listing 9-7 shows the
whole function.
TYPE RegTtypex
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
flags AS INTEGER
ds AS INTEGER
es AS INTEGER
END TYPE
FUNCTION GetNextMatchingFiles
InRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx
InRegs.ax = &H4F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
Match$ = ""
FOR i = 1 TO 13
NewCharS = CHRS(PEEK(MatchOffset% + 1))
IF NewChar$S = CHRS$(Q) THEN EXIT FOR
MatchS = Match$ + NewChar$
NEXT ji
DEF SEG
GetNextMatchingFile$S = Match$S
END FUNCTION
FileSpec$ = “*.DAT"
MatchFile$ = GetFirstMatchingFileS(FileSpec$)
You can see that we use GetFirstMatchingFile$( ) first, check the result, and then keep
on going with GetNextMatchingFile$(). If we got a null string from Get-
FirstMatchingFile$( ), there are no matches to our file specification, and we quit. If there
was a match, however, we print it out and then keep going, looping over GetNext-
MatchingFile$( ) until it finally returns “ ”, at which point we can quit:
398 » Advanced BASIC t
t
ie ee
We've come a long way through BIOS and DOS, and we’ve put some of the services
we've seen to work. Now we’re just about ready to wind up our DOS tour.
|NOTE: | If the service number is 16 bits long (e.g., &H5FO3), you have to load that
entire number into the ax register.
That’s it for the interrupt @H21 services, and with them, we wind down interrupt
&H21. As you can see, this interrupt can be very useful to BASIC programmers.
The remaining DOS interrupts are not usually very useful, however, with the excep-
tion of interrupts @H25 and &H26, which read and write sectors on the disk (see the
appendix for more information), and interrupt @H27, which lets you create TSR pro-
grams (although this has largely been superceded by interrupt @H21 service GH3C).
Here are the remaining DOS interrupts:
The rest of the interrupts, which run to @HFE also don’t offer much utility, except for
interrupt &H67, which provides support for LIM 4.0 and expanded memory:
Conclusion
We've completed our tour of BIOS and DOS, from interrupt 0 up through interrupt
SHFF and we've even put some of the available services to work. Now, however, it’s time
to really get to work on low-level programming. We start our assembly language excur-
sion in Chapter 10.
Welcome to Assembly Language
401
ee eh ay aon
: ys;
yr et 18 ed. Ds, Braet oo ae ce
a ; os raf cl alee eervicta toy are, New, i
Bk
4 de a8 Lee level pangrateerang, Wp sun or ese . a ar
> Welcome to Assembly Language 403
IN THIS CHAPTER, we're going to start working with the machine on the lowest
levels. Here’s where we'll boost BASIC’s speed and capabilities enormously. This chapter
is going to be about the essentials of assembly language, and in the next chapter we'll
interface it to BASIC. We'll see how to write our own BASIC subprograms and functions
entirely in assembly language for unprecedented power. The most important thing is to
get a good base to start from, and we'll do that in this chapter when we learn the
fundamentals of assembly language programming. In fact, through our use of the INTER-
RUPT( ) routine, we already have a big leg up.
Machine Language
To get a computer to do something, you have to supply machine language instructions,
and these bytes are really only comprehensible to the microprocessor. Often, only part of
the machine language instruction will be used to tell the computer what to do, and the rest
of the instruction is data. For example, you can write an instruction to put the byte @HFF
into a certain memory location. Part of the instruction will be to tell the microprocessor that
you want to store a number, part of the instruction will be to tell it the location in memory
you want to store the number, and part of it will be the number itself, @HFF
Although machine language instructions can be many bytes long, data and the instruc-
tion codes never mix across byte boundaries. For example, some machine language
instructions may be all instruction to the microprocessor (see Figure 10-1).
| __ 01010101 |
Instruction
Figure 10-1
And some will be a mix of both instruction and data (Figure 10-2).
10101010 | 10111010 |
Instruction Data Used by the Instruction
Figure 10-2
or even mostly data (Figure 10-3).
[01010101 | 10111010 10010101 001010100 |
Instruction Data Used by the Instruction
Figure 10-3
The data used by the instruction is either memory address(es) or data, like the @HFF
we wanted to store in a memory location earlier.
404 » Advanced BASIC
Reading this kind of binary code is extremely difficult. Imagine yourself confronted
with a page of numbers, all 0’s or 1’s. Even if such instructions were to be converted to
hex, you’d have to look up the meaning of each byte before understanding what was
going on (there are tables in the manuals that accompany assemblers listing what binary
instructions mean what). Mostly, however, what means everything to the microprocessor
means nothing to us.
Assembly Language
This is where assembly language enters. It is the direct intermediary between machine
language and English. For every machine language instruction, there is one assembly
language instruction. Rather than using a byte like 10101010B (we'll place a B for Binary
at the end of binary numbers; there is no binary type in BASIC, so we couldn’t denote this
number as something like @B10101010), an English language mneumonic is used such
as MOV AX,5.
This instruction, MOV AX,5, may be terse, but it is still an improvement over the
corresponding machine code, @HB8 &HOO G&HO05. What this instruction does is to
direct the machine to move (the MOV part of MOV AX,5) the value of 5 into the AX
register.
What an assembler does is simple. It takes the program you’ve written in assembly
language, and converts it, instruction by instruction, directly into machine language.
The machine language is then run by the microprocessor. Let’s see some assembly
language examples. These examples will be assembly language instructions that were
assembled (i.e., converted into machine language) so that the corresponding machine
language for each instruction (all numbers are in hex) becomes the following:
Sometimes assembly language is hard to write, sometimes it’s hard to debug; there is
no escaping the fact. It has to follow the design of the microprocessor, and as we'll see,
assembly language for the PC and PS/2 has many quirks.
The most fundamental assembly language instruction is MOV, the instruction that
moves data between registers and memory, or register and registers. The MOV format is
this: MOV destination, source. The data gets moved from the source into the destination.
If you have something stored in memory and want to work with it, you can use MOV.
Here’s how it works:
Here we are putting the number OFFFFH (65535) into AX. OFFFFH is the biggest
number any register (all are 16 bits) can hold. Note that in assembly language, the H for
Hex goes at the end of the number (OFFFFH) not at the beginning as in BASIC
(G@HFFFF). In addition, if any number begins with a letter, like FFFFH, you have to
preface it with a 0 (OFFFFH) to let the assembler know it’s really a number and not a
name.
We can take the OFFFFH in the register AX and move it into the DX register:
And now DX and AX hold the same value. We could also work one byte at a time:
Data can also be moved into registers from memory. Let’s say we have a memory
location with 0 in it. This can be moved into, say, CX, this way:
Or, we can move whatever is in DX into the memory location this way:
However, data cannot be moved from memory directly to memory. This is one of the
peculiarities of the 80 x 86: data cannot go directly from memory location to memory
location in one instruction. If we wanted to, we could move data from memory location 1
to AX, and then to memory location 2 like this:
C:\>DEBUG
-~
The command R in DEBUG stands for Register and it lets you see the contents of all the
80 X 86's registers. You can pick out the registers AX, BX, CX, and DX readily (note that
all numbers displayed in DEBUG are in hexadecimal, which is standard for assembly
language debuggers):
C:\>DEBUG
=R
C:\>DEBUG
“Rk
AX=0000 BX=0000 CX=0000 DX=0000 SP=FFEE pBP=0000 $1=0000 pD1=0000
DS=OEF1 ES=OEF1 SS=OEF1 CS=0EF1 IP=0100 NV UP EI PL NZ NA PO NC
OEF1:0100 9AECO4O020F CALL OFO2:04EC
The settings of the internal flags of the 8086 are shown (there are eight flags, as
mentioned in Chapter 9):
C:\>DEBUG
—-R
AX=0000 BX=0000 Cx=0000 Dx=0000 SP=FFEE sP=0000 sr=0000 D1=0000
DS=OEF1 ES=OEF1 SS=0EF1 CS=O0EF1 1ip=0100 NV UP EI PL NZ NA PO NC
OEF1:0100 9AECO4020F CALL OFO2:04EC
We've already seen the carry and zero flags, and the notations NZ and NC tell us that,
at the moment, they are not set. Flags are used in conditional jumps, and we'll work with
them later in this chapter. DEBUG also tells you the current memory location. Here, we
are at memory location OEF1:0100:
Cs \>DEBUG
=f
AX=0000 Bx=0000 CX=0000 Dx=0000 SP=FFEE sBP=0000 si=0000 »p1=0000
DS=QEF1 ES=0EF1 SS=OEF1 CS=0EF1 IP=0100 NV UP EI PL NZ NA PO NC
OEF1:0100 9AECO4O020F CALL OFO2:04EC.
The final part of the DEBUG R display indicates what is to be found at the current
memory location. In our case, those are the bytes following the address in the R display:
Cs \>DEBUG
DEBUG tries to group bytes together, starting at the current memory location (which
only holds one byte), into what would be a valid machine language instruction. It then
provides us with an assembly language translation of the machine language instruction
that begins at our present location.
408 } Advanced BASIC
C:\>DEBUG
Kh
AX=0000 BX=0000 Cx=0000 Dx=0000 SP=FFEE BP=0000 SsI=0000 pb1=0000
DS=OEF1 ES=OEF1 SS=OEF1 CS=OEF1 IPp=0100 NV UP EI PL NZ NA PO NC
We'll use the A (for Assemble) command to put our own program in, consisting of only
one line: MOV AX,5. The A command needs an address at which to start depositing the
machine language instructions it will generate in memory. Our current address is
OEF1:0100, and we will tell it to assemble the machine language right there, using the
shorthand A100:
C:\>DEBUG
-R z
Following the A100 command, DEBUG returned with the line OEF1:0100, showing
the current address at which it will deposit assembled code. We simply type MOV AX,5
and then a carriage return:
> Welcome to Assembly Language 409
C:\>DEBUG
-R
AX=0000 8X=0000 Cx=0000 DX=0000 SP=FFEE BP=0000 sir=0000 bI=0000
DS=OEF1 ES=QOEF1 SS=O0EF1 CS=O0EF1 1IP=0100 NV UP EI PL NZ NA PO NC
OEF1:0100 9AECO4020F CALL OFO2:04EC
~A100
OEF1:0100 MOV AX,5 « Type "MOV AX,5<cr>"
OEF1:0103
DEBUG then prompts for the next instruction we wish to give with the address
OEF1:0103. There are no more instructions to assemble at this time, so we give DEBUG a
carriage return. It interprets the blank line to mean that we are through assembling, and
returns to its normal prompt.
That’s all there is to it. We’ve just assembled our first line of assembly language. To see
what occurred, remember that the R command displays the current memory location and
instruction. Since we assembled MOV AX,5 at the current memory location, let’s give the
R command and take a look:
C:\>DEBUG
We can see the instruction (note the machine language bytes corresponding to MOV
AX,5 in DEBUG’s display). Executing the instruction is simple. DEBUG has a trace com-
mand, and typing T once will execute the current instruction, and increment us to the
next memory location:
C:\>DEBUG
~R
AX=0000 Bx=0000 CX=0000 »DxX=0000 SP=FFEE BP=0000 S1=0000 pd1=0000
DS=OEF1 ES=OEF1 SS=OEF1 CS=O0EF1 1P=0100 NV UP EI PL NZ NA PO NC
O0£F1:0100 9AECO4020F CALL OFO2:04EC
-A100
OEF1:0100 MOV AX,5
OEF1:0103
410 P& Advanced BASIC
mR .
After the T command, DEBUG displays what the register contents and flags are now:
AX holds 5.
All the flags remain unchanged. On the other hand, the memory location has changed,
from OEF1:0100 to OEF1:0103. This is because the machine language instruction corres-
ponding to MOV AX;,5 is three bytes long in memory (OB8H 05H OOH). The instruction
following it will begin three bytes later; therefore, the 100 has changed to 103.
C:\>DEBUG :
-A100 : -
OEF1:0100
We're going to use interrupt 21H service 2 to print out a letter. Just type in the
following assembly language instructions verbatim, followed by a carriage return after the
prompt OEF1:010A to stop assembling:
C:\>DEBUG
-A100
1€€1:0100 MOV AH,2
1CE1:0102 MOV DL,5A
1€E1:0104 INT 21
1€E1:0106 INT 20 :
1CE1:0108 | « Just type a<cr> here.
-A100 :
> Welcome to Assembly Language 411
What our progam is doing is loading the registers AH and DL with the MOV instruc-
tion. We're asking for service 2 of interrupt 21H, and that service prints out the ASCII
code in DL. We place the ASCII code for Z there, 5AH.
Then we issue an interrupt 21H instruction directly — INT 21H — followed by INT
20H. You may recall from our BIOS and DOS tour that interrupt 20H is used to end
programs at the assembly language level and return to the DOS prompt. This is how we'll
end all our programs.We can call the program PRINTZ.COM, following its function. We
name it this way:
C:\>DEBUG
-A100
1CE1:0100 MOV AH,2
1CE1:0102 MOV DL,5A
1CE1:0104 INT 21
1CE1:0106 INT 20
1€E1:0108
-NPRINTZ.COM —
And now we can write it out (DEBUG will write this file in the current directory).
DEBUG needs the number of bytes to write out, and, in our case, the program goes from
locations 0100H to 0107H. Each memory location holds a byte, so that makes 8 bytes
DEBUG allows you to do hexadecimal math with the H, or Hex command. For
example, H 8 2 returns both the sum and difference of 8 and 2 - A and 6.
The DEBUG W command, Write, reads the number of bytes to write as a file directly
out of the CX register. This means that to write our 8 byte program PRINTZ.COM, we
will have to load the CX register with 8 and then give the W command.
To move 8 into CX, we can use the R (Register) command again. If you use the R
command without any arguments, DEBUG gives you its standard display. On the other
hand, giving the command RCX indicates to DEBUG that you wish to change the value in
CX (this will work with any register). DEBUG displays the current value in CX (which
will be 0000) and gives us a colon prompt, after which we will type our new value for CX,
8, and a carriage return. Then we can write PRINTZ.COM by giving the W command:
412 » Advanced BASIC
C:\>DEBUG
-A100
10€1:0100 MOV AH,2
1CE1:0102 MOV DL,5A
1€0E1:0104 INT 21
1C0E1:0106 INT 20
1CE1:0108
-U100 106
1C0E1:0100 B402 MOV AH,02
10E1:0102 B25A MOV DL,5A
1CE1:0104 Cd21 INT 21
1€CE1:0106 CD20 INT 20
-NPRINTZ.COM
-RCX e
cx 0000
38
-W The W command
Writing 0008 bytes
-Q
Now we've written a functional 8-byte (!) program to disk. Let’s run it:
Ci \>PRINTZ
z
C:\>
And PRINTZ does what it’s supposed to do: It types out Z and exits.
For the first time, we’ve reached interrupt 21H directly (that is, without INTER-
RUPTC( )), using service 2 to print out a character on the screen. Here are some other
interrupt 21H character I/O services we'll find useful in this chapter:
Now that we have some experience, we can start putting together assembly language
programs without using DEBUG. To do that, we'll have to review the way memory is
> Welcome to Assembly Language 413
accessed by the 80X86 chips. Memory usage is much more important in assembly
language than it is in BASIC. In fact, knowing your way around is essential.
Memory Segmentation
The address OEF1:0100 is made up of two hex numbers, each 16 bits long. As we’ve
seen as long ago as Chapter 5, this is usual for addresses — two words are involved for a
full address. The OEF1H in OEF1:0100 is the segment address of that particular memory
location, and 0100H is the offset address (see Figure 10-4).
A Typical Address
:0100
Offset Address
Figure 10-4 Segment Address
Under DOS, you can access up to one megabyte of memory, or 2” bytes. To specify
memory locations that big, you need numbers 20 bits long, but the largest number of bits
a number can hold in the 80 X 86 (with the exception of the 386 and 486) is 16 bits. To
make 20-bit addresses out of 16-bit numbers, you need two of them, the segment address
and the offset address. This is the way it works: You move the segment address left by one
hex place and add it to the offset address to get the real 20-bit address (see Figure 10-5).
OEF1 «< Segment address shifted left one place
+_0100
OFO10 << Real 20-bit address
Figure 10-5
In other words, byte OEF1:0100 really means byte OFO10H, or byte 61456, in memory.
The lowest address is 0000:0000, byte 0 in memory, and the highest address is
FO00:FFFE, which corresponds to byte OFFFFFH, or 1048575. Using these 20-bit
addresses, we can refer to 1 megabyte, 1,024K (see Figure 10-6 below).
Segmented Address Real Address
FOOO:FFFF 1 Byte FFFFFH ~ € The top of memory
FO00:FEEE 1 Byte FFFFEH (FFFFFH = 1 MB — 1)
FO00:FFFD 1 Byte FFFFDH
FO00:FFFC 1 Byte FFFFCH
Figure 10-6
Segments in Memory
A segment is the memory space that can be addressed with one particular segment
address, and a segment can go from xxxx:0000 to xxxx:FFFF 64K. For example, the
segment that starts at the bottom of memory, segment 0000, can extend from 0000:0000
to 0000:FFFF (keeping the segment address, 0000, unchanged). Once you choose a
segment address, like 0000, you have a 64K workspace you can use without having to
change the segment address again.
On the other hand, even though segments can describe such a large area, they can
overlap. The next possible segment after segment 0000 is segment 0001. This segment
extends from 0001:0000 to 0001:FFFE Converting these numbers to 20-bit addresses
gives 00010H to 1000FH.
Segment 0001 starts just 16 bytes (called a paragraph) after segment 0000. Segment
0002 starts just 16 bytes after segment 0001, and so on. Choosing a segment gives you a
64K work space, but that 64K work space overlaps with many other segments too (see
Figure 10-7).
Segment 00002
The four segment registers are CS, DS, ES, and SS. They stand for Code Segment, Data
Segment, Extra Segment, and Stack Segment:
The code segment is where the instructions of your program will be stored. When your
program is loaded, the code segment is chosen for it by the program loader. We will not
have to set this segment register, CS, for the things we are going to do in this book. (See
Figure 10-8.)
cs
Code Segment
< CS holds the segment address
MOV AX,5 of your program’s code. It is
MOV DX,0 set for you.
Figure 10-8
The DS register holds the value of the data segment. We’ve seen this register and even
loaded values into it already in Chapter 9. Anything that you want to store as data, and
not have the computer execute (cell entries in a spreadsheet, for example, or text in a
word processor), can be stored here.
You usually set DS, the data segment register (if ever) at the beginning of the program
and then leave it alone. If, however, we want to read bytes from faraway places in memory
— to examine the screen buffer, or the keyboard buffer, for example — we'll have to set
DS before we can address them (just as we use DEF SEG in BASIC). Using DS as the high
word of our addresses, we can reach and read, or write any byte in memory.
Let’s say that our program code is in the segment 2000H, and data in the segment at
3000H (see Figure 10-9).
416 Pb Advanced BASIC
Figure 10-9
Now let’s say that we wanted to change data in the video buffer (i.e., the letters that
appear on the screen), which is at segment B800H for most monitors. We'd have to
change the data segment that we’re using, in DS, to B800H (see Figure 10-10).
CS=2000H 3000H — DS=B800H
Segment Segment
Figure 10-10
And then we could reference any data there with our instructions.
Whenever we read data from memory, the 80 X 86 checks the value of DS for the
segment part of the address. Instructions that reference memory locations (like the MOV
we used earlier) automatically mean that DS will be used as the segment address.
The Extra Segment can be used as another data segment, and we won’t make much use
of it here. There’s also the Stack Segment. DOS stores the return address for function and
subprogram calls on the stack, a special section of memory storage, and BASIC will pass
parameters to us on the stack. We'll learn more about the stack both in this chapter and the
next.
You can use the extra segment as well as the data segment when you want to
specify both a source and a destination for data, as in copying.
practice that we will not have to be careful about how the segment registers are set for
.COM files. Here, CS = DS = ES = SS, and DOS will set them for us.
A .COM file is the simplest working program you can write on the PC or PS/2
(although the .COM format is no longer supported under OS/2). This file is, quite liter-
ally, just machine language instructions, ready to be executed. Setting these files up is
going to give us the expertise we need to link assembly language into BASIC.
Assembler Directives
When you write an assembly language source file (which ends with the letters ASM),
to be turned into a .COM file, you have to specify where you want the code to be placed
in the code segment, whether you will have a separate data segment and other things. The
way you set up your segments is with assembler directives. These directives do not
generate any code. What they do is give directions to the assembler (and only that). They
work much like the SUB or FUNCTION statements in BASIC.
You can set up either code or data segments when you are writing a program, and you
use assembler directives to do it. If you are setting up a code segment, the program
instructions themselves will go there. If you are setting up a data segment, you are
predefining some variables or constants that your program will later use, or you are just
setting aside some blank space that the program will use. The assembler will make sure
that this information is loaded into the correct segments in memory if you specify them
with segment directives.
Everything we put into our .ASM files will be enclosed inside a segment definition. We
can add the code segment definition to the source code for the program PRINTZ we
wrote earlier:
~CODE <
MOV AH,2
MOV DL,5AH
INT 21H
INT 20H
From this point on, everything we write will be put into the code segment until the file
ends, or until the assembler finds another segment directive, such as .DATA to start a data
418 & Advanced BASIC
segment (which we'll explore later in this chapter). However, since there is only one seg-
ment in .COM files, everything goes into the code segment, and this is the only segment
directive we'll need.
Labels
We can label our data byte by byte, or word by word; labels are also directives. And, as
in BASIC, we can label an instruction in our program itself so that we can jump from the
current instruction to the labeled one, which may be some distance away.
For example, when MOV AH, is translated into machine language, it will be 3 bytes.
We can give a label to that instruction. Let’s call it Start, and we can also give a label to the
last instruction (INT 20H). Let’s call it Exit:
CODE
As in BASIC, to label an instruction, just use a name followed by a colon. If, during our
program, we wanted to leave quickly, we could just go the label Exit, and the INT 20H
instruction would be executed, causing us to finish and quit. If we did decide to go there,
the assembler would have to know the address of the Exit instruction, and it finds this by
counting the number of machine language bytes it has produced from the beginning of
the code segment (what we have labeled Start).
Be careful with long labels in asssembly language — the assembler only reads
the first 31 characters, so labels must differ during those characters.
header that is put at CS:0000, not the first instruction of the .COM file. This header is
100H — 256 decimal — bytes long. The header therefore runs from CS:0000 to CS:00FE
and the .COM file, now loaded into memory, starts exactly at CS:0100. (See Figure
10-11.)
.COM Files .EXE Files
: : MOV AX, 5
CS:0000 —_>- Header
Figure 10-11
This means that we are going to have to start the code in PRINTZ.ASM at offset 0100H
in the code segment. This is why we started assembling at 0100H in our DEBUG example
(using A100). In a .COM file, machine language instructions must start at offset 0100H
inside the program.
We can set our location in the code segment with the directive ORG (for origin):
- CODE
ORG 100H e
Start: MOV AH,2
MOV DL,SAH
INT 21H
Exit: INT 20H
END Start
This tells the assembler to make the offset of Start (the line immediately after the ORG
directive) 0100H. The assembler now treats the instruction MOV AH,2 as though it will
be placed at CS:0100, and not at CS:0000 (and everything to follow gets treated as
though it were placed after this instruction). In this way, we have made the correct
allowance for the .COM file header that is automatically created for the .COM file.
The last thing that is required is to set an entry point for the program. In BASIC, the
entry point is set when you define the main procedure — control is always passed to it
first. In assembly language, you can set the entry point anywhere in your program with
420 P& Advanced BASIC
the END directive. Every .ASM file needs to end with END so the assembler knows when
to stop. At the same time, you can set the entry point by adding its label after END.
In our case, we want the entry point to be at 0100H in the code segment. We have
labeled that instruction as Start already, so our final directive will be END Start. And that’s
it. Our program PRINTZ.ASM shown in Listing 10-1 is finished.
ORG 100H
Start: MOV AH,2
MOV DL,SAH
INT 21H
Exit: INT 20H
END Start oe
Assembling PRINTZ.ASM
If you have a word processor or editor, use it to type our program into a file that you
name PRINTZ.ASM; now we're ready to use an assembler. If you have the Microsoft
assembler (we will be using Microsoft MASM version 5.1), type this command:
R\:>MASM PRINTZ;
Microsoft (R) Macro Assembler Version 5.10
Copyright (C) Microsoft Corp 1981, 1988. Ait rights reserved.
0 Warning Errors
0 Severe Errors
We've assembled the program, but, so far, all we have is an .OBJ file. The next step
is
stripping off some information left by the assembler in the -OB] file. Although
the linker
is normally used for combining .OBJ files into big executable files, even single
.OBJ files
have to go through the linker before becoming .COM files.
The linker checks all segments, among other things. Since this is going
to be a .COM
file, there is only one segment, the code segment. Programs that
are not .COM
> Welcome to Assembly Language 421
files need all segments to be explicitly spelled out, and the linker is going to give us a
warning here that we have no stack segment. This is the normal warning you recieve
when you are producing .COM files. Use LINK like this:
R:\>LINK PRINTZ; e
Microsoft (R) Overlay Linker Version 3.64
Copyright (C) Microsoft Corp 1983-1988. Alt rights reserved.
The warning is there, and we are now almost ready: the linker has taken the .OB] file,
PRINTZ.OBJ, and produced an .EXE file, PRINTZ.EXE. We did not set this up as a .EXE
file, however; it’s a .COM file. For the final step, stripping off the header that the linker
left in the .EXE file, we run a DOS program called EXE2BIN. Run this on output from
LINK. This is the last step in the process, and it converts the .EXE file into .COM format:
Finally, our .COM file is there, ready to go. Give PRINTZ.COM a try. It does indeed
print out Z, just as our DEBUG version did. Now we’ve made another advancement.
We've got a working .ASM file.
We use the DB directive to set bytes apart in memory so that we can store data in them.
To define a byte named, say, VALUE, use DB like this:
422 }& Advanced BASIC
VALUE DB 5
If you wanted to put this data into a data segment, the code might look like this:
- CODE
MOV AL, VALUE
» DATA
— VALUE DB5
We ended the code segment and began the data segment by using the .DATA directive.
You can’t use .DATA in .COM files, but you can when designing .EXE files. Everything
that follows .DATA will go into the data segment. In memory, the code and data segments
might then look something like Figure 10-12.
cs DS
Code Segment Data Segment
Figure 10-12
Now we are free to read the data in VALUE in the program:
This is the way to use memory in the PC or PS/2: Set aside locations for it with DB and
give variable names to all the bytes you set aside. Then, you can use these names just like
variables in BASIC.
We can also store data a word at a time with the DW, or define word, directive like this:
> Welcome to Assembly Language 423
- CODE
MOV AX,WORD VALUE
DATA
> WORD_VALUE DW &H1234
Let's see how all this works by adding data to PRINTZ.ASM. In this case, we'll have to
put it into the code segment because there is only one segment available. The only data
we have is the character we’re going to print out, Z, so let’s store that in memory:
«CODE
ORG 100H
Our Character DB "Z" ¢
Start: MOV AH,2
MOV DL,5AH
INT 27H
Exit: INT 20H
END Start
DB tells the assembler that the data that follows is to be put in the program without
interpretation: It is data. We have set aside a location, one byte, that we've called
Our_Character, and initialized it by putting the character Z in it. The assembler will
translate the Z for us into the ASCII code that the machine needs: 5AH. (Alternatively, we
could have Our_Character DB 5AH. It is the same thing to the assembler.) Here are some
DB examples:
Flag? DB O
Char Zz 0B. “Z"
Numbers DB 1,2,3,4,5,0 <— 6 bytes are put aside.
Prompt DB “How long has it been since you called "
DB “your mother?” :
When we refer to the names Flag7, Char_Z, Numbers, or Prompt, we are actually
referring to the first byte that follows DB. For example, if we were to say:
MOV AH,Numbers
424 } Advanced BASIC
The 1, that is, the first number after DB, would be loaded into AH.
In PRINTZ.ASM, here is how we load our character into DL, just before printing it out:
CODE
ORG 100H
Our_ Character DB "Zz"
Start: MOV AH,2
MOV DL,Our_Character <<
INT 21H
Exit: INT 20H
END Start
Now we've been able to label and use a memory location. However, we've left our-
selves with a problem. The label Start is supposed to be at 100H in the code segment, and
now that we've added 1 byte of memory space just before it, it will be at the wrong
location (specifically, 101H). To solve this problem, we do what most .COM files with
data do. We set aside a data area at the beginning of the program, and add a jump
instruction, so that when things start at 100H, the first thing the microprocessor will do is
jump over the data area and to the first instruction. Listing 10-2 shows the program.
ORG 1008
Start: JMP PrintZ <—
Our Character DB 2)
PrintZ: MOV AH,2
MOV DL,Our Character
INT 21H
Exit: INT 20H
END Start
We've moved the label Start to point to a new instruction that is at 100H, but that
instruction says: JMP PrintZ. This instruction lets control jump over the data area to the
label PrintZ and then continue; JMP is an assembly language instruction that is exactly
like a GOTO in BASIC. To use JMP, just provide it with a label to jump to, as we've done
here MP PrintZ). ;
And that’s it; PRINTZ.ASM — updated to hold data — is done. It’s ready to be
assembled and run. If we want to fit in more data, we can insert it right after Our_
> Welcome to Assembly Language 425
Character. That’s it for setting up .COM files. In general, Figure 10-13 shows how a
-COM file shell looks.
-CODE
ORG 100H
Start: JMP PROG
This the data area. Use DB here.
PROG:
And this is where the program goes.
Exit: INT 20H
END Start
Figure 10-13
There is an area set aside for data (using DB or DW) and a part for the program code.
Now let’s develop a few more skills that we'll need.
Strings in Memory
We still have not resolved how to store character strings in memory. A character string
— like a STRING in BASIC — is just a number of characters, one after the other, that
makes sense to us but not to the computer. We want to keep them together: the computer
only sees a number of bytes with no apparent relation.
Strings in assembly language are just stored as bytes, as they are in BASIC. In assembly
language, they have a terminator to mark the end of the string. Although strings usually
end with 0 (in what we've seen is called ASCIIZ format), this is not always the case.
Strings are stored like PROMPT above, and if you want the string to end with a 0 byte, it
is your responsibilty to put it in:
The assembler lets you store strings this way, using the quotation marks as shorthand
(otherwise, you’d have to use DB for each letter).
The string printing service, service 9, of interrupt 21H prints out strings. It’s the
PRINT of assembly language. To terminate the string for this service, a $ (not a 0 byte) is
added as the last character. This is an indication to service 9 to stop printing
426 }& Advanced BASIC
and is one of the times strings are not terminated with a 0 byte. Here’s how it might look
if we wanted to start changing our program to PRINTXYZ:
- CODE
ORG 100H
Start: JMP Printz
Our Characters DB "XYZ$" <—
PrintZ: MOV , Ce
MOV DL,Our_ Character S
INT 2148 ee
Exit: INT 20H
END Start
Notice the $ terminating character, the last byte in the string. We still have to tell
service 9 where to find this string in memory, and change the call from service 2 to service
9. This service requires the address of the string in DS:DX. If the address was OEF1:0105,
for example, we’d have to load OEF1H into DS, and 0105H into DX.
Since we are dealing with a .COM file, the value of DS never changes, so DS is already
set for service 9. When the program runs, DS will be pointing at the code segment (i.e.,
the only segment). To get the offset address of Our_Characters into DX, we use the
OFFSET directive like this:
- CODE
ORG 100H i oe
Start: JMP Printz
Our Characters 0B "XYz$"
Printz: MOV AH,9 —
MOV DX, OFFSET Our Characters e
INT 21H
Exit: INT 20H
END Start
The OFFSET directive gives you a label’s offset value from the beginning of the data
segment (which is the same as the code segment here). You can think of it as returning a
pointer to that object like VARPTR( ). For example, our line (below)
will load the offset of the first byte of Our_Characters into DX. OFFSET is a handy
directive that we'll use often (especially in the next chapter), since many interrupt ser-
vices require that we pass them the address of data.
> Welcome to Assembly Language 427
That's all there is to it. Now we have a working .ASM file that prints out XYZ instead of
just Z.
Using Comments
Before we leave PRINTXYZ, let’s discuss commenting our code. Comments can be
added in assembly language by preceding them with a semi-colon (;) as shown in Listing
10-3.
By reading the comments, you can see what was intended in each line. Comments are
often more important in assembly language than in BASIC, and they can be a great help.
Now that we have the fundamentals of program writing down, let’s broaden our
library of instructions and start to read some keys from the keyboard. Working with
keyboard input will give us some experience in a vital assembly language topic: condi-
tional jumps (the IF..THEN statements of assembly language).
- CODE
ORG 100H
Start: JMP CAP
;Data Area
CAP:
gProgram will go here.
- CODE
ORG 100H
Start: JMP CAP
;Data Area
CAP: MOV AH,1 j;Request keyboard ides
- INT 27H ffrom INT 21H
END Start
After the INT 21H instruction is executed, the ASCII code of the typed character is in
AL. The program’s job is to capitalize the letter and print it out. There is an easy way to
capitalize letters — ASCII codes for lowercase letters, (e.g., a) have higher values than the
ASCII codes for uppercase letters (e.g., A). The ASCII codes for A to Z run from 65 to 90;
for a to z from 97 to 122. To capitalize a letter we just have to subtract a number from its
ASCII code to move the code from its place in the a...z part of table to its corresponding
place in the A...Z part:
- CODE
ORG 100H
Start: JMP CAP
Data Area
CAP: MOV AH,1 sRequest keyboard input
INT 21H zFrom INT 24H
— sus AL,"“a"=-"A" sCapitalize the typed key
END Start
sus AL,5
Here, 5 is subtracted from the contents of AL. AL is changed. In the same way, you
could do this:
SUB AX,DX
This subtracts the contents of DX from AX. AX is changed, DX is not. Besides the SUB
instruction, there is ADD, the built-in add instruction, which works like this:
ADD AL,5
ADD AX,DX
We will use ADD and SUB frequently. In addition, the assembler lets you use
expressions like “a” — “A”. For instance, we can use a line like this:
suB AL,“a"—"A"
430 } Advanced BASIC
which makes what we are doing much clearer than if we simply said:
SUB AL, 32
Now that we've read a character and capitalized it by subtracting a from A, we have to
print it out. Interrupt 21H service 2, which prints a character on the screen, expects the
ASCII code of the character that it is to print in DL. In CAPASM so far, the ASCII code is
still in the AL register (because service 1 returned it there). We move that code from AL to
DL and then print the character out (see Listing 10-4).
END Start
And that’s it; CAPASM is complete. We read in a typed key with INT 21H service 1,
capitalize it ourselves, and then print it out with INT 21H service 2. Type it in, assemble
and produce CAPCOM. Give it a try. When you run it, you see this:
Ds \>CAP
The program waits for a key to be typed. As soon as you type a letter, say s, it echoes it
and prints out a capital S. Then it simply exits:
> Welcome to Assembly Language 431
D:\>CAP
ss a
D:\>
Although it is gratifying to get the result we expected, there are a number of problems
with this progam. Perhaps the most serious one is what happens if you type in some
character other than a lowercase letter? Odd characters will be printed, since we are only
ready to handle lowercase letters.
This problem may be fixed if we check the incoming ASCII code to make sure that it
actually represents a lowercase letter. In other words, we have to check that the ASCII
code is between the values for a and z. This type of checking brings us to the topic of
conditional jumps, which are extremely important in assembly language, since they are
almost the only branching instructions available.
Conditional Jumps
We want to augment CAPASM to check that the incoming ASCII code is between a
and z. If it is not, we exit. To make this check, we divide the process into two steps. First,
we check whether the character is greater than or equal to a; next, we check whether it’s
less than or equal to z. If bothh tests pass, we capitalize the letter, type it, and exit.
Checking a value against some known comparison value is done with the assembly
language instruction compare, CMP. To branch on the results of the comparison, we then
use a conditional jump immediately after the CMP instruction. Unlike BASIC, com-
parisons are a two-step processes in assembly language. For example, here is the code to
check whether the value in AL (i.e., the ASCII code read from the keyboard) is above or
equal to a:
- CODE
ORG 100H
Start: JMP Cap
zData Area
MOV AH,1 zRequest keyboard input
Cap:
INT 21H jFrom INT 21H
AL,“a" ;Compare the incoming ASCII code to “a”.
> CMP
Exit ;Ilf the letter is not lower case, exit.
J8
432 Pb Advanced BASIC
END Start
Here, we’ve compared AL to the ASCII value for a and then immediately followed with
a JB — Jump if Below — instruction:
If the comparison indicates that the value in AL was below a in value, we will jump to
the label Exit at the end of the program and leave without capitalizing it. What actually
happens is this. First, the microprocessor’s flags are set by the CMP instruction, and then
the JB instruction checks these internal flags and acts accordingly:
Besides CMP, all the math instructions like ADD, SUB, or MUL set the flags
too, SO you can use conditional jumps after these instructions without a CMP.
The next step is to check whether the ASCII code is below or equal to z. This is done
with an instruction named, as you could probably guess, JA for Jump if Above. That
completes the program CAPASM (shown in Listing 10-5), our first real assembly lan-
guage program. It accepts input, and generates output, and even checks for errors.
END Start
You can see that there is a rich selection of jump instructions. Without such a selection
of conditional jumps, assembly language would be very difficult to use. As it is, there are
conditional jumps that meet most needs. If you are new to assembly language, it might
take you a while to become practised in their use.
434 P Advanced BASIC
Now let’s put our assembly language expertise to work with the final example of the
chapter, a program that converts hex numbers to decimal numbers.
- CODE
ORG 100H
ENTRY: JMP DEHEXER
DEHEXER:
#Program will go here.
INT 208
END ENTRY
First, we'll have to read the hex number from the keyboard. DOS provides special
input services to read strings, and we can use them by setting up a buffer in memory with
the DB directive like this:
BUFFER DB #, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
To fill this buffer, we will use INT 21H, service OAH — get string. This is the LINE
INPUT of assembly language. We set the number (# above) in the beginning of the buffer
to the length of the buffer. (Service OAH needs that number so it won't return too many
bytes.)
es Service OAH always sets the last byte of the buffer to ASCII 13 (a carriage
return) as an end-of-string marker, so you must leave room for one
more than
the number of characters you expect as input.
EE ee
> Welcome to Assembly Language 435
The second byte in the buffer will be filled by service OAH with the number of bytes
actually typed. If we’re careful, we can set up our buffer with some foresight by expressly
labeling the important bytes in it:
CODE
ORG 100H
ENTRY: JMP DEHEXER
+ BUFFER DBS
NUM_TYPED DB O
ASCTI_NUM DB 3 DUP (0)
END_NUM dB O
CRLF DBO
DEHEXER: MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
MOV AH, OAH
MoV DX,OFFSET BUFFER
INT 21H
INT 20H
BUFFER DBS
NUM_TYPED DBO
= ASCII_NUM DB 3 DUP (0)
END_NUM DB O
CRLF DB O
~ CODE
ORG 100H
ENTRY: JMP DEHEXER
> PROMPT DB “Type in a 4 digit hex number:$”
BUFFER DB 5
NUM_TYPED DB O
ASCII_NUM DB 3 DUP (0)
END NUM DB OO
CRLF DB OO
DEHEXER: MOV AH,9
+> MoV DX,OFFSET PROMPT
INT 21H
*°
Next, we use can service OAH to read the four-digit hex number from the keyboard.
This is the number that we'll convert to decimal and print out. We just have to pass the
offset of the beginning of the buffer to service OAH in DX, and that looks like this:
- CODE
ORG 100H
ENTRY: JMP DEHE XER
PROMPT DB "Type in a 4 digit hex number:$"
BUFFER DB 5
NUM TYPE D DBO
ASCII NU Le] DB 3 DUP (0)
END_NUM DB O
CRLF DB O
DEHEXER:
MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
MOV AH,OAH
~ MOV DX,OFFSET BUFFER
INT 21H
INT 20H
Next we issue an INT 21H instruction and accept the hex number.
> Welcome to Assembly Language 437
If you try to type more than the number of characters we can accept in the
buffer, the computer will beep. This is the same beep that DOS uses, and for
i
that matter, the same internal service (OAH) that it uses to accept keyboard
input at the DOS prompt.
After the buffer has been filled with input from the keyboard, the ASCII string extends
from the locations ASCII_NUM to END_NUM. The <cr>— CHR$(13) — at the end of
the returned string will go into the byte marked CRLF and we can ignore it. The first step
is to convert our character string into a number. If the number typed was 1234H, then
the buffer now looks like Figure 10-15.
BUFFER DB 5, 4, ah eae “9” Sa <cr>
CRLF
END_NUM
ASCil_NUM
NUM_TYPED
BUFFER
Figure 10-15
We can just point to the last number, 4, convert it from ASCII to binary, then point to
the next number, 3, convert it to binary, multiply by 16, add it to the 4 we already have,
and keep going to higher places. In this way we loop over all characters.
We have labeled the last ASCII digit as END_NUM, so we can read the ASCII character
at that location with the instruction MOV AL, END_NUM. But how do we point to the
previous digits?
We can use a register as a pointer. In assembly language, the BX register was designed
explictly to be used as a pointer; let's examine how that’s done. We'll need a loop to read
all four digits, so we start off by loading BX with the offset address of END_NUM:
- CODE
ORG 100H
ENTRY: JMP DEHEXER
PROMPT DB "Type in a 4 digit hex number:$”
BUFFER DB 5
NUM_TYPED DB O
ASCII_NUM DB 3 DUP (0)
END_NUM DBO
CRLF pB O
DEHEXER: MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
438 »& Advanced BASIC
MOV - AH,OAH
MOV DX,OFFSET BUFFER
INT 21H
MOV cx, 0
MOV AX,0
> MOV BX, OFFSET END NUM
LOOP1:
JB LOOP!
INT 20H
END ENTRY
Now we'll load the ASCII character into the DL register. We can do that like this:
- CODE
ORG 100H
ENTRY: JMP DEHEXER
PROMPT DB "Type in a 4 digit hex number:$"
BUFFER dB OS
_ NUM_TYPED DB O
ASCII NUM DB 3 DUP (0)
END_NUM DB O
CRLF DB O
DEHEXER: MOV AH,9?
MOV DX,OFFSET PROMPT
INT 21H
MOV AH,OAH
MOV DX,OFFSET BUFFER
INT 21H
JB LOOP
INT 20H
END ENTRY
Putting BX inside square brackets, [BX], means that the microprocessor will use the
value in BX as an address. This is called indirect addressing, and it’s practically going to
be the foundation of Chapter 11. [BX] stands for the byte at location END_NUM right
now because BX holds that byte’s offset address (see Figure 10-16).
> Welcome to Assembly Language 439
ren
BUFFER DB 5, 4, “1”, “2”, “3”, “4”, <er
| +— CRLF
END_NUM
ASCil_ NUM
NUM TYPED
BUFFER
i
Figure 10-16
And we can move that byte into DL like this: MOV DL, [BX]. After moving the byte
into DL, we decrement the pointer BX by 1 with the DEC instruction, which is just like
SUB BX,1. (The corresponding instruction for incrementing by 1 is INC.) This points us
to the previous ASCII character in preparation for the next time through the loop (see
Figure 10-17).
[BX]
BUEFER DB 5; 4,41), «2.3, 4, <ci>
CRLF
END_NUM
ASCil_NUM
NUM_TYPED
BUFFER
Figure 10-17
And that is how indirect addressing [BX] works. BX holds the offset address (in the
data segment) of the byte or word we want to locate.
Now that the last ASCII character (4) is in DL, we have to convert it into binary. If it’s
in the range 0 to 9, we have to subtract the ASCII value for 0 from it (i.e., 0 will become
0, 1 will become 1, and so on). If it’s in the range A to F we have to subtract A and add 10
to it. We do that this way:
-CODE
: ORG 100H
ENTRY: JMP DEHEXER
: PROMPT DB "Type in a 4 digit hex number:$"
BUFFER DB 5
NUM TYPED DB O
ASCII_NUM DB 3 DUP (0)
END_NUM DB OO
/ CRLF DBO
DEHEXER: MOV AH,9
MOV DX,OFFSET PROMPT
440 P& Advanced BASIC
Int 214
MOV AH, OAH
MOV DX, OFFSET BUFFER
INT 21H
MOV BX, OFFSET END_NUM
LOOP1; MOV DX,
MOV DL, CBX]
DEC BX
CMP DL, "9" -
JBE UNDER_A
sus oL, “at — *0" - 10
UNDER_A:SUB dL, "0"
JB LooP1
INT 20H
END ENTRY
DL now holds the numerical value of the current hex digit. To convert the entire four-
digit number to decimal, we have to multiply each digit by the appropriate power of 16
and add it to a running total. And, to multiply by 16, we can shift the value in DL left.
There is an assembly language instruction called SHL, for shift left (and there’s also
SHR for shift right). Every time we shift an operand left by one binary place, it’s the same
as multiplying by 2. We load the number of places to shift left into CL and shift the value
in DL left like this:
SHL BL, CL
For example, if CL held 2 and DL held 000000018, then after SHL DL,CL,DL would
hold 00000100B. Shifting left by four binary places is the same as multiplying by 16, and
each time through our loop, we'll add 4 to CL so that SHL DL,CL will produce the next
hex digit. Note that when we shift DL left by more than two hex places, however, the
result will be bigger than a byte can hold. Instead, we will use the whole DX register, not
just DL:
SHL BX, CL
After it is shifted, we have to add this current hext digit to the running total.
Let’s keep
the running total in, say, AX, and add DX to it each time we loop
through. (See Listing
10-6.)
+ MOV €x.00
MOV AX,0
MOV BX, OFFSET END NUM
LOOP1: MOV DX,G <_
MOV DL, [Bx]
DEC BX
CMP DL, "9"
JBE UNDER_A
suB DL, “a® = “O" - 10
UNDER_A:SUB Bi; 6”
SHL DX, CL
ADD AX, DX
ADD CL,4
CMP CL,16
JB LooPi
INT 20H
END ENTRY
Every time through the loop, we load the ASCII value into DL, make it into a binary
number, shift it to the left, and add it to the running total in AX. We only loop four times,
one for each digit.
At this point, the ASCII string has been converted into binary, and its value is in AX.
We still have to convert it back to decimal ASCII digits. The way to do that is to peel off
successively the decimal digits by dividing the number in AX by 10. Each time we divide
by 10, the remainder is a decimal digit. For example, if we divided the number 21 by 10,
the result would be 2 with a remainder of 1.
442 }& Advanced BASIC
There is a divide instruction in assembly language, named DIV. If you load the number
to divide into AX and divide by a byte-long register like this:
DIV BL
then the instruction divides AX by BL. AX is assumed to hold the number to divide when
you divide by a byte. The quotient is returned in AL and the remainder in AH.
On the other hand, if you give this instruction (with a 16-bit register):
DIV BX
then the microprocessor assumes that you are dividing the double word number in
DX:AX by the specifed register, BX.
Similarly, MUL BL will multiply AL by BL and leave the result in AX. MUL BX will
multiply BX by AX and leave the result in DX:AX.
It’s a good idea to plan ahead with your use of registers so that instructions like
MUL will leave the results in the registers you want, and you don’t have to
move data from register to register too much. You'll often see this in assembly
language programs.
If we use the DIV BX instruction, the number in DX:AX will be divided by BX, so let’s
load DX with 0 and BX with 10. AX already holds the number to convert. After the
division is through, AX will hold the quotient (ready to be divided by 10 again in the next
pass to peel the next decimal digit off) and DX holds the remainder:
Ee>e
————— Welcome to Assembly Language
EIQUAQO 443
GS
- CODE
ORG 100H
ENTRY: JMP DEHEXER
PROMPT DB “Type in a 4 digit hex number:$"
BUFFER DB 5
NUM_TYPED DB O
ASCII_NUM DB 3 pUP (0)
END_NUM DB O
CRLF dB O
DENEXER: MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
MOV AH,OAH
MOV DX,OFFSET BUFFER
INT 21H
MOV Cx, O
MOV AX,0
MOV BX, OFFSET END NUM
LOOP1: MOV Dx,0 aS
MOV DL, ECBXIJ
DEC Bx
CMP DL,"9"
JBE UNDER_A
sus DL, “a” - "0" - 10
UNDER_A:SUB DL, "O"
SHL DX, CL
ADD AX, DX
ADD CL,4
CMP CL,16
JB LOOP1
MOV cx,0
MOV BX, 10
LOOP2: MOV Dx,0
DIV BX « After this, DX holds current decimal digit.
INT 20H
END ENTRY
The remainder in DX is what we want. It’s the current decimal digit. Note that we are
peeling the digits off in backwards order. For example, if we had the number 4,321
(decimal) in AX, the first time we divided by 10 we would get a remainder of 1, the next
time a remainder of 2, and so on:
To store these decimal digits now in DX, we push them on the stack, using the
instruction PUSH Dx. The stack is a part of memory specially reserved to hold data, and,
in the next chapter, BASIC will PUSH values onto the stack for us to read. Let’s see how
the stack works. If there was a value of 5 in DX, and we executed the instruction PUSH
DX, the stack would look like Figure 10-18 (we can only PUSH words, not bytes).
a 5
Figure 10-18
444 » Advanced BASIC
Next we might push CX, which might hold a value of 3 (see Figure 10-19).
5 Stack “Grows” downwards — towards
lower memory locations
3
Figure 10-19
After that, we might push AX, which holds 0 (see Figure 10-20).
5
Figure 10-20
Then we could give the instruction POP BX to retrieve values from the stack. The value
placed on the stack last — 0 — will be popped into BX, and the stack will look like
Figure 10-21.
Figure 10-21
Another POP, say POP DX, would place 5 into DX and the stack would look like Figure
10-22.
5
Figure 10-22
POP CX would load that value into CX, and the stack would be empty. Notice
that
values come off the stack in opposite order of going on (this is going to be valuable
to us).
In our example DEHEXER, we'll strip each decimal digit off the value now
in AX and
PUSH it onto the stack. A decimal value of 8,573 would be pushed
onto the stack as 3, 7,
5, and 8 (we'll also keep track of how many numbers we’ve pushed so we
can POP them
later — a four-digit hex number may give from 1 to 5 decimal digits),
> Welcome to Assembly Language 445
|NOTE: | When we pop them, these digits will come off in the order we want to print
them: 8, 5, 7, and 3.
Here’s the way we load each decimal digit onto the stack:
- CODE
ORG 100H
ENTRY: JMP DEHEXER
PROMPT DB “Type in a 4 digit hex number:$”
BUFFER DB 5
NUM_ TYPED DB O
ASCII_NUM DB 3 DUP (0)
END_NUM DB O
CRLF dB O
DEHEXER: MOV AH,9
MOV DX,OFFSET PROMPT
INT 27H
MOV AH,OAH
MOV DX,OFFSET BUFFER
INT 21H
MOV cx, 0
MOV AXx,0
MOV BX, OFFSET END _NUM
LOOP1: MOV DXx,0
MOV DL, CBX]
DEC BX
CMP Di 9"
JBE UNDER_A
SUB Sepa - 10
UNDER_A: SUB pt
SHL DX, CL
ADD AX, DX
ADD CL,4
CMP CL,16
JB LOOP1
MOV cx,0
MOV Bx, 10
LOOP2: MOV Dx,0
DIV BX
PUSH DX
cx «~;CX holds the number of digits pushed
INC
pAnything left to strip digits off of?
CMP AX,0
JA LooP2 ZIF yes, loop again
INT 20H
END ENTRY
number
At this point, we’re almost done. The decimal digits are on the stack, and the
“That number in decimal is:’ ’ with
of digits is in CX. Let’s print out a message saying
service 9 of INT 21H:
446 Pb Advanced BASIC
-CODE
ORG 100H
ENTRY: JMP DEHE ER
PROMPT : DB "Type in a 4 digit hex number:$"
BUFFER DB 5
NUM_TYPE D DB O
ASCTI_NU M DB 3 DUP (0)
END_NUM DB O
CRLF dB O
+> ANS _STRI NG DB 13, 10, “That number in decimal is: $”
DEHEXER: MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
MOV AH,OAH
MOV DX,OFFSET BUFFER
INT 21H
MOV Cx, 0
MOV AX,0
MOV BX, OFFSET END_NUM
LOOP1: MOV Dx,0
MOV DL, EBX]
DEC BX
CMP DL,"9"
JBE UNDER_A
SUB DL, “a” - "9" - 10
UNDER_A:SUB DL, "Oo"
SHL DX, CL
ADD AX, DX
ADD CL,4
CMP CL,16
JB LOOP1
MOV Cx,0
MOV BX, 10
LoOP2: MOV 0x,0
DIV BX
PUSH DX
INC Cx
CMP AX,0
JA LOOP2
~ MOV AH,9
MOV DX,OFFSET ANS STRING
INT 21H
INT 20H
END ENTRY
And then we can just print out the digits using service 2 of INT
21H and POP Dx.
Note that since we peeled the digits off in reverse order, and that
since using the stack has
reversed that order once again, we can just print digits as we
POP them. In other words, if
the number was 1,2 34, we would have peeled the digits off and
pushed them in the order
of4, 3, 2, and 1, so the stack looks like Figure 10-23.
> Welcome to Assembly Language 447
Figure 10-23
The first digit to be popped, and printed, is 1, followed by 2, and so on, meaning that
we'll print 1,234. Each digit still has to be converted to ASCII. For example, if we pop 1
off the stack, that’s still not ASCII “1”. To convert them to ASCII, we have to add the
ASCII value for “O” to them; i.e., 0 will become “0”, 1 will become “1”, and so on.
Here’s the loop where we pop the digits. We’ll pop them into DX since service 2
expects the ASCII character to print to be in DL, and use the new LOOP instruction:
-CODE
ORG 100H
ENTRY: JMP DEHE XER
PROMPT DB "Type in a 4 digit hex number:$"
BUFFER DB 5
NUM_TYPE D DB O
ASCII_NU 4 DB 3 DUP (0)
END_NUM DB O
CRLF DB O :
ANS _STRI NG DB 13, 10, “That number in decimal is: $”
DEHEXER: MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
MoV AH,OAH
MOV DX,OFFSET BUFFER
INT 24H
MoV cx, O
Mov AXx,0
MOV BX, OFFSET END_NUM
LOOP1: MoV bdx,0
MOV DL, CBX]
DEC BX
cmp DL,"9"
JBE UNDER_A
SUB DL, Har = HQ” 10
SHL ox, LL
ADD AX, DX
ADD CL,4
cMP CL,16
JB LOOP’
MOV cx,0
448 P Advanced BASIC
MOV BX, 10
LOOP2: MOV DxX,0
DIV BX
PUSH DX
INC Cx
CMP AX,0
JA LooP2
MOV AH,9
MOV DX OFFSET ANS STRING
INT 21H
MOV AH,2 co
LOOP3: POP DX
ADD ox,"0"
INT 21H
LOOP LOOPS
INT 20H
END ENTRY
The LOOP instruction is just like a FOR...NEXT loop in BASIC. Here we use LOOP to
loop over the pushed digits, pop them, and print them out. In order to use LOOP just fill
CX with the number of times you want to loop, define a label, and loop like this:
MOVs €X,,5
Loop_1:
Loop = LooP_1
MOV AH,2
LOOPS: POP Dx
ADD Dx,"0"
INT 21H
Loop LOOPS
reversed their order, and now printed them out. We've made tremendous progress.
The program works. Give it a try. It accepts four-digit hex numbers and prints out the
correct decimal version. On the other hand, there is still one difference between it and
most .ASM files. Most .ASM files have at least one procedure defined inside them. This
will be the final topic of this chapter, and it’s crucial to know about procedures for
Chapter 11.
~ CODE
ORG 100H
ENTRY: JMP DEHEXER : :
PROMPT DB “Type in a 4 digit hex number:$”
DEHEXER PROC
MOV AH,9
MOV DX,OFFSET PROMPT : The DEHEXER Procedure
INT 208
DEHEXER ENDP
END ENTRY
«CODE
ORG 100H
ENTRY: JMP DEHEXER
PROMPT DB “Type in a 4 digit hex number:$"
BUFFER DB 5
NUM_TYPED DB O
ASCII_NUM pB 3 buP (0)
END_NUM oB O
CRLF dB O
13, 10, “That number in decimal is: $"
ANS_ STRING DB
DEHEXER PROC e
MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
MOV AH,OAH
450 » Advanced BASIC
We have added the PROC and ENDP directives, which define procedures. The PROC
directive lets the assembler know that you want to define a procedure, and the ENDP
directive indicates that the procedure definition is finished.
And, unless you are in the main procedure, you have to end the procedure with a
return, or RET instruction. Let’s break DEHEXER into two procedures to see how this is
done. We can, for example, print out the decimal answer in a new procedure called
PRINT_NUM.
As soon as we've decoded the typed hex number into a binary value in AX, we can call
PRINT_NUM to do the job of stripping decimal digits from AX, placing them
hth> Professional
fardebacleInput
sda451
on the stack, and then printing them out. We call Print_Num with the instruct
ion CALL
Print_Num, and return at the end with a RET instruction (just like RETURN
at the end of
a BASIC subroutine). (See Figure 10-24.)
CALL Print_Num
INT 20H
Print_Num PROC
[Print AX]
RET
Print_ Num ENDP
Figure 10-24
Here’s how it looks in outline:
- CODE
ORG 100H
ENTRY: JMP DEHEXER
zData
DEHEXER PROC
| DEMEXER
CALL PRINT_NUM - | Procedure Ce
INT 200° —
- DEHEXER ENDP aa
PRINT _NUM PROC —
eee | PRENT_NUM
: Procedure
= RET
PRINT_NUM ENDP
) END _—ENTRY
This works as you’d expect it to. When you CALL PRINT_NUM, control is transferred
to the first line there. Execution continues until the return instruction, RET, is reached,
and control returns to the line just after the CALL PRINT_NUM instruction in the main
procedure. You never call procedures with arguments (like CALL PRINT_NUM(AX)).
Instead, you pass values in the registers. The whole program is shown in Listing 10-7.
452 }& Advanced BASIC
ORG 100H
ENTRY: JMP DEHEXER
PROMPT DB “Type in a 4 digit hex number:$"
BUFFER dB 5
NUM_TYPED DB O
ASCII_NUM DB 3 DUP (0)
END_NUM DB O
CRLF DB O
ANS STRING 0B 13, 10, "That number in decimal is: $"
DEHEXER PROC
MOV AH,9
MOV DX,OFFSET PROMPT
INT 21H
MOV AH,OAH
MOV OX,OFFSET BUFFER
INT 218
MOV cx, 0
MOV AXx,0
MOV BX, OFFSET END NUM
LOOP1: MOV DX,0
MOV DL, CBXI
DEC BX
CMP DL,"9”
JBE UNDER_A
$uB DL, “a” - "0" - 10
UNDER_A:SUB BL, OQ"
SHL DX, CL
ADD AX, DX
ADD CL,4
CMP CL ,416
J8 LooP1
CALL PRINT_NUM e
INT 20H
DEHEXER ENDP
PRINT_NUM PROC
MOV Cx,0
MOV BX, 10
LOOP2: MOV Dx,0
DIV BX
PUSH DX
INC Cx
CMP AX,0
JA LooP2
MOV AH,9
MOV DX,OFFSET ANS STRING
INT 21H
MOV AH,2
LOOP3: POP DX
> Welcome to Assembly Language 453
END ENTRY
Procedures in assembly language don’t specifically return any values, as functions can
in BASIC. Instead, information is returned in the registers, or, in some cases, in the flags.
That’s how PROC and ENDP work, and we'll see much more of them soon.
Conclusion
That’s it for our assembly language primer. Now that we’ve reached this point, we’re
up to speed in assembly language, and we're ready to press on. We can start putting all
our knowledge to work when we interface BASIC to assembly language, and we'll do that
next, in Chapter 11, BASIC/Assembly Language Interface.
st
EK apeudrn. ee
imaged oF ano
~~ a we ee
Fg
| Fa a
&, aa
ie
A - s _ ey
and
es oo a?
By linking in your own assembly language code into BASIC, you can streamline your
programs and add raw computing power. We will present many assembly language
examples for both BASIC functions and subprograms. To BASIC, these will look like
normal BASIC subprograms and functions although they will actually be written in
assembly language. For example, Listing 11-1 shows what a BASIC function that adds
two integers looks like in assembly language.
PUBLIC Addem
Addem PROC FAR USES DI SI DS ES, VALUET:WORD, VALUE2: WORD
MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
MOV AX, CBX]
MOV BX, VALUE2
ADD AX, CBX]
RET
Addem ENDP
END
If you wish, you can use the examples in this chapter as templates, keeping the BASIC
interface of the assembly language code intact and adapting the body of that code to suit
your needs.
Keep in mind, however, that this is an advanced topic. A good deal of the program-
ming that follows is not exactly straightforward.
contains an assembly language procedure named PrintString( ). The first thing you'll
have to do is to assemble the file:
D:\>MASM A;
Microsoft (R) Macro Assembler Version 5.10
Copyright (C) Microsoft Corp 1981, 1988. Alt rights reserved.
O Warning Errors
O Severe Errors
This produces A.OBJ. If you are using QuickBASIC, there’s only one way to make
PrintString( ) available to your programs. You have to link A.OBJ into a Quick Library
and load that library when you start QuickBASIC.
Let’s produce a quick library named A.QLB. That process works like this:
Now that we've created A.QLB, we can just start QB this way (where the program you
want to call PrintString( ) from is in PROGRAM.BAS):
D:\>Q@B PROGRAM
/L A
The /L command loads the Quick Library A.QLB into memory, ready for use. In
extended QuickBASIC, you build the Quick Library like this:
And now that you’ve created A.QLB, you start QBX this way:
D:\>Q@BX PROGRAM /L A
You can also use use the command line compiler, BC.EXE (any version
— 4.5 or 7.10,
for example). In that case, you’d begin by assembling A.ASM, the
assembly language
source for PrintString( ):
> BASIC/Assembly Language Interface 459
D:\>MASM A;
Microsoft (R) Macro Assembler Version 5.10
Copyright (C) Microsoft Corp 1981, 1988. ALL rights reserved.
49894 + 311895 Bytes symbol space free
0 Warning Errors
Q Severe’ Errors
This creates A.OBJ. Then, you would compile the BASIC program that calls
PrintString( ):
D:\>BC PROGRAM; \
Microsoft (R) QuickBASIC Compiler Version 4.50
(C) Copyright Microsoft Corporation 1982-1988.
ALL rights reserved.
Simultaneously published in the U.S. and Canada.
O Warning Error(s)
OQ Severe Error(s)
This creates PROGRAM.OBJ. Next, we link the two .OBJ files together with LINK
(note that you'll also have to add any other libraries required by the code in PROGRAM):
D:\>LINK PROGRAM + A;
IBM Linker/2 Version 1.10
(C) Copyright IBM Corporation 1984, 1988
(C) Copyright Microsoft Corp 1983, 1988. All rights reserved.
A tool named the Programmer's Work Bench (PWB) comes with the BASIC
PDS 7.10. The PWB is designed to facilitate working in multiple language
environments. Unfortunately, support has only been offered so far for connec-
ting BASIC with C, not with assembiy language.
Before beginning, we should also note that the QBX.EXE compiler occasionally han-
dles data (strings and arrays) a little differently than either BC.EXE or QB.EXE. For that
reason, some of the examples below will be split into two parts, one for BC.EXE and
QB.EXE and one for QBX.EXE.
460 P& Advanced BASIC
Then, before control is passed to MID$( ), MyString$ is pushed onto the stack, followed
by Place%, and then Num%. Pushing parameters in this order is called the BASIC calling
convention.
Each parameter passed is actually passed by near reference (unless you use the BYVAL
or SEG keywords). This means that what BASIC actually passes is the one-word offset
address of the variable in memory. For example, if we had a function named Add5%( ),
which adds 5 to a passed integer, and called it this way:
Valt = 2 -
PRINT 2 + 5 — “‘Add5%(Vala)
then BASIC pushes the offset address of Val% (from the beginning of the data segment)
onto the stack before calling Add5%( ). As soon as we get to Add5%( ), the stack looks
like Figure 11-1.
Val% Addr
Stack Grows Downward
|
Return
Addr
Figure 11-1
Here Return Addr is the (two-word) address that control returns to when Add5%( ) is
done. Let’s see how we can read the argument passed to us, Val%.
In the old days, the assembly language programmer had to read values like Val% Addr
directly off the stack, but the new, extended PROC directive (under MASM 5.1) saves us
the trouble. Instead, we can start the assembly language code for Add5 like this, with the
new, extended PROC definition:
> BASIC/Assembly Language Interface 461
Here we tell the assembler we’re going to use the DI, SI, DS, and ES registers in our
program (we do not actually use them; this is only for the purposes of demonstration).
The assembler then automatically adds code at the beginning to push and at the end to
pop these registers so that the original values in them that BASIC was using are preserved.
We also indicate that the parameter VALUE1, a word, will be passed to our program.
From now on, we can refer to VALUE] as we would to any other variable. That is, to
get the address of the integer that’s been passed to us into BX, we only need to do this:
> MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
Remember that what BASIC passes is the address of the parameter, not the actual
parameter itself. Since each address is an offset address, that is, one word, all passed
parameters will be one word long. We use that offset address to get the actual parameter
into AX, and then we can add 5 to it:
MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
> MOV AX, CBX]
ADD AX, 5
Our next job is to return the result to BASIC, and we'll see more about this process in
the next section. Briefly, it turns out that you return INTEGER values to BASIC from a
function in the AX register. Our result is already in AX, so we just have to add a return
instruction:
462 & Advanced BASIC
MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
MOV AX, CBX]
ADD AX, 5
> RET
Add5 ENDP
END
And that’s it. We’ve loaded the value being passed to us into AX, added 5 to it, and left
it in AX as the return value of our function. The calling program will read this values from
that register.
We'll make use of the extended PROC definition frequently in this chapter to pick the
parameters passed to us off the stack. In fact, the extended PROC definition will make the
our procedures very simple. Now let’s see how to return values to BASIC.
In other words, to return an INTEGER value, place the integer in the AX register and
exit. Io return a LONG integer, place the upper word in DX, the lower word in AX, and
exit.
For all other data types, you must return the offset address of the data in AX. This can
be a little tricky. If you plan to return a non-INTEGER or non-LONG type, you must
follow certain steps. BASIC passes you the address to store your return data at on the
stack when it calls your function. This address is at [BP+6] on the stack when you gain
control, where BP is special register, called the base pointer.
We've seen how the addressing mode [BX] works in the last chapter. [BP] works
in the
same way, except that BP will point us to some location in the stack section of memory. It
turns out that we actually don’t want the value at [BP], but at the address six bytes later;
that is, at BP + 6. The assembler allows us to refer to that value like this: [BP+6] (negative
values like [BP-6] or [BX-2] are also allowed). Here, then, are the steps that we'll follow
to
return values other than INTEGERs or LONGs:
> BASIC/Assembly Language Interface 463
1. When we gain control, we'll store the value at [BP+6] — the return value offset —
for later use. For example, we can load that offset into DX this way: MOV Dx,
[BP+6].
2. When we're ready to return data, we'll place our return value at that offset in
memory.
3. Then we place that offset into AX, and return.
We'll see how this works in the section Function Returning a String below. In the
meantime, let’s see how to work with the actual BASIC data that we receive.
Let’s get more familiar with each of these data types. To do advanced work with data
stored in these formats, we'll have to know the details of how they're stored inside BASIC.
In particular, this information will be crucial to interface BASIC to assembly language.
Unless we understand each data type at the byte-by-byte level, we won't be able to work
with it.
Let’s work through the different BASIC data formats, beginning with the easiest format,
integers.
464 }& Advanced BASIC
Value Stored As
1 &HO0001
29 &HO001D
32,767 SH7FFF
On the other hand, here’s how some negative INTEGERs are stored:
Value Stored As
-l QHPFFEE
-219 GQHEP25
-32,768 &H8000
|NOTE: | The top bit, bit 7, is always equal to 1 in negative INTEGERs and LONGs.
What BASIC calls an INTEGER, assembly language calls a word. You can
provide space in memory for a BASIC INTEGER just by using the DW
directive.
These negative numbers are stored in what is called two’s complement notation, which
was mentioned as long ago as Chapter 3, and we should understand it before continuing.
11114111111111111B
+ 1
NARA i
Carry
Figure 11-2
The result is 1OOOOOO0000000000B. That is, the INTEGER is left holding
0000000000000000, or 0, and there is a carry since the 1 is in the 2” place (65,536),
more than the integer’s capacity to hold. If we ignore this carry, and only look at the 16
bits that fit into the integer, we are left with OOOOO00000000000B. In other words,
GHFFFF + 1 = 0. That’s exactly what happens when we're working with two’s comple-
ment notation. We ignore the carry. That means that @HFFFF is the two’s complement of
1 in INTEGER format. In LONG format, this number would be QHFFFFFFFE
If you take a number like 5, which is 0000000000000101B in INTEGER format, then
flip all its bits, 1111111111111010B (= @HFFFA), and add the two together, you get all
ones (see Figure 11-3).
0000000000000101B < 5
+ 1111111111111010B 5 with bits flipped [ &HFFFA ]
4119111111111111B = &HFFFF
Figure 11-3
And adding 1 to this sum gives us 0 with a carry (see Figure 11-4)
0000000000000101B < 5
+ 1111111111111010B 5 with bits flipped &HFFFA
+ 1
ew apr = 0 with a carry
Carry
Figure 11-4
Since we ignore the carry, this result is 0. Therefore, -5, the two's complement of 5, must
equal GHFFFA + 1, which is @HFFFB. And this is how negative numbers are found.
For example, to find -1, start with +1, which is stored in INTEGER format as
0000000000000001B (16 bits). First, reverse all the bits. Zeros become ones and ones
Heeome zeros, PLT NPITITITIIIIOB. “Now, add 2 tothe result to get
1111111111111111B. This is the two’s complement of 1; that is, -1 is stored as the
466 }& Advanced BASIC
S Exp. — Significand.
Figure 11-5
> BASIC/Assembly Language Interface 467
This can be made into Hex by grouping every four binary digits together since four
binary digits represent exactly one hex digit (see Figure 11-7).
01000001 001010000000000000000000
i fi dat aa
eet ee eee ee Pee e602? ()
Figure 11-7
So 10.5 is stored as GH41280000.
The format for DOUBLEs (8 bytes) is shown in Figure 11-8.
Bit# 63 62 52 51 0
S Exp. Significand.
Figure 11-8
It’s worth noting that the format for both SINGLEs and DOUBLEs is the standard IEEE
format, which is the same format used by the 80 X 87 co-processors, so you can interface
with them easily.
The easiest way of handling BASIC’s floating point numbers from assembly
language is to let BASIC handle all floating point math and then pass both
mantissa and exponent to your assembly language code as integers, avoiding
the need to decode them.
CURRENCY
The currency type is pretty easy. It’s just an 8-byte two’s complement integer, scaled by
10,000. That is, to find the amount represented by a variable of type CURRENCY, just
treat the eight bytes as one number and divide it by 10,000. The fractional part represents
the cents (since that’s four digits, it’s accurate down to 1/100 of a cent), and the whole
468 bP Advanced BASIC
number represents the dollar amount. For example, if you had one dollar, that would be
stored as 10,000, which is &H2710, so it would be stored like this:
Let’s finish up by covering the last of the standard data types, STRINGs.
LEN(String) ©SADD(String)
[Length] [Address]
Figure 11-9
Where LEN(String) is the string’s length, stored in two bytes, and SADD(String) is the
address of the string data, also stored in bytes (i.e., an offset address). What BASIC will
pass to us is the address of this descriptor, not the address of the actual string data. To
find the string data, we must read the string’s offset address from the descriptor (as we'll
do later).
In extended QuickBASIC, QBX.EXE, we'll have to find the string’s length and address
in a different fashion. QBX.EXE has two internal routines that it uses itself to find a
string’s address and length, named StringAddress( ) and StringLength( ). When BASIC
passes us a string descriptor, we'll pass it to these two internal routines to find the string’s
actual length and address.
In addition, when we pass a string back to a program under QBX, we'll have to put
together a string descriptor that it will recognize, and we’ll use QBX’s own internal
subroutine named StringAssign( ) to do that.
Array descriptors are considerably more complex than string descriptors. However, it
is still useful to know how BASIC stores the elements of an array. For example, even if we
know the address of Array(1, 1), where is Array(5, 1)? If we fill an array like this (from
Chapter 7):
> BASIC/Assembly Language Interface 469
Array(1, 1) = 10.00 e
Array(2, 1) = 53.00
AcrayCS, 1) = 7.17
Array(4, 1) = 9.67
Array(5, 1) = 87.99
Array(6, 1) = 14.00
Array(7, 1) = 91.19
Array(8, 1) = 12.73
Array(9, 1) = 1.03
Array(10, 1) = 5.04
Array(1, 2) = 9.67
Apceayt2, ¢€) = 3.5
Array(3, 2) = 8.97
Array(4, 2) = 10.00
Array(5, 2) = 78.33
Array(6, 2) = 17.00
Array(7, 2) = 91.36
Array(8, 2) = 12.73
Array(9, 2) = 16.12
Array(10, 2) = 7.98
Col 1 Col 2
Figure 11-10
Internally, BASIC stores arrays in column major order as its default, which means that
the numbers in column | are all stored before the numbers in column 2, and so on. You'll
find the above numbers in ascending order in memory, like this:
9.67 « Row
3.5 Row ht
IB; Using column major order, you can work up an array to find the entry you want.
For example, for an integer array, each entry is a word long. If you know where
the array begins in memory, you can now find any element you want directly,
instead of relying on BASIC.
= —— ee ee ee ee eee
integer value of 5. We begin by declaring the standard memory model for BASIC.
When you write a program, it’s not necessary to restrict yourself to one segment. In
fact, it’s not necessary to restrict either the data or the code to a single segment either.
However, if you wish to use multiple segments for data or code, labels need to be stored
by the assembler as two words; that is, as segment:offset, not just as an offset address.
Microsoft has defined various memory models corresponding to the possible sizes of the
code and data sections of a program, and Microsoft BASIC uses the MEDIUM model,
which means that there can be more than 64K of code, but less than 64K of data.
We'll always use the default memory model here, the MEDIUM model. All we have to
do is to specify that model to the assembler with a MODEL directive, and then we can
forget it. In the same line, we inform the assembler that the BASIC calling convention will
be used by the program that calls ours (i.e., parameters are pushed in order of
appearance):
Now we must declare the procedure Return5 as PUBLIC. This means that the assembler
will put the name Return into the .OBJ file’s header, where LINK.EXE can find it
(without the PUBLIC statement, we could not link Return5 into BASIC). We also start
defining the procedure:
PUBLIC Return5 e
Return5S PROC :
RET
Return5 ENDP
END
472 }& Advanced BASIC
Finally, we’re ready to write the procedure itself. We'll be using several of the registers,
and it’s good programming practice to preserve the original contents of those registers
and restore them later in case BASIC was relying on them. We can do that by pushing the
original contents of the registers we'll use onto the stack (and we'll pop them back when
we leave):
PUBL IC Return5
Return5 PROC
~ PUSH BX ;Should save all registers
PUSH cx
PUSH ES
PUSH DS
RET
Return5 ENDP
END
PUBL Ic Return5
Return5 PROC
PUSH BX sShould save ail registers
PUSH cx
PUSH ES
PUSH DS
RET
Return5 ENDP
END
> BASIC/Assembly Language Interface 473
To BASIC, Return5%(_) looks just like a normal BASIC function. We just declare it and
use it this way:
And that’s all there is to it: you can link Return5%( ) into this BASIC program and run
it.
PUBLIC Add5
Add5 PROC _-FAR USES DI SI DS ES, VALUE1:WORD <
Now we get the address of the integer that’s been passed to us into BX:
PUBLIC Add5
Add5 PROC FAR USES DI SI DS ES, VALUE1:WORD
> MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
BASIC passes the address of the parameter, not the actual parameter itself. We use that
address to get the actual parameter, and add 5 to it this way:
474 } Advanced BASIC
PUBLIC Add5
Add5 PROC FAR USES DI SI DS ES, VALUE1:WORD
MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
ay MOV AX, CBX]
ADD AX, 5
RET
AddS ENDP
END
Since we have to return the value in AX, we're all set. Listing 11-3 shows the whole
thing:
PUBLIC Add5
Add5 PROC FAR USES DI SI DS ES, VALUE1:WORD
MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
MOV AX, [CBX]
ADD AX, 5
RET
Add5 ENDP
END
Here’s how to use Add5%( ), which is, to BASIC, just another function:
PRINT “9 + 5 =";Add5%(9%)
PUBLIC Add5Long
Add5Long PROC FAR USES DI SI DS ES, LONG VALUE: WORD
> MOV BX, LONG VALUE ;Pick Addr of LONG VALUE from stack (BASIC passes
MOV AX, CBX] ¢ by reference)
MOV DX, CBX+2]
Since a LONG is two words, we read the value at [BX] and then the next word
(that is,
2 bytes later) as well: [BX+2]. We load those values into DX:AX. Now we have to add 5 to
this LONG integer, and we can do that like this:
ADD AX, 5
ADC DX, O
Here we're using the ADD instruction to add 5 to the value in AX, but this might
produce a carry, which we have to add to the high word in DX. We do that with the
assembly language instruction ADC, or add with carry. We just use ADC DX, 0, which
adds 0 to DX, along with any possible carry from the previous instruction. Breaking
additions up with ADD and ADC let us generate results of high accuracy. Here’s how we
put those two instructions to work:
PUBLIC Add5Long
AddS5Long PROC FARUSES DI SI DS ES, LONG VALUE: WORD
MOV BX, LONG _VALUE ;Pick Addr of LONG VALUE from stack (BASIC passes
MOV AX, CBX] 3; by reference)
é MOV DX, CBX+2]
= ADD AX, 5 gAdd 5 to DX:AX
ADC DX, O
RET
And that’s it: we’re supposed to return a LONG in DX:AX, and those registers are
already loaded. Listing 11-4 shows the whole function.
476 } Advanced BASIC
PUBLIC Add5Long
Add5lLong PROC FAR USES DI SI DS ES, LONG VALUE: WORD
MOV BX, LONG_VALUE ;Pick Addr of LONG VALUE from stack (BASIC passes
MOV AX, CBX] 3; by reference)
MOV DX, [CBX+2]
ADD AX, 5 zAdd 5 to DX:AX
ADC DX, 0
RET
Add5Long ENDP
END
LEN(String) |SADD(String)
[Length] [Address]
Figure 11-11
To return a string, we have to place the descriptor we make at the offset address BASIC
specifies (as well as returning that address itself in AX).
When we receive control, BASIC has already passed the return value’s offset address at
[BP+6]. We load that address into both BX (so we can use BX as an indirect index) and
AX (since we have to return that address in AX to BASIC anyway):
> BASIC/Assembly Language Interface 477
PUBLIC Getaaa
GetaaQ@ PROC FAR USES BX
Now we have to set up our string descriptor at that address. The first word will be 3,
the length of the “QQQ” string:
PUBLIC Get@a@
GetQ@QQ@ PROC FAR USES BX
The expression WORD PTR deserves note here. Those special keywords are necessary
for a situation that arises only in assembly language: the case when we exchange data
between two operands, and neither of them have a defined size.
In BASIC, all variables have an assigned type, like INTEGER or LONG, so this problem
never arises. Even in assembly language, there is no problem most of the time, for
example, if we had this instruction:
MOV AX, 3
then there is no problem. The assembler knows that, since AX is one word long, we
intend to fill that whole word with the value 3. Similarly, if we have defined MY_NUM-
BER like this: MY_NUMBER DB 5, then we could change that number to 3 like this:
MOV MY_NUMBER, 3
There would be no problem because the assembler knows that the variable MY_NUMBER
stands for 1 byte. On the other hand, if we used this instruction:
MOV CBXJ, 3
478 }& Advanced BASIC
then the assembler, which can handle either byte- or word-sized values, is unsure what to
do: do we want to fill the word or the byte pointed to by [BX] with 3? In this case, the
assembler will give you an error because neither operand has a well-defined size. The
correct thing to do is specify the size with either BYTE PTR:
In which case, the byte at location [BX] is filled with 03H, or WORD PTR:
PUBLIC Getaa@
Get@aq PROC FAR USES BX
The next thing we have to do here is to store the string, “QQQ.” But as we just saw, we
can’t do that like this:
Because “Q” is just a numeric value, 51H, to the assembler, which sees this:
> BASIC/Assembly Language Interface 479
In this case, neither operand has a defined size (do we mean 51H or 0051H2), so we
have to store the string like this, and then we can return:
RET
Getaae ENDP
And that’s it: we’ve returned a string from a function in BASIC under BC.EXE or
QB.EXE. Listing 11-5 shows the whole listing.
PUBLIC Geta@a
GetaaQq PROC FAR USES BX
RET
Getaaga ENDP
END
480 } Advanced BASIC
Here’s how to use GetQQQ3$( ), just like any other BASIC function that returns a
string:
PRINT Getaaas
In this case, all these values are passed by value, not reference. Here, SourceStrAddr is
the address of the string we want a descriptor for; SourceStrLen% is the length of that
string; DescAddr is the address we want the descriptor to be placed at; and DestStrLen%
will be set to 0 (indicating that we are creating a BASIC string).
To begin, we set up our data in the data segment (since the final executable code will
be an .EXE file, we can use both code and data segments here). When the program is
linked together, our data will automatically go into the data segment of the whole .EXE
file.
This is how we'll store data in code that we link into BASIC — in the data segment.
We'll need to store the string itself in memory, as well as provide space for a two word
string descriptor, so let’s set up a data segment with the directive .DATA:
We've defined the return string Ret_Str as “QQQ,” and two words (using the DW
directive), Ret_Desc_1 and Ret_Desc_2, to hold the descriptor that StringAssign( ) will
> BASIC/Assembly Language Interface 481
give us. Now we can start the code segment with .CODE. As with BC.EXE or QB.EXE, we
save the return value’s offset as passed to us:
Ret_Desc_1 DW
Ret_Desc_2 DW
- CODE
PUBLIC Getaaa
Getaag PROC FAR USES BX
Then we have to call StringAssign( ) to get a descriptor for “QQQ” — i.e., Ret_str —
so that we can pass that descriptor back to the calling program as the return value of our
function. Here’s what the call to StringAssign( ) looks like:
To call StringAssign( ), we'll have to place parameters on the stack just as if we were a
BASIC program. The first parameter to pass to StringAssign( ) is the address of “QQQ,”
the string we want to return. We have to push the segment address (in DS), followed by
its offset (OFFSET Ret_Str). Then we have to pass the length of our string, which is 3:
PUBLIC GetQQ@Q
Getaag PROC FAR USES BX
Next we have to pass the address we want BASIC to write the string descriptor to.
We've set aside two words named Ret_Desc_] and Ret_Desc_2 for that purpose. We pass
we want
the address of the first of these words, and then a word equal to 0 to indicate that
to make this string a BASIC string. Now we’re ready to call StringAssign( ):
Note that we also used the statement EXTRN StringAssign: PROC. This is much like a
DECLARE statement in BASIC; EXTRN tells the assembler that we’re going to link ina
procedure named StringAssign( ) later, but we don’t have the address for it right now. In
this way, the assembler leaves space for that address in the assembled code, and the linker
will fill it in. And that’s it.
When we return from StringAssign( ), the two words of the descriptor will be in
Ret_Desc_1 and Ret_Desc_2. This is the descriptor we have to pass to BASIC as the
return value of our function.
QBX has already given us the return value’s offset, so we place the descriptor there and
then we’re done. Listing 11-6 shows the whole program (QBX version).
> BASIC/Assembly Language Interface 483
PRINT GetQags
PUBLIC Getaae
GetQ@aQ@ PROC FAR USES BX
RET
Getaae ENDP
END
It’s a little extra work, but if you're using QBX.EXE, this is the way you have to do it.
PUBLIC Addem
Addem PROC FAR USES DI SI DS ES, VALUE1:WORD, VALUE2: WORD =
Now VALUE1 and VALUE2 hold the addresses of the two integers. We can load and
add them like this:
PUBLIC Addem
Addem PROC FAR USES DI SI DS ES, VALUE1:WORD, VALUE2: WORD
~ MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
MOV AX, CBX]
MOV BX, VALUE2
ADD AX, [CBX]
RET
Addem ENDP
The result is returned in AX, and that’s it. Listing 11-7 shows the whole thing.
PUBLIC Addem
Addem PROC FAR USES DI SI DS ES, VALUE1:WORD, VALUE2:WORD
MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
MOV AX, CBX]
MOV BX, VALUE2
ADD AX, CBX]
RET
Addem ENDP
END
PRINT “9 + 5 ="7Addem%(5,9)
> BASIC/Assembly Language Interface 485
PUBLIC Addem
-DATA e
VAL1 DW 0
VAL2 Dw O
Now we can refer to VALI and VAL2 as normal assembly language variables in our
code:
PUBLIC Addem
-~DATA
VAL1 DW O
VAL2 DW O
- CODE
Addem PROC FAR USES DI SI DS ES, VALUE1: WORD, VALUE2:WORD
RET
Addem ENDP
END
486 > Advanced BASIC
And that’s all there is to it. We can store and retrieve data in the data segment this way.
Listing 11-8 shows the entire listing.
PUBLIC Addem
«DATA \
VAL1 DW O
VAL2 DW O
- CODE
Addem PROC FAR USES DI SI DS ES, VALUE1:WORD, VALUE2:WORD
MOV BX, VALUE1 ;Pick addr of VALUE from stack (BASIC passes ref.)
MOV CX, EBX]
MOV VAL1, CX
MOV BX, VALUE2
MOV CX, EBX]
MOV VAL2, CX
RET
Addem ENDP
END
> MOV BX, The Char ;Pick addr of The_Char from stack (BASIC passes ref.)
: MOV DX, EBX] gload into DL
MOV AH, 2
INT 21H
> BASIC/Assembly Language Interface 487
PUBLIC PrintChar
PrintChar PROC FAR USES DI SI DS ES, The Char:WORD
MOV BX, The Char ;Pick addr of The_Char from stack (BASIC passes ref.)
MOV DX, CBX] gload into DL
MOV AH, 2
INT 21H
RET
PrintChar ENDP
END
CALL PrintChar(ASC(“a"))
In the same way as with functions, we can receive two parameters. Here's a quick
example of a subprogram named PrintSum%( ), which accepts two integers and prints
the result out (as long as that result is a single digit, 0-9):
PUBLIC PrintSum
PROC FAR USES DI SI DS ES, VALUE1:WORD, VALUE2: WORD
PrintSum
RET
PrintSum ENDP
END
488 Pb Advanced BASIC
We just read two parameters, which are both integers, convert the result (which must be
in the range 0 - 9) to an ASCII character, and print it out with interrupt GH21 service pe
String handling is one of the most common uses for assembly language. The
80 x 86 microprocessors have a number of built-in string instructions with
names like MOVS, CMPS, and SCAS. They are much faster than the compar-
able instructions in BASIC.
Again, since QBX.EXE handles string descriptors differently, we'll have to split the
example up into two versions, one for BC.EXE and QB.EXE and one for QBX.EXE.
MOV AH, 2
MOV BX, The String ;Addr of str_len:word, str_data_addr:word
MOV CX, CBX] 7String Length
MOV BX, CBX+2] ;String data addr
°
Now we only need to loop over the string data, loading each byte into DL and printing
it out with service 2 like this:
MOV AH, 2
MOV BX, The_String ;Addr of str_len:word, str_data_addr:word
MOV CX, CBX] sString Length
MOV BX, [CBX+2] ;String data addr
Print_Loop:
> MOV DX, CBX]
~ INT 21H
INC BX
LOOP Print_Loop
RET
PrintString ENDP
END
That’s it; we’re done. Listing 11-10 shows the whole listing.
PUBLIC PrintString
PrintString PROC FAR USES DI SI DS ES, The_String:WORD
MOV AH, 2
MOV BX, The_String ;Addr of str_len:word, str_data_addr:word
MOV CX, CBX] 7String Length
MOV BX, CBX+2] ;String data addr
Print_Loop:
MOV DX, CBX]
INT 21H
INC BX
LOOP Print_Loop
RET
PrintString ENDP
END
490 & Advanced BASIC
The length of the string is returned in AX. Like the version of PrintString( ) we devel-
oped before, we'll move that value into CX so we can use the LOOP instruction to print
the string out.
Next, we have to pass the address of the descriptor to StringAddress( ). This function
returns the two-word address of the string data. Since that return value is two words long,
DX will hold the high word (segment address), and AX will hold the low word (offset
address):
PUBLIC PrintString
CODE
PrintString PROC FAR USES DI SI DS ES, The String: WORD
PUSH The String
EXTRN StringLength: PROC
> BASIC/Assembly Language Interface 491
CALL StringLength
MOV CX, AX sload string length into CX for LOOP
Now we want to read the string, character by character. To do that, we must first make
the segment address returned by StringAddress( ) into our data segment by loading it
into DS. Then we can load BX with the string’s offset address and read characters like this:
MOV DL, [BX]. (As we saw in Chapter 10, in the [BX] method of addressing, BX holds
the offset from the beginning of the data segment.)
The string’s segment address is in DX right now, and its offset is in AX, so we first save
the original data segment address and we can load DS and BX like this:
PUBLIC PrintString
-CODE
PrintString PROC FAR USES DI SI DS ES, The String: WORD
PUSH The_String
EXTRN Stringlength: PROC
CALL Stringlength
MOV CX, AX jload string length into CX for LOOP
PUSH The_String
EXTRN StringAddress: PROC
CALL StringAddress
~ PUSH DS jSave DS
MOV DS, DX jos € String'’s segment
MOV BX, AX 7 BX String's offset
Now we're free to use the same printing loop as we’ve developed in the earlier version
of PrintString:
PUBLIC PrintString
. CODE
PrintString PROC FAR USES DI SI DS ES, The_String: WORD
PUSH The String
EXTRN StringLength: PROC
CALL StringLength
492 }& Advanced BASIC
PUSH The_String
EXTRN StringAddress: PROC
CALL StringAddress
PUSH DS ;Save DS
MOV DS, DX yds € String's segment
MOV BX, AX 7 BX String's offset
MOV AH, 2
Print Loop:
MOV DL, CBX] et
INT 21H
INC BX
LOOP Print_Loop
At the end, we have to restore the original value of DS, and then we can return. Listing
11-11 shows the complete version of PrintString set up for QBX.EXE.
PUBLIC PrintString
- CODE
PrintString PROC FAR USES DI SI DS ES, The_String:WORD
PUSH The String
EXTRN StringLength: PROC
CALL StringLength
MOV CX, AX ;load string tength into CX for LOOP
MOV AH, 2
Print_Loop:
MOV DL, CBX]
INT 21H
INC BX
LOOP Print_Loop
POP DS yRestore DS
RET
PrintString ENDP :
END
> BASIC/Assembly Language Interface 493
And that’s it. Again, it’s a little more work to use string under QBX.EXE, but if you use
that compiler, it’s your only option.
We have to pass PrintFirstElement( ) the address of the first element of an array we can
call A( ). To do that, we don’t want to just pass VARPTR(A(1)). That would pass the
address of the address of A(1), since arguments are passed by reference. To avoid that,
we'll use BYVAL in the declaration of PrintFirstElement( ) so that our program passes
parameters by value and not by reference. Then we're ready to call PrintFirstElelement( )
with an argument of VARPTR(A(1)):
AC1)
AC2)
AC3)
AC4)
AC5)
AC6) nunhun
wt =
AuUFWN
And that’s it. Listing 11-12 shows the whole procedure (BC.EXE version).
PUBLIC PrintFirstElement
PrintFirstElement PROC FAR USES DI SI DS ES, The_Array_Addr: WORD
RET
PrintFirstElement ENDP
END
AC1)
AC2)
AC3)
AC4)
AC5)
AC6) ouunun
wn
Aur
Then, in the procedure PrintFirstElement, we place the segment address into DS (after
saving the original value on the stack), the offset address into BX, and read the first
element of the array like this: MOV DX, [BX]. We can add ASCII “0” to this value to
convert it to an ASCII digit (assuming its value was 0 — 9), and print it out. Listing 11-13
shows the QB.EXE and QBX.EXE version.
PUBLIC PrintFirstElement
PrintFirstElement PROC FAR USES DI SI DS ES, Array_Seg:WORD,Array_Offset:
WORD
=> PUSH DS
MOV BX, Array_Offset
PUSH Array Seg
POP DS
MOV DX, CBX]
POP DS
ADD DX, "O"
MOV AH,2
INT 21H
RET
PrintFirstElement ENDP
END
That’s all there is to it, and that’s the end of our last assembly language example.
496 P Advanced BASIC
Conclusion
We've come far in this book, and we’ve ranged widely over the computer, from screen
control to menus, from the keyboard to the mouse, from our own paint program to our
own database program. We’ve become experts in interfacing BASIC to assembly language,
in sorting data, and in drawing windows. We've even toured through the operating
system and put it to use for us as well.
In fact, we’ve become experts in almost all areas of BASIC. We’ve seen how profes-
sional programs get things done and had the chance to peek behind the scenes. And that’s
it. The only thing that remains now is to put all this knowledge to use. Good luck — and
happy programming!
BIOS and DOS Reference
497
7.
“4
“ey 2 emcthegtt>
ay, Die
et
; =e Fear~ a
we 0st FPS e
ram _
A
ws tar 6 endl, Ve
Ss
| nokia
is
. a ae onl 7
a
aea c
7
< “i \arenn> .
a »
—
47
a.
> BIOS and DOS Reference 499
This appendix is intended for use as a reference. We will work through all the inter-
rupts that are available, from 0 to FFH, reviewing the ones that are useful.
Interrupt 0 Divide by 0
This is the first of the BIOS interrupts. BIOS uses interrupts 0 to 1FH, and DOS
continues from 20H upward. Interrupt 0 is the divide by zero routine. If a divide by
zero occurs, then this interrupt is called. It prints out its message, “Divide Over-
flow,” and usually stops program execution.
Interrupt 1 Single Step
No one, except a debugger, uses this interrupt. It is used to single step through
code, with a call to this interrupt between executed instructions.
Interrupt 2 Nonmaskable Interrupt (NMI)
This is a hardware interrupt. This interrupt cannot be blocked off by using STI and
CLI. It always gets executed when called.
Interrupt 3 Breakpoint
This is another debugger interrupt. DEBUG uses this interrupt with the Go com-
mand. If you want to execute all the code up to a particular address and then stop,
DEBUG will insert an INT 3 into the code at that point and then give control to the
program. When the INT 3 is reached, DEBUG can take control again.
Interrupt 4 Overflow
This is similar to INT 0. If there is an overflow condition, this interrupt is called.
Usually, though, no action is called for, and BIOS simply returns.
Interrupt 5 Print Screen
This interrupt was chosen by BIOS to print the screen out. If you use the PrtSc key
on the keyboard, this is the interrupt that gets called. Needless to say, your program
can also issue an INT 5 by just including that instruction in the program. There are
no arguments to be passed.
Interrupts 6 and 7 Reserved
This interrupt calls INT 1CH as well. If you want to intercept the timer and do
something 18.2 times a second, it is recommended you intercept INT 1CH instead
of this one.
Interrupt 9 Keyboard
This hardware interrupt may be intercepted by memory resident programs.
Interrupt OAH Reserved
Interrupts OBH-OFH
These interrupts point to the BIOS routine D_EOI, which is BIOS’ End of Interrupt
routine. All this routine does is to reset the interrupt handler at port 20H and
return.
INT 10H Service 0 Set Screen Mode
Input
AH=0
AL=Mode
Output
New Cursor
Output
Cursor position changed
Output
DH,DL=Row, Column of Cursor.
CH,CL=Cursor Mode currently Set.
502 & Advanced BASIC
Output
AH=0- Light pen switch not down.
AL=l1 DH,DL=Row, Column of Light Pen position.
CH Raster line (Vertical) 0-199
BX Pixel Column (Horizontal) 0-319,639
Output
Active Page Changed
Output
AL=Character read (ASCII).
AH=Attribute of character (Alphanumerics only).
INT 10H Service 9 Write Attribute and Character at Cursor Position
Input
BH=Page Number
BL—Alpha Modes=Attribute;
Graphics Modes=Color
CX=Count of characters to write
AL=IBM ASCII code
AH=9
Output
Character written on screen at Cursor Position
Output
Character written on screen at Cursor Position
|NOTE: If bit 7 of AL is 1, the color value is XORed with the current value of the dot.
Output
AL=Color Value (0-3)
[0,0] is upper left. If bit 7 of AL is 1, the color value is XORed with the current
value of the dot.
Output
AH=Number of alphanumeric columns on screen
AL=Current mode (See INT 10H Service 0)
BH=Active display page
> BIOS and DOS Reference 505
Output
BH = Register setting.
INT 10H Service 10H Function 8 — Read Overscan (Border) Register
Input
AM =slOrt
Alz=&
Output
BH = overscan setting.
INT 10H Service 10H Function 10H — Set DAC Register
Input
AH =" 10H
AL = 10H
BX = Register to set (0 - 255)
CH = Green Intensity
CL = Blue Intensity
DH = Red Intensity
INT 10H Service 10H Function 12H — Set DAC Registers
Input
AH = 10H
Al=12H
BX = First register to set (0 - 255)
CX = Number of registers to set (1 - 256)
ES:DX = Address of a table of color intensities. Three bytes are used
for each
DAC register (use only lower 6 bits of each byte). Table is set up:
red
green, blue, red, green, blue....
?
> BIOS and DOS Reference 507
INT 10H Service 10H Function 13H — Select Color Page Mode
Input
AH = 10H
AL = 13H
BL = 0 Select Color Paging Mode
BH = 0 Selects 4 DAC register pages of 64 registers each
BH = 1 Selects 16 DAC register pages of 16 registers each
BL = 1 Select Active Color Page
For use with 4 page mode:
BH = 0 Selects the first block of 64 DAC registers
BH = 1 Selects the second block of 64 DAC registers
BH = 2 Selects the third block of 64 DAC registers
BH = 3 Selects the fourth block of 64 DAC registers
For use with 16 page setting:
BH = 0 Selects the first block of 16 DAC registers
BH = 1 Selects the second block of 16 DAC registers
Output
No carry > AH=0, Success
Carry > AH=Error Code (see Service 1)
|NOTE: | DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
DL=Drive Number
DH=Head Number
CH=Cylinder or Track (Floppies) Number
CL=bits 7,6 high 2 bits of 10-bit cylinder number.
CL=Sector Number (bit 0-5)
AL=Number of Sectors to Read (Floppies 1-8; Hard disks 1-80H; Hard disks
read/write Long 1-79H)
ES:BX=Address of buffer for reads and writes
Output
No Carry AL = Number of sectors read (diskette)
Carry AH=Disk Error Code (see Service 1)
DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
DL=Drive Number
510 » Advanced BASIC
DH=Head Number
CH=Cylinder or track (floppies) number
CL=bits 7,6 high 2 bits of 10-bit cylinder number
CL=Sector Number (bits 0-5)
AL=Number of Sectors to Write (floppies 1-8; Hard disks 1-80H; hard disks
read/write Long 1-79H).
ES:BX=Address of buffer for reads and writes
Output
No carryAL = No. sectors written (diskette)
Carry AH=Disk Error Code (see Service 1)
DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
Output
No Carry—AH=0, Success
Carry AH=Disk error code (see Service 1)
NOTE: | DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
es rr ee ee eee
> BIOS and DOS Reference 511
Input
AH=8
DL=Drive number (0 based)
Output
DL=Number of drives attached to controller
DH=Maximum value for Head Number
CH=Maximum cylinder value
CL=bits 7,6 high 2 bits of 10-bit cylinder No.
CL=Maximum value for sector number (bits 0-5)
BL (For PS/2 diskettes only)
= 1 > 360K drive
2 — 1.2 MB drive
= 3 > 720K drive
= 4 > 1.44 MB drive
DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
Input
AH=0CH
DH=Head Number
DL=Drive number (80H-87H allowed)
CH=Cylinder Number
CL=Sector Number; bits 7,6 of CL = high 2 bits of 10-bit cylinder No.
Output
No Carry—AH=0, Success
Carry AH=disk Error Code (see Service 1)
512 P& Advanced BASIC
DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
Input
AH=11H (Read)
Output
No Carry—AH=0, Success
Carry AH=Disk Error Code (see Service 1)
|NOTE: DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
tc ee St NY ONG IA BONVIC’ Met ten
Input
AH=12H (RAM diagnostic)
AH=13H (Drive diagnostic)
AH=14H (Controller diagnostic)
DL=Drive Number (80H-87H allowed)
Output
No carry—AH=0, Success
Carry AH=Disk Error Code (see Service 1)
> BIOS and DOS Reference 513
|NOTE: | DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
Output
Carry = 1 — Error, AH = Error code
= 0 > Success
DL = Drive number; set bit 7 to 1 for hard disks. For hard disks, drive number in
DL can range from 80H to 87H.
Output
If Bit 7 of AH is set, failure
If Bit 7 is not set, bits 0-6 hold status (see INT 14H, AH=3)
INT 14H, AH=2 Receive Character from Serial Port
Input
AH=2
Output
AL=Character Received
AH=0, success
Otherwise, AH holds an error code (see INT 14H, AH=3)
Output
AH Bits Set:
7—Time Out
6—Shift Register Empty
5—Holding Register Empty
4— Break detected
3—Framing error
2—Parity error
1—Overrun error
0—Data Ready
AL Bits Set:
7>Received Line signal Detect
6—Ring Indicator
5—Data Set Ready
4—Clear to Send
3—>Delta Receive Line Signal Detect
2—Trailing Edge Ring Detector
1—>Delta Data Set Ready
O—Delta Clear to Send
> BIOS and DOS Reference 515
Output
DX=Number of bytes actually read.
Carry flag set if error.
If Carry, AH=01->CRC Error.
=02—Data transitions lost.
=04—No Data Found
In recent BIOS versions, new items have been added to this interrupt, such as
joystick support, the ability to switch processor mode (protected or not), mouse
support, and some BIOS parameters.
Output
AH=Scan Code AL=ASCII code
Output
Zero Flag=1 — Buffer Empty
Zero Flag=0 — AH=Scan Code
AL=ASCII Code
516 P& Advanced BASIC
Output
AL=Keyboard Status byte.
INT 17H Service 0 Print character in AL
Input
AH=0
AL=Character to be printed.
DX=Printer Number (0,1,2)
Output
AH=1 — Printer Time Out.
Output
AH=Printer Status:
Bits Set of AH:
7—Printer Not Busy.
6—Acknowledge.
5— Out of Paper.
4—Selected.
31//O Error.
2—Unused.
1—Also Unused.
O—Time Out.
Output
AH Set to Status Byte as in INT 17H, AH=1.
INT 18H Resident BASIC
This interrupt starts up ROM resident BASIC in the PC.
INT 19H Bootstrap
This interrupt is the one that boots the machine (try it with DEBUG).
INT 1AH Service 0 Read Time of Day
Input
AH=0
Output
CX=High Word of Timer Count.
DX=Low Word of Timer Count.
AL=0 If Timer has not passed 24 hours since last read.
DOS Interrupts
Interrupt 1FH is the last BIOS Interrupt, and DOS starts with INT 20H.
INT 20H Terminate
Programs are usually ended with an INT 20H.
Interrupt 21H
Interrupt 21H is the DOS service interrupt. To call one of these services, load AH
with the service number, and the other registers as shown.
INT 21H Service 0 Program Terminate
Input
AH -= 0
INT 21H Service 1 Keyboard Input
Input
(Nabe)
Output
AL = ASCII code of struck key does echo on screen
Output
Character in AL
Output
AL = ASCII code of struck key NO Echo on screen.
Output
AL = ASCII code of struck key. Does NOT echo the typed key.
Output
Buffer at DS:DX filled
Echo the typed keys
Output
AL = FF — Character ready
AL = 00 — Nothing to read in
INT 21H Service OCH Clear Keyboard Buffer and Invoke Service
Input
A= 0CH
AL = Keyboard Function #
Output
Standard Output from the selected Service
Output
AL=0 -— Success
AL=FF —> Failure
Output
AL=0 — Success
AL=FF — Failure
Output
AL=FF — Failure
AL=0 — Success. DTA holds FCB for match
Output
AL=FF —> Failure
AL=0 — Success. DTA holds FCB for match
Output
AL=FF —> Failure
AL=0 — Success
Output
Requested Record put in DTA
AL=0 Success
1 End of File, no data in record.
2 DTA Segment too small for record.
3 End of File; record padded with 0
Output
One record read from DTA and written.
AL=0 Success.
1 Disk full.
2 DTA Segment too small for record.
Output
AL=0 success
=FF directory full
INT 21H Service 17H Rename File
Input
DS:DX points to a MODIFIED FCB.
AH=17H
Output
AL=0 success
=FF failure
|NOTE: Modified FCB — Second file name starts 6 bytes after the end of the first file
name, at DS:DX+11H.
NNN
524 }& Advanced BASIC
Output
AL=Current Disk (0=A, 1=B, and so on).
DTA= Disk Transfer Address, the data area used with FCB services. Default
DTA is 128 bytes long, starting at CS:0080 in the PSP.
Output
DS:BX points to the “FAT Byte”
DxX=Number of Clusters
AL=Number of Sectors/cluster
CX=Size of a Sector (512 bytes)
|NOTE: | Files are stored in clusters, the smallest allocatable unit on a disk.
—————
Output
DS:BX points to the “FAT Byte”
DX=Number of Clusters
AL=Number of Sectors/Cluster
CX=Size of a Sector (512)
|NOTE: | Files are stored in clusters, the smallest allocatable unit on a disk.
AH=21H
Output
AL=00 success.
=01 end of file, no more data
=02 not enough space in DTA segment
=03 end of file, partial record padded with Os
INT 21H Service 22H Random Write
Input
DS:DX points to an opened FCB.
Set FCB’s Random Record field
at DS:DX+33 and DS:DX+35
AH=21H
Output
AL=00 success.
=01 disk is full.
=02 not enough space in DTA segment.
526 > Advanced BASIC
Output
AL=00 Success.
=FF No file found that matched FCB. Random Record Field set to file length
in records, rounded up.
INT 21H Service 24H Set Random Record Field
Input
DS:DX points to an opened FCB
AH=24H
Output
Random Record Field set to match Current Record and Current Block.
AH=27H
> BIOS and DOS Reference 527
Output
AL =00 Success.
=01 end of file, no more data
=02 not enough space in DTA segment
=03 end of file, partial record padded with Os
CX =Number of records read
Random Record Fields set to access next record
|NOTE: | The data buffer used in FCB services is the DTA, or disk transfer area.
Output
AL=00 success
=01 disk is full
=02 not enough space in DTA segment
CX=0 = file to set to the size indicated by the Random Record field. The
data buffer used iin FCB services is the DTA, or disk transfer area.
nmeEEEEEEEEEESSE SaaS TanTEDnnnEnUnISEEIETnEI NEnnn
Output
DS:SI = lst character after filename.
ES:DI = Valid FCB
|NOTE: | If the command line does not contain a valid filename, ES:[DI+1] will be a
blank.
Output
CX = Year - 1980
DH = Month (1=January, etc.)
DL = Day of the month
INT 21H Service 2BH Set Date
Input
CX = Year - 1980
DH = Month (1=January, etc.)
DL = Day of the month.
AH=2BH
Output
AL = O Success
AL = FF Date not valid
aaa
> BIOS and DOS Reference 529
an ee ee eee
Output
CH =. Hours (0-23)
CL = Minutes (0-59)
DH = Seconds (0-59)
DL = Hundredths of seconds (0-99)
INT 21H Service 2DH Set Time
Input
AH=2DH
CH = Hours (0-23)
CL = Minutes (0-59)
DH = Seconds (0-59)
DL = Hundreds of seconds (0-99)
Output
AL = O Success
AL = FF Time is Invalid
Output
ES:BX = Current DTA address
|NOTE: | The data buffer used in FCB services is the DTA, or Disk Transfer Area.
530 Pb Advanced BASIC
Output
AL=Major Version Number (3 in DOS 3.10)
AH=Minor Version Number (10 in DOS 3.10)
BX=0
Cx=0
|NOTE: | lf AL returns 0, you are working with a version of DOS before 2.0.
|NOTE: | Exit code can be read by a parent program with Service 4DH. It can also be
tested by ERRORLEVEL commands in batch files.
Output
DL=0 > Off
DL=1 > On
INT 21H Service 34H Internal to DOS
> BIOS and DOS Reference 531
Output
ES:BX = Interrupt’s Vector
Output
AX=OFFFH—Drive Number invalid
AX=Number of Sectors/cluster
BX=Number of available clusters
CX=Size of a Sector (512)
DX=Number of Clusters
|NOTE: | Files are stored in clusters, the smallest allocatable unit on a disk.
1 Byte set to 0.
1 Byte decimal separator (ASCII)
1 Byte set to 0.
24 Bytes used internally.
Output
Filled in 32-byte block (see below)
Output
No Carry— success
Carry— AH has error value
AH=3 path not found
AH=5 access denied
INT 21H Service 3AH Delete a Subdirectory
Input
AH=3AH
DS:DX point to ASCIIZ string with directory name.
ai
Output
No Carry Success
> BIOS and DOS Reference 533
Output
No Carry—> Success
Carry AH has error value.
AH=3 path not found.
INT 21H Service 3CH Create a File
Input
DS:DX points to ASCIIZ filename.
CX=Attribute of File.
AH=3CH
Output
No carry > AX=File Handle
Carry > AL=3 Path not found
AL=4 Too many files open
AL=5 Directory full, or previous
Read Only file exists
INT 21H Service 3DH Open a File
Input
DS:DX points to ASCIIZ filename
AL=Access code. reall
AH=3DH
Output
No Carry — AX=File Handle
Carry — AL=Error Code
(Check Error Table)
INT 21H Service 3EH Close a File Handle
Input
BX holds a valid File Handle
AH=3EH
Output
Carry + AL=6 = Invalid handle
INT 21H Service 3FH Read from File or Device
Input
DS:DX = Data Buffer Address
CX=Number of bytes to read
BX=File Handle
AH=3FH
Output
No Carry AX=Number of bytes read
Carry § AL=5 Access Denied
AL=6 Invalid Handle
> BIOS and DOS Reference 535
Output
No Carry— AX=Number of bytes written
Carry AL=5 Access Denied
AL=6 Invalid Handle
|NOTE: | Full disk is NOT considered an error: check the number of bytes you wanted to
write (CX) against the number actually written (returned in AX). If they do not
match, the disk is probably full.
Output
No Carry— Success
Carry AL=2 File Not Found
AL=5 Access Denied
Output
No Carry DX:AX=New Location of Pointer.
Carry AL=!1 Illegal Function Number.
AL=6 Invalid Handle.
INT 21H Service 43H Change File’s Attribute
Input
DS:DX = ASCIIZ Filestring
AL=1-— File attribute changed; CX holds new attribute
AL=0-— File’s current attribute returned in CX
AH=43H
Output
No carry success.
Carry AL=2 File Not Found.
AL=3 Path Not Found.
AL=5 Access Denied.
If AL was 0, CX returns the attribute.
INT 21H Service 44H I/O Control
Output
No Carry AX=New, duplicated Handle
Carry> AL=4 Too many files open
AL=6 Invalid Handle
INT 21H Service 46H Force Duplication of a File Handl
e
Input
BX=File Handle to duplicate
> BIOS and DOS Reference 537
Output
No carry— Handles refer to same “stream”
Carry AL=6 Invalid handle
INT 21H Service 47H Get Current Directory on Specified Drive
Input
AH=47H
DS:SI point to 64 byte buffer.
DL=Drive Number.
Output
No carry— Success, ASCIIZ at DS:SI
Carry AH=15 Invalid Drive Specified.
BX=Number of paragraphs
Output
No carryAX:0000 memory block address
Carry AL=7 memory control blocks destroyed
AL=8 Insufficient memory, BX contains maximum allowable request
INT 21H Service 49H Free Allocated Memory
Input
AH=49H
ES=Segment of block being freed
538 b& Advanced BASIC
Output
No carry > SUCCESS
Carry > AL=7 memory control blocks destroyed
AL=9 incorrect memory block address
INT 21H Service 4AH SETBLOCK
Input
AH=4AH
ES=Segment of block to modify
BX = Requested size in paragraphs
Output
No carry — success.
Carry > AL = 7 memory control blocks destroyed
AL = 8 insufficient memory; BX holds maximum possible request
AL = 9 invalid memory block address
INT 21H Service 4BH Load or Execute a Program — EXEC
Input
AH= 4BH
DS:DX=ASCIIZ string with drive, pathname, filename.
ES:BX= Parameter Block Address (See Below)
AL= 0 - Load and execute the program
3 — Load but create no PSP, don’t run (Overlay)
See Below
Output
No Carry > Success
Carry > AL=1 Invalid Function Number
AL=2 File Not Found on Disk
AL=5 Access Denied
AL=8 Insufficient Memory for requested operation
AL=10 Invalid Environment
AL=11 Invalid Format
INT 21H Service 4CH Exit
Input
AH=4CH
AL=Binary Return Code.
Output
AL=Binary Return Code from Subprocess
AH=0 If subprocess ended normally
1 If subprocess ended with a ~ Break
2 If it ended with a critical device error
3 If it ended with Service 31H
INT 21H Service 4EH Find First Matching File
Input
DS:DX—ASCIIZ filestring.
540 » Advanced BASIC
lhe
CXzattribute to match.
AH=4EH
Output
Carry AL=2 No Match Found
AL=18 No More Files
No carryDTA filled as follows:
21 bytes reserved.
1 byte found attribute.
2 bytes file’s time.
2 bytes file’s date.
2 bytes low word of size.
2 bytes high word of size.
13 bytes name and extension of found file in ASCIIZ form (NO
pathname).
|NOTE: | The data buffer used in FCB services is the DTA, or disk transfer area. See
earlier services.
Output
Carry—AL=18 no more files
No carry->DTA filled as follows:
21 bytes reserved.
1 byte found attribute.
2 bytes file’s time.
2 bytes file’s date.
2 bytes low word of size.
2 bytes high word of size.
13 bytes name and extension of found file in ASCIIZ form (NO
pathname).
ae a > BIOS
ee ha and DOSlaa
Reference 541
il
|NOTE: | The data buffer used in FCB services is the DTA, or Disk Transfer Area.
See
earlier services.
Output
AL=0— verify is OFF
1— verify is ON
INT 21H Service 55H Internal to DOS
Output
No Carry success.
Carry AL=3 Path Not Found.
AL=5 Access Denied.
AL=17 Not Same Device.
INT 21H Service 57H Get or Set a File’s Date & Time
Input
BX=File handle.
AL=0—Get Date & Time
Output
No Carry:
CX returns Time
DX returns Date
Output
AX = extended error
BH = error class
BL = suggested action
CH = locus
Output
AX = Error if Carry is set
DS:DX = ASCIIZ path and filename
INT 21H Service 5BH Create a New File DOS 3+
Input
AH = 5BH
DS:DX = Address of an ASCIIZ path (ending with “\”)
CX = File’s attribute
Output
AX = Error if Carry is set
= Handle if Carry is not set
INT 21H Service 5CH Lock and Unlock Access to a File DOS 3+
Input
BEL = OC
AL = 0 = lock byte range
1 > Unlock byte range
BX = File handle
CX = byte range start (high word)
DX = byte range start (low word)
SI = No. bytes to (un)lock (high word)
DI = No. bytes to (un)lock (low word)
Output
If Carry = 1, AX = Error
Output
DS:DX = ASCIIZ computer name
CH = 0 > Name not defined
CL = NETBIOS number
AX = Error if carry set
544 P& Advanced BASIC
Output
AX = Error if carry is set
INT 21H Service 5E03 Get Printer Setup DOS 3+
Input
AX = 5E03H
BX = Redirection list index
ES:DI = Pointer to printer setup buffer
Output
AX = Error if Carry is set
CX = Length of data returned
ES:DI = Filled with printer
setup string
INT 21H Service 5F03 Redirect Device DOS 3+
Input
AX = 5F03H
BL = Device type
= 3 —> Printer Device
= 4 > File Device
CX = Value to save for caller
DS:SI = Source ASCIIZ device name
ES:DI = Destination ASCIIZ network path with password
Output
AX = Error if Carry is set
> BIOS and DOS Reference 545
Output
AX = Error if Carry is set
INT 21H Service 62H Get Program Segment Prefix DOS 3+
Input
AX = 62H
Output
BX = Segment of currently
executing program.
Output
AX = Error if Carry is set
INT 21H Service 68H Commit File (Write Buffers) DOS 3.30
Input
AX = 68H
Output
BX = File Handle
|NOTE: | 68H is the last of the DOS 3.3 INT 21H services.
If you just execute an IRET, DOS will take an action based on the contents of AL.
If AL=0, the error will be ignored. If AL=1, the operation will be retried. If AL=2, the
program will be terminated through INT 23H.
INT 25H Absolute Disk Read
Input
AL=Drive Number
CX=Number of Sectors to Read.
DX=First logical sector.
DS:BX=Buffer address.
Output
No Carry— Success
Carry AH=80H Disk didn’t respond.
AH=40H Seek Failed.
AH=20H Controller Failure
AH=10H Bad CRC error check.
AH=08 DMA overrun.
AH=04 Sector not found.
AH=03 Write protect error.
en
el 2 > BIOS and DOS Reference
thet 547
daha chal
|NOTE: | Flags left on stack after this INT call because information is returned in current
flags. After you check the flags that were returned, make sure you do a POPF.
Also, this INT destroys the contents of ALL registers.
—$——————————————
e — e ee es
Output
No Carry Success
Carry—AH=80H Disk didn’t respond
AH=40H Seek failed
AH=20H Controller failure
AH=10H Bad CRC error check
AH=08 DMA overrun
AH=04 Sector not found
AH=03 Write protect error
AH=02 Address Mark missing
AH=00 Error unknown
Flags left on stack after this INT call because information is returned in current
flags. After you check the flags that were returned, make sure you do a POPF.
Also, this INT destroys the contents of ALL registers.
Commit File DOS 3.30 (interrupt 21H current, 276 dynamic arrays, 231
service 68H), 399, 545 deleting, 285-288
COMMON statements, 41-42, 129 seeking, 280-285 E
computers, determining installed equipment, updating, 288-290 EGA monitor, 250-251
371-373 table EGA Monitor (interrupt 10H service 12),
conditional jumps, 431-433 name, 267-270 251
Console I/O without Echo (interrupt 21H number, 269-270 END assembler directive, 419-420
service 6), 16-18, 377, 519 DB (define byte) assembler directive, 421- ENDP assembler directive, 449-450
Console Input Without Echo (interrupt 21H 425 entry point into program, 419-420
service 7), 377, 412, 519 DB program, 266-290 EOF statement, 281
Console Input w/o Echo with C Check Debug menu, 348-349 Equipment Determination (interrupt 11H),
(interrupt 21H service 8), 377, 519 Debug option, 346 371, 507-508
Consumption() array, 241, 245 DEBUG program, 406-413 ES (Extra Segment) segment register, 415-
Control Break Exit Address (interrupt 23H), A (Assemble) command, 408, 410-411 416
399, 545 D (Memory Dump) command, 408 EXE files, 418, 421-422
Control Mouse Cursor (interrupt 33H service H (Hex) command, 411 EXE2BIN program, 421
4), 98 memory location, 407 Exit (interrupt 21H service 4CH), 386, 539
Control-Break Check (interrupt 21H service R (Register) command, 406-409, 411 Exploded( ) array, 241-242, 245
33H), 382, 530 T (Trace) command, 409 exponents, 466
Create File W (Write) command, 411 expressions, evaluating with CodeView, 360
(interrupt 21H service 16H), 379, 523 Debug window, 343-354 extended ASCII code, 10
(interrupt 21H service 3CH), 385, 533 DEBUG.COM file, 343 strings as, 6
Create New File DOS 3+ (interrupt 21H Debugged Alphabetizing Program listing, unable to print, 5
service 5BH), 398, 543 353-363 Extended Open/Create DOS 4.0 (interrupt
Create New Program Segment (PSP) debugging, 343-364 21H service 6CH), 399
(interrupt 21H service 26H), 381, decimal numbers, converting hex to, 434-
526 451 F
Create Subdirectory (interrupt 21H service DefaultChart( )subprogram, 244 FAT Information for Default Drive (interrupt
39H), 382, 532 DEHEXER.ASM Program listing, 441 21H service 1BH), 379, 524
Create Unique File DOS 3+ (interrupt 21H DEHEXER.ASM with Subroutine listing, 452- FAT Information for Specified Drive
service 5AH), 398, 542-543 453 (interrupt 21H service 1CH), 379,
CREATEINDEX statement, 263, 266, 271 Delete File 524-525
Critical Error Handler (interrupt 24H), 399, (interrupt 21H service 13H), 379, 522 fields, 259
546 (interrupt 21H service 41H), 386, 535 File Allocation Table (FAT), 304-305
CS (Code Segment) segment register, 415-417 DELETE statement, 285 file attributes, 389
positioning code, 418-419 Delete Subdirectory (interrupt 21H service file control block (FCB), 379
CURRENCY data type, 467-468 3AH), 382, 532-533 file handles, 385
current index, 264 Diagnostic Services (interrupt 13H services File Size (interrupt 21H service 23H), 381,
CurrentString$ expression, 29 12-14), 374, 508, 512-513 526
cursor (mouse) digits, converting letters into, 22 files
hiding, 91-93 directives. See assembler directives -ASM, 417, 420, 449
horizontally restricting, 106-108 directories, searching for file, 386-397 -BI, 185, 241
moving, 98-100 Disk Reset (interrupt 21H service ODH), -COM, 416-422, 424-425
style, 111-116 377, 52t -EXE, 418, 421-422
vertically restricting, 109-111 Disk Transfer Area (DTA), 387 -MAK, 185
visibility, 90-91 Diskette Parameters (interrupt 1EH), 376, -OBJ, 420-421
cursor mask, 111 517 A.OBJ, 459
DiskFree& Shows listing, 384-385 CAP.ASM, 430
D DiskFree&( ) function, 382-385 CHRTB.BI, 243
data disks, space available, 382-385 CHRTBEFR.QLB, 241
adding to assembler programs, 421-425 Display Mouse Cursor (interrupt 33H service DEBUG.COM, 343
advanced handling and sorting, 297-338 1), 90 FRIENDS.DAT, 266-271, 274
BASIC internal representation, 463-470 DIV instruction, 442-447 GENERAL.BAS, 80, 185
moving between registers or registers and Divide by Zero (interrupt 0), 367, 499 VO, 381
memory, 405-406 DOS, 367 IBMDOS.COM, 389
searching through, 333-338 file services, 385-386 LINK.EXE, 471
sorting, 317-332 DOS interrupts, 10-19, 87, 376-400 MENU.BAS, 80, 185
DATA assembler directive, 422, 480, 485 reference, 518-548 MOUSE.BAS, 80, 185
data formats DOS Reserved (interrupt 30H-3FH), 399, MOUSE.COM, 87
CURRENCY, 467-468 548 MOUSESYS.COM, 87
DOUBLE, 466-467 DOS Service Interrupt (interrupt 21H), 518 MSDOS.COM, 389
INTEGER, 464-466 DOUBLE data format, 466-467 PAINT.BAS, 203
LONG, 464-466, 474-475
SINGLE, 466-467
doubly linked list, 309-317 PAINT.DAT, 209, 212
DRAW statement, 227-229 PIE.BAS, 241
data segment, 480, 485-486 DrawBox subroutine, 219-220 PRINTZ.COM, 421
DATA statement, 298-299 DrawCircle subroutine, 220-221 PRINTZ.EXE, 421
data structures, 302-303 DrawLine subroutine, 216-219 PRINTZ.OBJ, 421
data types, 462-463 DrawPaint subroutine, 221-222 PROGRAM.OBJ, 459
Database Program Using ISAM listing, 291- DrawPixel subroutine, 214-216 reading strings from, 4-5
293 DrawSprite subroutine, 234-236 searching directory for, 386-397
databases, 259-293 DrawSprite Subroutine listing, 235-236 sorting by pointers, 264-265
fields, 259 DS (Data Segment) segment register, 415- SPRITE.DAT, 238
index, 260-265 416 UIASM.LIB, 80
current, 264 Duplicate File Handle (interrupt 21H service UIASM.OBj, 80
NULL, 264, 271 45H), 386, 536 UIASM.QLB, 80, 185
moving around in, 274-276 DW (define word) assembler directive, 421- WINDOW.BAS, 80, 185
records, 259-263 425, 480 WINDOWER BAS. 80-84
> Index 551
Find Current Disk (interrupt 21H service GetFirstMatchingFile$ Function listing, 393- (interrupt 21H service 1DH-20H), 379,
19H), 379-380, 524 394 525
Find Cursor Position (interrupt 10H service GetInteger%() function, 19-28 (interrupt 21H service 32H), 382, 530
3), 368, 501 GetLeftButtonPress subroutine, 201, 205-206 (interrupt 21H service 34H), 382, 530
Find First Matching File (interrupt 21H GetNextMatchingFile$ Function listing, 396- (interrupt 21H service 37H), 382, 531
service 4EH), 386-387, 390, 539-540 397 (interrupt 21H service 50H-53H), 398,
Find Keyboard Status (interrupt 16H service GetNextMatchingFile$() function, 387, 394- 541
2), 375, 516 397 (interrupt 21H service 55H), 398, 541
Find Next Matching File (interrupt 21H GetQQQ Function listing, +79 (interrupt 21H service 58H), 398, 542
service 4FH), 386, 394-395, 540 GetQQQ$ (QBX Version) Program listing, (interrupt 28H-2EH), 399, 548
flags, 14 483 interrupts, 10-19, 367-400
floating point numbers, 466-467 GetQQQ$() function, 476-483 interrupt 0 (Divide by Zero), 367, 499
exponents, 466 GetSprite subroutine, 231, 236-238 interrupt 1 (Single Step), 367, 499
significand, 466 GetSprite Subroutine listing, 237 interrupt 2 (Nonmaskable Interrupt (NMI),
Force Duplication of File Handle (interrupt GetVideo Card$ Function listing, 253 367, 499
21H service 46H), 386, 536-537 GetVideoCard$() function, 250-254 interrupt 3 (Breakpoint), 367, 499
Free Allocated Memory (interrupt 21H GetXYRange% Function listing, 255 interrupt 4 (Overflow), 367, 499
service 49H), 386, 537-538 GetXYRanges%( )function, 254-256 interrupt 5 (Print Screen), 367, 499
FRIENDS.DAT file, 266-271, 274 graphics, 199-256 interrupt 6 (Reserved), 368, 499
FriendsByAge index, 262, 271 pie charts, 241-244 interrupt 7 (Reserved), 368, 499
FriendsByHeight index, 263, 271 presentation, 240-249 interrupt 8 (Time of Day), 368, 499-500
functions, 200 video cards, 249-254 interrupt 9 (Keyboard), 368, 500
data segment and, 485-486 Graphics Character Definitions (interrupt interrupt OAH (Reserved), 368, 500
no parameters, 470-472 1FH), 376, 517 interrupt OBH-OFH (BIOS End of Interrupt),
one long integer parameter, 474-476 368, 500
one parameter, 473-474 H interrupt 10
professional input, 19-28 handles for windows, 39-40, 49, 69, 76 service 0 (Set Screen Mode), 368, 500-501
returning string, +76-483 hexadecimal numbers, 12-13 service 1 (Set Cursor Type), 368, 501
two parameters, 483-484 converting to decimal, 434-451 service 2 (Set Cursor Position), 368, 501
hidden files, searching for, 389 service 3 (Find Cursor Position), 368, 501
G Hide Mouse Cursor (interrupt 33H service 2), service 4 (Read Light Pen Position), 368,
GENERAL.BAS file, 80, 185 92, 216 502
Get Arrow Key$ Function listing, 8-9 service 5 (Set Active Display Page), 368,
Get Current Directory on Specified Drive I 502
(interrupt 21H service 47H), 386, V/O Control (interrupt 21H service 44H), service 6 (Scroll Active Page Up), 141,
537 386, 536 368, 502
Get Current DTA (interrupt 21H service IBMDOS.COM file, 389 service 7 (Scroll Active Page Down), 141,
2FH), 382, 387, 529 indirect addressing, 438-439 368, 502
Get Date (interrupt 21H service 2AH), 382, initialization subroutine, 203-204 service 8 (Read Attribute and Character at
528 Initialize Mouse (interrupt 33H service 0), Cursor Position), 368, 503
Get DOS Version Number (interrupt 21H 88 service 9 (Write Attribute and Character at
service 30H), 382, 530 Initialize Printer Port (interrupt 17H service Cursor Position), 49, 51, 137, 368,
Get Equipment%( ) function, 372-373 1), 375, 516 503
Get Extended Country Information (interrupt Initialize RS232 Port (interrupt 14H AH=0), service A (Write Character ONLY at
21H service 65H), 398 374,513) Cursor Position), 368, 503
Get Extended Error DOS 3+ (interrupt 21H Initialize subroutine, 201 service B (Set Color Palette), 368, 503
initializing service C (Write Dot), 368, 504
service 59H), 398, 542
Get Free Disk Space (interrupt 21H service mouse, 87-89 service D (Read Dot), 368, 504
menus, 125-135 service E (Teletype Write to Active Page),
36H), 382-383, 531
Get Global Code Page (interrupt 21H service mouse, 117 368, 504
windows, 40-49 service FH (Return Video State), 368-370,
6601H), 398
Get Integer% Function listing, 27-28 INKEY$ statement, 5-9, 18, 27, 148 504
InKeyNoEcho$( ) function, 5, 9-10, 16-19 service 10H (Set Palette Registers), 368, 505
Get Interrupt Vector (interrupt 21H service
InKeyNoEcho$( ) Function listing, 18-19 function 0 (Set Individual Palette
35H), 382, 531
input, 3-35 Register), 368, 505
Get Machine Name DOS 3+ (interrupt 21H
service 5EOOH), 398, 543 interpreting keyboard, 28 function 1 (Set Overscan Register), 369,
keyboard capabilities, 3 505
Get Printer Setup DOS 3+ (interrupt 21H
parsing, 29-35 function 2 (Set All Palette Registers),
service 5E03H), 398, 544
Get Program Segment Prefix DOS 3 + input functions, 19-28 369, 505-506
Input Parsing Function listing, 35 function 7 (Read Individual Palette
(interrupt 21H service 62), 398, 545
INPUT statement, 3-5 Register), 369, 506
Get Return Code of Subprocess (interrupt
INPUT$() function, 4-5, 9 function 8 (Read Overscan Register),
21H service 4DH), 386, 539
InRegs data structure, 13 369, 506
GET statement, 209, 213
INSERT statement, 270 function 10H (Set DAC Register), 369,
Get Time (interrupt 21H service 2CH), 382,
Installed Memory Size (interrupt 12H), 374 506
529
INSTR() function, 32-33, 64 function 12H (Set DAC Registers), 369,
Get Verify State (interrupt 21H service 54H),
instructions, labeling, 418 506
398, 541
INTEGER data format, 464-466 function 13H (Select Color Page Mode),
Get/Set File Date & Time (interrupt 21H
integer values 369, 507
service 57H), 398, 541-542
reading from keyboard, 19-28 service 1] (Character Generator), 369,
GetArray( )array, 231, 236-238, 240
verifying positive (+) or negative (-), 24-26 507
GetArrowKey$() function, 7-9 service 12H (Alternate Select), 251, 369,
GetDefaultDisk$() function, 380-381 integers
adding, 483-484 507
GetDefaultDisk$() Function listing, 380-381
summing and printing, 487-488 service 1A (Video Card), 250
GetEquipment% listing, 372-373
Internal to DOS interrupt 11H (Equipment Determination),
GetFirstMatchingFile$() function, 387-394, see ad 1 SALT SA £9594 271 &N7 SNR
552
nnn
& Advanced
nnn eEUUU EEEESSESEEEISSSIIISS SSIS
BASIC
interrupt 12H (Determine Memory Size), service 7 (Console Input Without Echo), service 3AH (Delete Subdirectory), 382,
374, 508 377, 412, 519 532-533
interrupt 13H service 8 (Console Input w/o Echo with C service 3BH (Change Current Directory),
service 0 (Reset Disk), 374, 508 Check), 377, 519 382, 533
service 1 (Read Status of Last Operation), service 9 (String Print), 377, 412, 425- service 3CH (Create File), 385, 533
374, 508-509 427, 445, 520 service 3DH (Open File), 385, 533-534
service 2 (Read Sectors into Memory), service OAH (String Input), 377, 434-436, service 3EH (Close File Handle), 385, 534
374, 509 520 service 3FH (Read from File or Device),
service 3 (Write Sectors to Disk), 374, service OBH (Check Input Status), 377, 386, 534
509-510 520 service 40H (Write to File or Device),
service 4 (Verify Sectors), 374, 510 service OCH (Clear Keyboard Buffer and 386, 535
service 8 (Return Drive Parameters), 374, Invoke Service), 377, 520 service 41H (Delete File), 386, 535
511 service ODH (Disk Reset), 377, 521 service 42H (Move Read/Write Pointer),
service OAH-OBH (Reserved), 374, 511 service OEH (Select Disk), 377-378, 521 386, 535-536
service OCH (Seek), 374, 511-512 service OFH (Open Preexisting File), 379, service 43H (Change File Attributes), 386,
service ODH (Alternate Disk Reset), 374, 521 536
512) service 10H (Close File), 379, 521 service 44H (I/O Control), 386, 536
service OEH-OFH (Reserved), 374, 512 service 11H (Search for First Matching service 45H (Duplicate File Handle), 386,
service 10H (Test Drive Ready), 374, 512 File), 379, 521 536
service 11H (Recalibrate Hard Drive), 374, service 12H (Search for Next Matching service 46H (Force Duplication of File
512 File), 379, 522 Handle), 386, 536-537
service 12H-14H (Diagnostic Services), service 13H (Delete Files), 379, 522 service 47H (Get Current Directory on
374 service 14H (Sequential Read), 379, 522 Specified Drive), 386, 537
service 19H (Park Heads PS/2 Only), 374, service 15H (Sequential Write), 379, 523 service 48H (Allocate Memory), 386, 537
513 service 16H (Create File), 379, 523 service 49H (Free Allocated Memory),
interrupt 13H (Diagnostic Services), 512-513 service 17H (Rename File), 523 386, 537-538
interrupt 14H service 18H (Internal to DOS), 379, 524 service 4AH (SETBLOCK), 386, 538
AH=0 (Initialize RS232 Port), 374, 513 service 19H (Find Current Disk), 379-
service 4BH (Load or Execute Program
AH=1 (Send Character Through Serial 380, 524 (EXEC)), 386, 538-539
service 4CH (Exit), 386, 539
Port), 374, 513-514 service 1AH (Set DTA Location), 379, 524
AH = 2 (Receive Character From Serial service 1BH (FAT Information for Default
service 4DH (Get Return Code of
Port), 374, 514 Drive), 379, 524
Subprocess), 386, 539
AH =3 (Return Serial Port Status), 374, 514 service 1CH (FAT Information for service 4EH (Find First Matching File),
interrupt 15H (Cassette I/O), 375, 515 Specified Drive), 379, 524-525 386-387, 390, 539-540
service 4FH (Find Next Matching File),
interrupt 16H service 1DH-20H (Internal to DOS), 379,
service 0 (Read Key from Keyboard), 375, 386, 394-395, 540
525
515 service 50H-53H (Internal to DOS), 398,
service 21H (Random Read), 381, 525
541
service 1 (Check if Key Ready to be service 22H (Random Write), 381, 525
Read), 375, 515 service 23H (File Size), 381, 526
service 55H (Internal to DOS), 398, 541
service 2 (Find Keyboard Status), 375, 516
service 56H (Rename File), 398, 541
service 24H (Set Random Record Field),
interrupt 17H 381, 526
service 57H (Gev/Set File Date & Time),
service O (Print Character in AL), 375, 398, 541-542
service 25H (Set Interrupt Vector), 381,
service 58H (Internal to DOS), 398, 542
516 526
service 1 (Initialize Printer Port), 375, 516 service 59H (Get Extended Error DOS
service 26H (Create New Program
service 2 (Read Printer Status into AH), 3+), 398, 542
Segment (PSP)), 381, 526
375, 516-517 service 5AH (Create Unique File DOS
service 27H (Random Block Read), 381,
interrupt 18H (Resident BASIC), 375, 517 3+), 398, 542-543
526-527
interrupt 19H (Bootstrap), 375, 517 service 5BH (Create New File DOS 3+),
service 28H (Random Block Write), 381,
interrupt LAH 398, 543
527
service 0 (Read Time of Day), 375, 517 service 5CH (Lock/Unlock Access to File
service 29H (Parse File Name), 381, 527-
DOS 3+), 398, 543
service 1 (Set Time of Day), 375, 517 528
service 5EOOH (Get Machine Name DOS
interrupt 1BH (Keyboard Break Address), service 2AH (Get Date), 382, 528
376, 517 3+), 398, 543
service 2BH (Set Date), 382, 528
interrupt 1CH (Timer Tick Interrupt), 376, service 5E02H (Set Printer Setup DOS
service 2CH (Get Time), 382, 529
517 3+), 398, 544
service 2DH (Set Time), 382, 529
interrupt 1DH (Video Parameter Tables), service 5E03H (Get Printer Setup DOS
service 2EH (Set or Reset Verify Switch),
376, 517 3+), 398, 544
382, 529
interrupt 1EH (Diskette Parameters), 376, service 5FO3H (Redirect Device DOS 3+),
service 2FH (Get Current DTA, 382, 387,
517
398, 544
529
interrupt 1FH (Graphics Character service 5FO4H (Cancel Redirection DOS
service 30H (Get DOS Version Number),
Definitions), 376, 517 3+), 398, 545
382, 530 service 61H (Reserved), 398
interrupt 20H (Terminate), 376, 411, 518 service 31H (Terminate Process and Keep
interrupt 21H (DOS Service Interrupt), 518 service 62H (Get Program Segment Prefix
Resident), 382, 530 DOS 3+), 398, 545
service 0 (Program Terminate), 377, 518 service 32H (Internal to DOS), 382, 530
service 1 (Keyboard Input), 377, 412, service 63H-64H (Reserved), 398
service 33H (Control-Break Check), 382, service 65H (Get Extended Country
427, 430, 518 530
service 2 (Character Output on Screen), Information), 398
service 34H (Internal to DOS), 382, 530 service 6601H (Get Global Code Page),
14-15, 377, 410-412, 430, 446, service 35H (Get Interrupt Vector), 382,
486, 488, 518
398
531
service 3 (Standard Auxiliary Device service 6602H (Set Global Code Page),
service 36H (Get Free Disk Space), 382- 398
Input), 377, 518 383, 531 service 67H (Set Handle Count DOS 3.0),
service 4 (Standard Auxiliary Device service 37H (Internal to DOS), 382, 531 398, 545
Outpud, 377, 518 service 38H (Returns Country Dependent service 68H (Commit File DOS 3.30),
service 5 (Printer Output), 377, 519 Information), 382, 531-532 399, 545
service 6 (Console I/O without Echo), 16, service 39H (Create Subdirectory), 382, service 6CH (Extended Open/Create DOS
377, 519 532 Alh\ 200
er >
eer Index 553
elit eaten
interrupt 22H (Terminate Address), 399, Keyboard Break Address (interrupt 1BH), MenuMarkChoice( ) subprogram, 175-178
545 376, 517 MenuNames( ) array, 126
interrupt 23H (Control Break Exit Address), keyboard buffer, 308-309 MenuReadChoice$() function, 181-184
399, 545 Keyboard Input (interrupt 21H service 1), MenuReadChoices$ Function listing, 183
interrupt 24H (Critical Error Handler), 399, 377, 412, 427, 430, 518 menus, 123-194
546 keyboards designing system, 123-125
interrupt 25H (Absolute Disk Read), 399, input, 3 directly reading input, 166-174
546-547 assembly language, 427-431 displaying menubar, 135-141
interrupt 26H (Absolute Disk Write), 399, interpreting, 28 displaying/hiding individual, 150
547 reading hiding menubar, 141-146
interrupt 27H (Terminate and Stay integer values, 19-28 initializing, 125-135
Resident), 399, 547 strings, 4-6 interrogating on choices, 181-184
interrupt 28H-2EH (Internal to DOS), 399, keys, scan code, 6-10, 18 keypress, 145-152
548 marking selection, 175-178
interrupt 2FH (Multiplex Interrupt), 399, L opening/closing, 150
548 labels, 418 Professional Development System (PDS),
interrupt 30H-3FH (DOS Reserved), 399, LEFT$( ) function, 6, 34 185-194
548 LEN() function, 6 teading input, 146-166
interrupt 33H (Mouse Interrupt), 87 letters, converting into digits, 22 special considerations, 124-125
service O (Initialize Mouse), 88 LIM 4.0 Support (interrupt 67H), 399 toggle items, 174
service 1 (Display Mouse Cursor), 90 Line Chart listing, 249 unmarking selection, 178-181
service 2 (Hide Mouse Cursor), 92, 216 line charts, 248-249 MenuShow( )subprogram, 135-141, 190
service 3 (Button Status), 94, 216 LINE INPUT statement, 5, 27 MenuShow( )Subprogram listing, 138-140
service 4 (Control Mouse Cursor), 98 LINE statement, 217, 219 MENUTEST.BAS program, 185
service 5 (Number of Presses of Mouse LINK.EXE, 471 MenuUnMarkChoice listing, 178-179
Button), 100-101, 205 Linked List Example listing, 307-308 MenuUnMarkChoice( )subprogram, 178-181
service 6 (Mouse Button Released), 103- linked lists, 303-308 Microsoft assembler, 420
106, 215 doubly, 309-317 MID$() function, 6
service 7 (Restrict Column Mouse Cursor), File Allocation Table (FAT), 304-305 MODEL assembler directive, +71
106-108 listings. See program listings modular programming, 42
service 8 (Restrict Row Mouse Cursor), Load or Execute Program (EXEC) (interrupt monitors
106, 109-110 21H service 4BH), 386, 538-539 CGA, 250, 252
service AH (Mouse Cursor Style), 111-114 LoadSprite subroutine, 232, 238 EGA, 250-251
interrupt 40H-5FH (Reserved), 399, 548 LoadSprite Subroutine listing, 238 MDA, 250, 252
interrupt 60H-67H (Reserved for User LOCATE statement, 52 VGA, 250-251
Software), 399, 548 Lock/Unlock Access to File DOS 3+ monochrome screen, windows, 49
interrupt 68H-7FH (Not Used), 400, 548 (interrupt 21H service 5CH), 398, mouse, 87-118
interrupt 80H-85H (Reserved by BASIC), 543 button, 100-106
LONG data format, 464-466, 474-475 cursor
400, 548
loop instruction, 447-449 color invert characters, 112
interrupt 86H-FOH (Used by BASIC
loops, 448-449 displaying then hiding, 93
Interpreter), 400, 548
LTRIM$() function, 183 hiding, 91-93
interrupt F1H-FFH (Not Used), 400, 548
INTERRUPT( ) routine, 9-15, 87, 367-368 horizontally restricting, 106-108
INTERRUPTX() routine, 11, 15, 367, 390 M moving cursor, 98-100
ISAM (Indexed Sequential Access Method), machine language, 403-404 style, 111-116
259-290 MAK files, 185 text mode, 98
See also databases masks (cursor and screen), 111 vertically restricting, 109-111
compacting files, 288 MASM 5.1, 457 visibility, 90-91
creating files, 266 MDA monitor, 250, 252 driver software, 87
MEDIUM model, 471 initializing, 87-89, 117
NULL index, 261
ISAMPACK utility, 288
memory monitoring, 153-157
addresses, 210 Professional Development System (PDS),
location, 407 116-118
J
model, 471 reading immediate information from, 94-98
JA (Jump if Above) instruction, 432-433 segmentation, 413-417 Mouse Button Released (interrupt 33H
JAE (jump if Above or Equal) instruction, storing character strings, 425-427 service 6), 103-106, 215
433 Menu Initialize% Function listing,” 133-134 Mouse Cursor Style (interrupt 33H service
JB (ump if Below) instruction, 432-433 MENU.BAS file, 80, 185 AH), 111, 113-114
JBE (ump if Below or Equal) instruction, MenuAttrbs( ) array, 128 Mouse Information Subprogram listing, 96
433 menubar Mouse Interrupt (interrupt 33H), 87
JCXZ Jump of CX = 0) instruction, 433 displaying, 135-141 mouse-driven paint program, 199-222
JE (Jump if Equal) instruction, 433 hiding, 141-146 DrawBox subroutine, 219-220
JMP instruction, 424 MenuCheck( ) function, 193 DrawCircle subroutine, 220-221
JNA (Jump if Not Above) instruction, 433 MenuCheckEvent$ listing, 168-173 DrawLine subroutine, 216-219
JNAE (Jump if Not Above or Equal) MenuCheckEvent$( ) function, 146, 166-174 DrawPaint subroutine, 221-222
instruction, 433 MenuChoice subroutine, 202, 206-214 DrawPixel subroutine, 214-216
JNB (Jump if Not Below) instruction, 433 MenuChoices( ) array, 126 GetLeftButtonPress subroutine, 205-206
JNBE (Jump if Not Below or Equal) MenuColor( )subprogram, 189 initialization subroutine, 203-204
instruction, 433 MenuGetEvent$() function, 146-166 MenuChoice subroutine, 207-214
JNE (ump if Not Equal) instruction, 433 MenuGetEvents$ Function listing, 158-163 Mouse-driven Paint Program listing, 222-226
JNZ Gump if Result was Not Zero) MenuHide( ) subprogram, 141-146 MOUSE.BAS file, 80, 185
instruction, 433 MenuHide( )Subprogram listing, 143-144 MOUSE.COM file, 87
jump instruction, 424 Menulnit() subprogram, 187 MouseBorder( ) subprogram, 117
JZ Gump if Result was Zero) instruction, 433 Menulnitialize%() function, 125-135 MouseDriver( )subprogram, 117
MenulnKey$() function, 192 MouseHide Cursor Program listing, 93
K MenuMarkChoice Subprogram listing, 175- MouseHide( ) subprogram, 117
Keyboard (interrupt 9), 368, 500 176 MouseHideCursor Subprogram listing, 92-93
554 P Advanced BASIC
MouseHideCursor( )subprogram, 92-93 OutRegs data structure, 13 ADDEM.ASM Using the Data Segment,
MouseHorizontalRange Subprogram listing, Overflow (interrupt 4), 367, 499 486
107-108 Array Example, 299-300
MouseHorizontalRange( )subprogram, 107- P Assembly Language Version of BASIC
108 PAINT statement, 221 Function That Adds Two Integers,
Mouselnformation( )subprogram, 94-98 PAINT.BAS file, 203 457
Mouselnit( ) subprogram, 117 PAINT.DAT file, 209, 212 Averaging Program, 3
Mouselnitialize%( ) function, 87-91 paragraphs, 414 Averaging Program Variation, 3
Mouselnitialize%() Function listing, 89 parameters, 460-461 CAP.ASM Program, 430, 432-433
MouseMoveCursor Subprogram listing, 99 address, 461 Database Program Using ISAM, 291-293
MouseMoveCursor( )subprogram, 98-100 near reference, 460 Debugged Alphabetizing Program, 353,
MousePoll( )subprogram, 117-118 Park Heads PS/2 Only (interrupt 13H service 363
MouseSetCursor subprogram, 111-116 19H), 374, 513 DEHEXER.ASM Program, 441
MouseSetCursor( )Subprogram listing, 114- Parse File Name (interrupt 21H service DEHEXER.ASM with Subroutine, 452-453
115 DiskFree& Shows, 384-385
29H), 381, 527-528
MouseShow( ) subprogram, 117, 190 parsing input, 29-35 DrawSprite Subroutine, 235-236
MouseShowCursor( )subprogram, 90-92 PDS. See Professional Development System Get Arrow Key$ Function, 8-9
MouseShowCursor( ) Subprogram listing, 90 Get Integer% Function, 27-28
(PDS)
MOUSESYS.COM file, 87 GetDefaultDisk$() Function, 380-381
PDS Bar Graph listing, 246-248
MouseTimesPressed Subprogram listing, 102 pie charts, 241-246 GetEquipment%, 372-373
MouseTimesPressed( )subprogram, 100-103 GetFirstMatchingFile$ Function, 393-394
PIE.BAS file, 241
MouseTimesReleased Subprogram listing, GetNextMatchingFile$ Function, 396-397
pointers, sorting file by, 264-265
105 GetQQQ Function, +79
POP instruction, 444, 446
MouseTimesReleased( )subprogram, 103- GetQQQ$ (QBX Version) Program, 483
106
positive (+) integer values, 24-26
presentation graphics GetSprite Subroutine, 237
MouseVerticalRange( ) subprogram, 109-111 GetVideo Card$ Function, 253
MouseVerticalRange( ) Subprogram listing, bar charts, 246-248
line charts, 248-249 GetXYRange% Function, 255
109-110
pie charts, 241-246 InKeyNoEcho§$( ) Function, 18-19
MOV instruction, 405-406
Professional Development System (PDS), Input Parsing Function, 35
Move Read/Write Pointer (interrupt 21H
240-249
Line Chart, 249
service 42H), 386, 535-536
Print Character in AL (interrupt 17H service Linked List Example, 307-308
MOVE statements, 274, 281
0), 375, 516 LoadSprite Subroutine, 238
MOVENEXT statement, 276
Print Screen (interrupt 5), 367, 499 Menu Initialize% Function, 133-134
MOVS instruction, 488
MSDOS.COM file, 389 PRINT statement, 51, 276 MenuCheckEvent$, 168-173
MUL instruction, 432, 440, 442-447 PRINT USING statement, 51 MenuGetEvents$ Function, 158-163
Multiplex Interrupt (interrupt 2FH), 399, PrintChar()subprogram, 486-487 MenuHide( )Subprogram, 143-144
548 PRINTCHAR.ASM listing, 487 MenuMarkChoice Subprogram, 175-176
MyFriend index, 271 Printer Output (interrupt 21H service 5), MenuReadChoices$ Function, 183
377, 519 MenuShow( ) Subprogram, 138-140
N PrintFirst Element (BC Version) listing, 494 MenuUnMarkChoice, 178-179
Names() array, debugging, 346-349 PrintFirstElement (QB.EXE and QBX.EXE Mouse Information Subprogram, 96
near reference, 460 Version) listing, 495 Mouse-driven Paint Program, 222-226
negative (-) integer values, 24-26 PrintFirstElement() subprogram, 493-495 MouseHide Cursor Program, 93
negative numbers, 464-466 printing MouseHideCursor Subprogram, 92-93
NextTerm$( ) function, 32-35 borders in windows, 67 MouseHorizontalRange Subprogram, 107-
NextTerm() function, 29-30 scan codes, 9 108
Nonmaskable Interrupt (NMI) (interrupt 2), PrintString( )subprogram, 458-459, 488-492 Mouselnitialize%() Function, 89
367, 499 PRINTSTRING.ASM (QBX.EXE Version) MouseMoveCursor Subprogram, 99
normalized numbers, 466 listing, 492 MouseSetCursor() Subprogram, 114-115
Not Used PRINTSTRING.ASM listing, 489 MouseShowCursor( )Subprogram, 90
(interrupt 68H-7FH), 548 PrintSum%( )subprogram, 487-488 MouseTimesPressed Subprogram, 102
(interrupt F1H-FFH), 548 PRINTXYZ.ASM listing, 427 MouseTimesReleased Subprogram, 105
NULL index, 261, 264, 271 PRINTZ, 411-412 MouseVerticalRange( )Subprogram, 109-
null string, 9-10 adding data, 423-424 110
Number of Presses of Mouse Button assembling, 420-421 Ordered Search Program, 338
(interrupt 33H service 5), 100-101, PRINTZ.ASM listing, 420
PDS Bar Graph, 247-248
205 PDS Pie Chart, 246
PRINTZ.ASM Program listing, 424
numbers PRINTZ.COM file, 421
PRINTCHAR.ASM, 487
averaging, 3 PRINTZ.EXE file, 421 PrintFirst Element (BC Version), 494
implicit, 466 PRINTZ.OB) file, 421
PrintFirstElement (QB.EXE and QBX.EXE
normalized, 466 Version), 495
PROC assembler directive, 449-450, 460-
PRINTSTRING.ASM, 489
462, 473
fe} procedures, 449-451, 453 PRINTSTRING.ASM (QBX.EXE Version),
OB) files, 420-421 492
Professional Development System (PDS)
offset address, 210, 460-461 PRINTXYZ.ASM, 427
CURRENCY variable type, 297-298
OFFSET assembler directive, 426 PRINTZ.ASM, 420
ISAM (Indexed Sequential Access
OldAttrb() array, 50-51, 57, 129 PRINTZ.ASM Program, 424
Method), 259-290
OldText() array, 44, 50-51, 57, 129 QuickSort Program, 330-331
menus, 185-194
OnFlag() variable, 44-45 Return5 Function, 472
mouse, 116-118
Open File (interrupt 21H service 3DH), 385 SavesSprite( )Subroutine, 238
, presentation graphics, 240-249
533-534 Shell Sort Program, 324-325
windows, 80-84
Open Preexisting File (interrupt 21H service Sprite.BAS, 233-234, 239-240
professional input functions, 19-28 Two-dimensional QuickSort Program,
OFH), 379, 521 program listings
OPEN statement, 266-267 331-332
Add5 Function, 474 Two-dimensional Shell Sort Program, 325-
Ordered Search Program listing, 338 Add5Long Function, 476
ORG assembler directive, 419 326
ADDEM.ASM, 484
Unordered Data Search, 333
> Index 555
Using the BASIC PDS Menu System, 193- Read Overscan Register (interrupt 10H RPStack! variable, 30-31
194 service 10H function 8), 369, 506 RTRIM$( ) function, 183
Using Windows in BASIC PDS, 84 Read Printer Status into AH (interrupt 17H Run menu, 348
VideoMode% Function, 371 service 2), 375, 516-517
WindowDelete Subprogram, 78-79 Read Sectors into Memory (interrupt 13H s
WindowGetHandle Function, 45-46 service 2), 374, 509 SADD( ) function, 388
WindowHide Subprogram, 58 READ statement, 299 SaveSprite subroutine, 232, 238
WindowMove% Function, 73-75 Read Status of Last Operation (interrupt 13H SavesSprite( )Subroutine listing, 238
WindowPrint% Function, 65-67 service 1), 374, 508-509 SBB (subtract with borrow) instruction, 475
WindowShow( ) Subprogram, 54-55 Read Time of Day (interrupt 1AH service 0), scan codes, 6-10, 18
Program Terminate 375, 517 printing, 9
(interrupt 20H), 376, 411 Recalibrate Hard Drive (interrupt 13H Screen Control
(interrupt 21H service 0), 377, 518 service 11H), 374, 512 (interrupt 10H service 6), 141
PROGRAM.OBJ file, 459 Receive Character From Serial Port (interrupt (interrupt 10H service 7), 141
Programmer's Work Bench (PWB), 459 14H AH=2), 374, 514 SCREEN function, 50, 136
programming, modular, 41-42 records, 259-263 screens
programs current, 276 mask, 111
CAP.COM, 427-431 deleting, 285-288 mode, 242, 369-371
CodeView, 343-364 index, 260-265 page number, 51
converting hex to decimal, 434-451 insertion order, 260 pixel ranges, 254-256
DB, 266-290 seeking, 280-285 saving region, 136
DEBUG, 406-413 sorting, 273 sprites, 199, 229-238
debugging, 343-364 type, 267-270 Scroll Active Page Down (interrupt 10H
entry point, 419-420 updating, 288-290 service 7), 368, 502
EXE2BIN, 421 recursion, 327 Scroll Active Page Up (interrupt 10H service
loading with /L switch, 15 REDIM statement, 231, 236 6), 368, 502
loops, 448-449 Redirect Device DOS 3+ (interrupt 21H Search for First Matching File (interrupt 21H
MENUTEST.BAS, 185 service 5FO3H), 398, 544 service 11H), 379, 521
mouse-driven paint, 199-222 Redo from start error message, 3-4 Search for Next Matching File (interrupt
PRINTZ, 411-412 Regions()array, 241, 245 21H service 12H), 379, 522
Return5%( ), 470-472 registers, 10-15 Seek (interrupt 13H service OCH), 374, 511-
sharing variables with modules, 41 segment, 388, 414 512
SPRITE, 230-240 viewing contents, 406-407
SEEK statement, 265, 281
PROISAM.EXE TSR program, 266 RegType data type, 13-14 SEEKEQ statement, 280
PROISAMD.EXE TSR program, 266 Rename File SEEKGE statement, 280
prompt string, question mark (?) after, 3 (interrupt 21H service 17H), 523 SEEKGT statement, 280, 283, 285
PSET statement, 216, 234-235 (interrupt 21H service 56H), 398, 541 segment addresses, 210
pull-down menus. See menus Reserved segment registers, 388, 414
PUSH instruction, 443 (interrupt 6), 368, 499 CS (Code Segment), 415-417
PUT statement, 213, 232-233, 240 (interrupt 7), 368, +99 DS (Data Segment), 415-416
(interrupt 0AH), 368, 500 ES (Extra Segment), 415-416
Q (interrupt 13H service OAH-OBH), 374, setting all to same value, 416-417
QB.EXE compiler, 459, 476-479, 488-489, 511 SS (Stack Segment), 415-416
494-495 (interrupt 13H service OEH-OFH), 374, segments, 414-416
descriptors, 468 512 SELECT CASE statement, 30-31, 206, 250
QBX.EXE compiler, 459, 479-483, 490-492, (interrupt 21H service 61H), 398 Select Color Page Mode
494-495 (interrupt 21H service 63H-64H), 398 (interrupt 10H service 10H function 12H),
descriptors, 468 (interrupt 40H-5FH), 399, 548
369
UIASM.OBJ file, 80 Reserved by BASIC (interrupt 80H-85H), (interrupt 10H service 10H function 13H),
question mark (?) after prompt string, 3 400, 548 369, 507
Quick libraries, 80 Reserved for User Software (interrupt 60H- Select Disk (interrupt 21H service OEH),
A.QLB, 458 67H), 399, 548 377-378, 521
Quicksort, 326-332 Reset Disk (interrupt 13H service 0), 374,
Send Character Through Serial Port
recursion, 327 508 (interrupt 14H AH= 1), 374, 513-
QuickSort Program listing, 330-331 Resident BASIC (interrupt 18H), 375, 517
514
RESTORE statement, 299 Sequential Read (interrupt 21H service 14H),
R Restrict Column Mouse Cursor (interrupt
379, 522
Random Block Read (interrupt 21H service 33H service 7), 106-108
Sequential Write (interrupt 21H service
27H), 381, 526-527 Restrict Row Mouse Cursor (interrupt 33H
15H), 379, 523
Random Block Write (interrupt 21H service service 8), 106, 109-110
Set Active Display Page (interrupt 10H
28H), 381, 527 RET (return) instruction, 450-451
service 5), 368, 502
Random Read (interrupt 21H service 21H), RETRIEVE statement, 276, 282, 288
Set All Palette Registers (interrupt 10H
3a) 525 Return Drive Parameters (interrupt 13H
service 10H function 2), 369, 505-
Random Write (interrupt 21H service 22H), service 8), 374, 511
Return Serial Port Status (interrupt 14H 506
381, 525 Set Color Palette (interrupt 10H service B),
Read Attribute and Character at Cursor AH=3), 374, 514
Return Video State (interrupt 10H service 368, 503
Position (interrupt 10H service 8), Set Cursor Position (interrupt 10H service
368, 503 FH), 368, 370, 504
Return5 Function listing, 472
2), 368, 501
Read Dot (interrupt 10H service D), 368, 504 Set Cursor Type (interrupt 10H service 1),
Read from File or Device (interrupt 21H Return5 procedure, 471
Return5%() program, 470-472 368, 501
service 3FH), 386, 534 Set DAC Register (interrupt 10H service 10H
Read Individual Palette Register (interrupt Returns Country Dependent Information
(interrupt 21H service 38H), 382, function 10H), 369, 506
10H service 10 function 7), 369, 506 Set DAC Registers (interrupt 10H service
Read Key from Keyboard (interrupt 16H 531-532
reverse Polish calculator program, 28-35 10H function 12H), 506
service 0), 375, 515
Set Date (Interrupt 21H service 2BH), 382,
Read Light Pen Position (interrupt 10H RIGHTS( ) function, 6, 34
Rows(i) array, 46 528
service 4), 368, 502
556 P» Advanced BASIC
Set DTA Location (interrupt 21H service passing arrays, 493-495 Verify Sectors (interrupt 13H service 4),
1AH), 379, 524 string parameters, 488-492 374, 510
Set Global Code Page (interrupt 21H service with parameters, 486-488 VGA monitor, 250-251
6602H), 398 subroutines, 200 Video Card (interrupt 10H service 1A), 250
Set Handle Count DOS 3.0 (interrupt 21H SWAP statement, 323 video cards, 249-254
service 67H), 398, 545 Video Parameter Tables (interrupt 1DH),
Set Individual Palette Register (interrupt 10H T 376, 517
service 10H function 0), 368, 505 tables VideoMode% Function listing, 371
Set Interrupt Vector (interrupt 21H service name, 267-270 VideoMode%( ) function, 370-371
25H), 381, 526 number, 269-270
Set or Reset Verify Switch (interrupt 21H Teletype Write to Active Page (interrupt 10H Ww
service 2EH), 382, 529 service E), 368, 504 WarningBeep% parameter, 7, 20
Set Overscan Register (interrupt 10H service Terminate (interrupt 20H), 518 Watch menu, 356
10H function 1), 359, 505 Terminate Address (interrupt 22H), 399, Watch... option, 349
Set Palette Registers (interrupt 10H service 545 watchpoints, 363
10H), 368, 505 Terminate and Stay Resident (interrupt WINDOW.BAS file, 80, 185
Set Printer Setup DOS 3+ (interrupt 21H 27H), 399, 547 WindowDelete Subprogram listing, 78-79
service 5EO2H), 398, 544 Terminate Process and Keep Resident WindowDelete( )subprogram, 76-79
Set Random Record Field (interrupt 21H (interrupt 21H service 31H), 382, WINDOWER.BAS file, 80-84
service 24H), 381, 526 530 WindowGetHandle Function listing, 45-46
Set Screen Mode (interrupt 10H service 0), Test Drive Ready (interrupt 13H service WindowGetHandle%( ) function, 39-49, 55,
368, 500-501 10H), 374, 512 Td.
Set Time (interrupt 21H service 2DH), 382, text WindowHide Subprogram listing, 58
529 printing in windows, 60-68 WindowHide( ) subprogram, 40, 49, 56-59,
Set Time of Day (interrupt 1AH service 1), saving in windows, 50-55 71
375, 517 text mode, mouse cursor, 98 WindowLocate() subprogram, 83-84
SETBLOCK (interrupt 21H service 4AH), Text() atray, 43, 129 WindowMove% Function listing, 73-75
386, 538 Time of Day (interrupt 8), 368, 499-500 WindowMove%( ) function, 68-76
SetDefaultDisk%( ) function, 377-378 TIMER function, 229 WindowOpen( ) subprogram, 82-83
SETINDEX statement, 264, 273, 285 Timer Tick Interrupt (interrupt 1CH), 376, WindowPrint% Function listing, 65-67
SETMEM() function, 231 517 WindowPrint%( ) function, 40, 60-68
SHARED COMMON statement, 54, 200 tracepoints, 363 Window Print( ) subprogram, 83-84
SHARED keyword, 41 TSR (Terminate and Stay Resident) programs, windows, 39-84
Shell Sort Program listing, 324-325 376 arrays for data, 42
shell sorts, 317-326 PROISAM.EXE, 266 attributes, 43
SHL (shift left) instruction, 440 PROISAMD.EXE, 266 color, 41, 48
SHR (shift right) instruction, 440 TurnOffMenu() subprogram, 150 deleting, 76-79
SINGLE data format, 466-467 TumOnMenu( ) subprogram, 150 displaying, 40, 49-56
Single Step (interrupt 1), 367, 499 two's complement notation, 464-466 handles, 39-40, 49, 69, 76
SLEEP statement, 246 Two-dimensional QuickSort Program listing, hiding, 40, 56-59
sorting records, 273 331-332 initializing, 40-49
SortQuick( )subprogram, 327-329 Two-dimensional Shell Sort Program listing, location, 43
SPRITE program, 230-240 325-326 monochrome screen, 49
Sprite.BAS listing, 233-234, 239-240 moving, 68-76
SPRITE.DAT file, 238 multiple, 39
SpriteArray() array, 234
U
number used, 42
UIASM.LIB file, 80
sprites, 199, 229-238 printing
UIASM.OBJ file, 80
SS (Stack Segment) segment register, 415- borders, 67
UIASM.QLB file, 80, 185
416 text in, 60-68
stack Unordered Data Search listing, 333
Professional Development System (PDS),
popping values off, 444 Unused Interrupt
80-84
(interrupt 68H-7FH), 400
pushing values on, 443-445 saving
(interrupt F1H-FFH), 400
Standard Auxiliary Device Input (interrupt attributes, 50-55
21H service 3), 377, 518 UPDATE statement, 288
text, 50-55
Standard Auxiliary Device Output (interrupt Used by BASIC Interpreter (interrupt 86H- screen
FOH), 548
21H service 4), 377, 518 attributes, 44-49
StartOver subroutine, 20-21, 23 Using BASIC PDS Menu System listing, 193- position, 40
Storage() array, 203, 209-211 194
size, 41, 43
String Input (interrupt 21H service OAH), Using Windows in BASIC PDS listing, 84 text, 44
utilities
377, 434-436, 520 visibility, 44-45
String Print (interrupt 21H service 9), 520 ISAMPACK, 288
WindowShow( )subprogram, 39-56, 59, 72-73
StringAddress( ) function, 468, 490-491 WindowShow( ) Subprogram listing, 54-55
StringAssign() subprogram, 468, 480-482 Vv words, 464
StringLength() function, 468, 490 VAL( ) function, 30 Write Attribute and Character at Cursor
strings, 468-470 values, checking against comparison value, Position (interrupt 10H service 9),
as extended ASCII code, 6 431-432 368, 503
ASCIIZ, 388 variables, 200, 297-298 Write Character ONLY at Cursor Position
descriptors, 468-470 assigning different types to each other, 24 (interrupt 10H service A), 368, 503
function returning, 476-483 CURRENCY type, 297-298 Write Dot (interrupt 10H service C), 368,
null, 9-10 DOUBLE type, 297 504 S
passed as argument and printed, 488-492 INTEGER type, 297 Write Sectors to Disk (interrupt 13H service
printing, 425-427 LONG type, 297 3), 374, 509-510
reading from files or keyboard, 4-6 sharing, 41 Write to File or Device (interrupt 21H service
separating out characters, 6 SINGLE type, 297 40H), 386, 535
stripping off first word, 64 STRING type, 297
SUB instruction, 429-430, 432 VARPRT( ) function, 210 x
subprograms, 200 VARSEGC( ) function, 210 XOR, 213
CUSTOMER SUPPORT
It is important that you register your purchase of any Simon & Schuster software package.
By completing and returning your Owner Registration Card, you become eligible for:
Software Support
S &S will provide support to registered owners. Our technical support number is (212) 373-
7212 or FAX (212) 373-8192.
Mail-in Support Service—Registered owners may write to us with questions. We will
respond in writing. There is no additional charge for this service.
We realize that our software packages are put to a wide variety of uses, however, we can
only answer questions about the software package itself. We cannot support the hardware
and operating system required to run our software packages.
Before Calling Customer Support
Before calling our Technical Support Department, please make sure you have followed the
steps in the “Pre-call Checklist” below.
Pre-call Checklist
1. Ifyou are having difficulty understanding the program, have you read and performed
the suggestions listed in the manual?
2. If you are not sure how to operate the program, have you used the help system (where
available) to find the answer?
3. If there seems to be a problem in the software, can you reproduce the problem by
following your steps again?
4, Ifthe program displayed an error message, please write down the exact message.
5. Youshould be familiar with the hardware configuration you are using. We may need to
know the brand/model of your computer, printer, the total amount of memory avail-
able, what video adaptor(s) you have in the system, the operating system version, etc.
6. When you call our Technical Support Department, please be at your computer or be
prepared to repeat the sequence of steps leading up to the problem.
Important! Read before Opening Sealed Diskette
END USER LICENSE AGREEMENT
eeeeee ee LL aeae a Ce Ce Ce Ce
Advanced BASIC
LIMITED WARRANTY REGISTRATION CARD
In order to preserve your rights as provided in the limited warranty, this card must be on file with Simon & Schuster within thirty days of purchase.
70-65875
Get the Spark. Get BradyLine.
Published quarterly. Free exclusively to our customers.
TT 1 Check here tn heain vour cubsccrintion.
Advanced BASIC
Please fill out the information below and return it to the address listed with your original 5.25-inch
diskette. Please print clearly.
[| | am ordering a replacement diskette after 30 days but within one year. | have enclosed
my original diskette, a copy of the dated sales receipt, and check or money order
for $5.00 made out to Simon & Schuster, Inc. ISBN 0-13-658758-5
[ | am ordering a 3.5-inch disk. | have enclosed my original 5.25-inch diskette along with a
check or money order for $5.00 made out to Simon & Schuster, Inc.
ISBN 0-13-663071-5
Please mail this request to: MICROSERVICES, 200 Old Tappan Road, Old Tappan, NJ 07675.
For more information, call (201) 767-5054.
a
Basic/IBM
INTERMEDIATE/
BASIC and Beyond with Peter Norton
ADVANCED : stands out from the rest,
gee Create professional software that
transform your BASIC programs with new speed and finesse!
Advanced BASIC presents the best secrets of BASIC—techniques
and hints that will supercharge your applications and set new
performance standards. Page after page, Advanced BASIC gives
you clear, practical discussions and working examples—ideas and
insights that quickly bring you an expert grasp of BASIC. Topics
covered include:
Windowing !) The mouse
Pull-down menus Graphics sprites and animation
New database techniques ™ Working with BIOS and DOS
Interfacing with assembly language ™ And more...
ISBN 0-13-658758-5
|| 90000>
9°78 0136'587583 |