Advanced BASIC

Download as pdf or txt
Download as pdf or txt
You are on page 1of 582

mike hile d

Byt.
) (6
Navaies(WrAourlittl
The Peter Norton Computing Group

7
&

>
oo

_
: * —

ie
* a

-

:

7 ;
is)

i i
|
Advanced BASIC
The Peter Norton Programming Series

Who This Book Is For


Intermediate to advanced BASIC programmers who want to extend their program-
ming expertise and add new performance to their programs.

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

About the Peter Norton Microcomputer Libraries


from Brady
All of the volumes in the Peter Norton Libraries, written in collaboration with Peter
Norton Computing, provide clear, in-depth discussions of the latest developments
in computer hardware, operating systems, and programming. Fully tested and
rigorously reviewed by the experts at Peter Norton Computing, these libraries
deserve a special place on your bookshelf. These libraries are comprised of two
series:
The Peter Norton Hardware Library gives you an insider’s grasp of your computer
and the way it works. Included are such best-selling classics as Inside the IBM
PC, Inside the Apple Macintosh, and The Hard Disk Companion.
The Peter Norton Programming Library focuses on creating programs that work
right away and offers the best tips and techniques in the industry. It includes
Advanced Assembly Language, C Programming, C++ Programming, QBasic Pro-
gramming, Advanced DOS, and more.
MS besnovbA 9=
ected onimennages4 natoll vetsteth = - Z

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» ; »

Am otek of ole pworle i noua OF aE atetobe e~ ee -


ayes vdinw* giagin prs _

sgnondit tatucqedeso
Ri nohcl rte eit toad
~ > : :

eto” (ow sonmodelias siti ne ecnade) nearer el ah nheag rent He


iuertolev sh ud ahs anteeiaally tie rely shining galego
hing ieee vill grierames Ate elirslege gntiamge ,siewlyeel pauses
dotteadhl saul, uit)
Chained ani ors? we amen ih oo penal Hen,
ENT OTE TI Wie<n sendsl: Seah Vested TY HG valhye v

Wuyinas wo? hepa o ghiegi rages iS “prea 4 tu es


MU ait chives 24, sda slriStsinke owe ote bobeiiat ealrove Hye ne stall
YG Wied aa vaatbbra ‘neh oe a

Aig teal) ocree ay grt. RO pid tigi eget

asbiines teak
_ Oe RAD gil
a oe afl erespantetonn ois Tete
reat
Nee 7 ron

tint oF ene OME ng*

Bee
Advanced BASIC

Steven Holzner with Peter Norton Computing

Brady Publishing

New York London’ Toronto Sydney Tokyo Singapore


Copyright 1991 by Peter Norton
All rights reserved,
including the right of reproduction
in whole or in part in any form

Brady Publishing

A Division of Simon & Schuster, Inc.


15 Columbus Circle
New York, NY 10023

Manufactured in the United States of America

LOPOIS
MeO Die sr2

Library of Congress Cataloging-in-Publication Data

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 1. Professional Input


The INPUT Statement
The INPUT$ Function
The LINE INPUT Statement
INKEY$
GetArrowKey$—Reads Arrow Keys
InKeyNoEcho$
Using BIOS and DOS Interrupts
A DOS Keyboard Service
Writing a Professional Input Function
Interpreting Our Keyboard Input
Conclusion

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

Chapter 3. The Mouse


Getting Started
Initializing the Mouse
Mouselnitialize%(_)
Mouse Initializing Window
Making the Mouse Cursor Visible
MouseShowCursor( )
Using MouseShowCursor( )
Hiding the Mouse Cursor
MouseHideCursor( )
Using MouseHideCursor( )
Reading Immediate Mouse Information
MouselInformation( )
Finding the Mouse’s Current State
Moving the Mouse Cursor
MouseMoveCursor( )
Moving to the Top Left of the Screen
Reading the Button Pressed Queue
MouseTimesPressed( )
Counting Button Presses
Reading the Button Released Queue
MouseTimesReleased( )
Counting Button Releases
Restricting the Mouse Horizontally
MouseHorizontalRange( )
MouseHorizontalRange( ) at Work
Restricting the Mouse Vertically
MouseVerticalRange( )
eee & Contontss,
BAS be Contents vil
vii

MouseVerticalRange( ) at Work 110


Setting the Mouse Cursor 111
How to Use Screen and Cursor Masks 111
Making the Mouse Cursor into a Dot 115
The Mouse in the BASIC PDS 116
Conclusion 119

Chapter 4. Pull-down Menus 121


Designing Our Menu System 123
Special Menu Considerations 124
Initializing Our Menus 125
Menulnitialize%( ) 128
Initializing a Test Menubar 134
Displaying the Menubar 135
Showing Our Test Menubar 140
Hiding the Menubar 141
Hiding Our Test Menubar 145
Reading Menu Input 146
Waiting for Input from Our Test Menus 164
Reading Menu Input without Waiting 166
Getting Input from Our Test Menus 173
Marking a Menu Selection 175
Marking Items in Our Test Menus 176
Unmarking a Menu Selection 178
Unmarking Items in Our Test Menus 179
Checking Up on a Menu 181
Reading Items from Our Test Menus 184
Menus in the BASIC PDS 185
Conclusion 195

Chapter 5. Graphics 197


A Mouse-driven Paint Program 199
Subroutine Initialize 203
Subroutine GetLeftButtonPress 205
Subroutine MenuChoice 206
Subroutine DrawPixel 214
viii B® Advanced BASIC

Subroutine DrawLine 216


Subroutine DrawBox 219
Subroutine DrawCircle 220
Subroutine DrawPaint 221
The Paint Program Listing 222
Drawing a Clock 227
Sprites and Animation 229
Subroutine DrawSprite 234
Subroutine GetSprite 236
Subroutine SaveSprite 238
Subroutine LoadSprite 238
The SPRITE.BAS Listing 238
Presentation Graphics in the BASIC PDS 240
Pie Charts 241
Bar Charts 246
Line Charts 248
What Video Card Do We Have? 249
Finding the Screen’s Pixel Ranges 254
Conclusion 256

Chapter 6. Databasing 257


The Current Database Index 264
Our Database Program 265
Seeking Records 280
Deleting Records 285
Updating ISAM Files 288
Chapter 7. Advanced Data Handling and Sorting 295
BASIC Variables 297
The DATA Statement 298
Arrays 299
Data Structures 302
Linked Lists 303
Circular Buffers | 308
Binary Trees 309
Shell sorts
317
Quicksorts 326
> Contents ix

Searching Your Data 333


Conclusion 339

Chapter 8. Debugging 341


QuickBASIC Debugging 343
CodeView 354

Chapter 9. A Tour of BIOS and DOS 365


The BIOS Interrupts 367
The BIOS Screen Mode 369
Determining Equipment Installed in Your Computer 371
The DOS Interrupts 376
Setting the Default Disk 377
Getting the Default Disk 380
Finding Free Disk Space 382
The DOS File Services 385
Finding the First Matching File 386
Finding the Next Matching File(s) 394
The Rest of DOS 398
Conclusion 400

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

Adding Data to Assembler Programs 421


The DB and DW Directives 421
Strings in Memory 425
INT 21H Service 9—Print a String 425
Using Comments 427
Accepting Keyboard Input 427
The Program CAPCOM 427
The SUB and ADD Instructions 429
Conditional Jumps 431
The CMP Instruction 431
More Conditional Jumps 433
An Assembler Program to Convert Hex to Decimal 434
The DIV and MUL Instructions 442
The LOOP Instruction 448
Procedures in Assembly Language 449
Conclusion 453

Chapter 11. BASIC/Assembly Language Interface 455


Linking into BASIC 457
Receiving Parameters from BASIC 460
Returning Values to BASIC 462
BASIC’s Internal Representation of Data 463
INTEGERs and LONGs 464
Two’s Complement Notation 464
SINGLEs and DOUBLEs 466
CURRENCY 467
Strings and Arrays 468
A Function with No Parameters 470
A Function with One Parameter 473
A Function with One Long Integer Parameter 474
A Function Returning a String 476
GetQQQ$( ) with QB.EXE and BC.EXE 476
GetQQQ$(_) with QBX.EXE 480
A Function with Two Parameters 483
A Function Using the Data Segment 485
A Subprogram with Parameters 486
> Contents § xi

A Subprogram with a String Parameter 488


PrintString( ) with BC.EXE and QB.EXE 488
PrintString( ) with QBX.EXE 490
A Subprogram with an Array Parameter 493
PrintFirstElement with BC.EXE 493
PrintFirstElement with QB.EXE and QBX.EXE 494
Conclusion 496

Appendix. BIOS and DOS Reference 497

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

Basic and QuickBASIC are trademarks of Microsoft Corporation.


OS/2 is a trademark of IBM Corporation.
Logitech mouse is a trademark of Logitech Inc.
Introduction
BASIC Makes a Comeback
For many long years, BASIC was the language not of programmers, but of
dilettantes. BASIC (or BASICA or GWBASIC) was the language that came free with
your computer. Originally intended as a learning language, it became the language
that let you scroll “Hello” up on the screen to show you what your new computer
was good for. However, if you attempted any substantial programming, you found
that the results were invariably slow and prone to awkward errors (“Redo from
start”). To professionals, BASIC was the Dick and Jane of programming.
Circumstances can change, however, and nowhere faster than in computing.
And BASIC did change. It gained a new, unexpected vitality with the introduction
of superfast compilers. Compilers that produce tight, efficient code gave BASIC a
shot in the arm.
Among these compilers were the Microsoft BASIC products, including Quick-
BASIC, with which this book is designed to be used. The executable code these
compilers began to produce was, while not the fastest, at least good. And program
speed began to improve with later releases. Programmers at Microsoft began to
write important pieces of code in BASIC. These and other developments
impressed the PC programming community, and BASIC began to make its
comeback.
As run times decreased, programmers began to notice once again the major
asset of the language: its enormous number of instructions, unrivaled in practically
any language for the PC today. Even the very earliest version of MS BASIC sup-
ported no less than 160 built-in functions, statements, and commands. What
might take lines and lines of code in other languages was often one single state-
ment in BASIC.
During its time in the wings, BASIC borrowed much from other languages. For
example, it took the SELECT CASE structure from Pascal, the COMMON state-
ment from FORTRAN, the rudimentary use of pointers — VARPTR, VARSEG,
SSEG, SADD, SSEGADD and others — from C. BASIC got better.
The notorious one-line IF statement had been improved to an IF
THEN...ELSE...END IF structure. Data types became user defined. Support for
reaching the operating system directly was added (with INTERRUPT( ) and
INTERRUPTX( )). The language that once had everything seemed to have every-
thing once again.
xiii
xiv Pb Advanced BASIC

The BASIC Professional Development System


In November 1989, a significant milestone was reached with the release of
Microsoft BASIC 7.0, also known as the BASIC Professional Development System
(PDS). This package is intended as a serious programming tool (matched by a
serious price), and it indicates the level of Microsoft’s commitment to BASIC.
Special libraries have been added, including financial functions and some
advanced formatting functions. An entire toolbox kit was also included, filled with
tools for matrix math, presentation graphics, and an advanced user interface —
and we'll dig into that toolbox in this book.
In addition, a whole new type of file structure was added for databases — ISAM
files. Referenced by pointers, ISAM records can be sorted quickly and easily, and
they provide support for professional database applications. BASIC just continues
to grow: The language itself, before including these add-ons, now supports no less
than 252 built-in functions, statements, commands, metacommands, and rou-
tines. And the number of BASIC programmers also continues to grow.
In short, BASIC has once again become a premiere programming language.
BASIC is back.

BASIC Taken Seriously


Now that BASIC is being used by serious programmers and professionals, books
on BASIC have to follow the trend. No longer will you find chapter-long exposi-
tions on checkbook balancing programs. No longer will you see books that plod
through the BASIC instruction set alphabetically as though you were reading a
dictionary and not a learning tool. Instead, BASIC books today have to provide
real value, significantly enhancing the reader's knowledge. Programmers will settle
for nothing less.
This is even more true of a book on advanced BASIC because only people
serious about programming will read it — and they want to use their time profita-
bly, not waste it. For that reason, we’re going to fill this book with the hottest
programming topics available today. We'll work through all the tricks a profes-
sional programmer has, from adding mouse support to programs to showing how
fast animation works and from adding pull-down menus to interfacing assembly
language directly into BASIC for raw computing power. We'll even use some of
BASIC’s own internal subprograms to make our job easier.
As we work our way through advanced BASIC, we'll also explore many of the
tools that the powerful BASIC Professional Development System has
& Introduction xv

to offer. In fact, we devote one chapter (Chapter 6, Databasing) exclusively to PDS


programming; in that chapter we create a database program using the new ISAM
file system. Readers that have the BASIC PDS will find just how far they can go.
All in all, we'll make a complete survey of up-to-the-minute BASIC in this book.
We're not going to have much time to get boring — not with as much power as we
can generate in today’s BASIC. Let’s take a look at what’s coming.

What’s in This Book

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:

Introduction What you're reading now. Provides an overview and explains


what you'll need to run the programs in this book.
Chapter 1: Professional Input
This is the perfect place to start — with the input to our
programs. We'll see how to handle input in a professional way,
and add a few enhancements that BASIC left out (such as a
custom version of INKEY$ that doesn’t echo struck characters
on the screen).
Chapter 2: Windows
Here’s where we develop our window system. This chapter has
plenty of examples, and we see how to set up a window, show
it on the screen, print to it, move it around, resize, or hide it.
The Professional Development System (PDS) offers some built-
in window functions as well here.
Chapter 3: The Mouse
In this chapter we add support for the mouse through
interrupt @H33. We see how to turn the mouse cursor on and
off, and to read information from it with a few easy calls. For
those with the PDS, we'll look at the built-in mouse interface.
xvi > Advanced BASIC

Chapter 4: Pull-down Menus


Here we put windows and the mouse together and come up
with pull-down menus. We'll see how easy it is to set up a
mouse- or keyboard driven menubar and find what selection
was made. Again, the PDS offers some menu tools, and we'll
take a look at them too.
Chapter 5: Graphics
In this chapter, we see how to augment BASIC’s graphics
capabilities by writing our own paint program. Among other
things, we design and use graphics sprites in this chapter, and
look into the PDS’ presentation graphics, as well as animation.
Chapter 6: Databasing
In Chapter 6, we'll start putting together a database program.
We'll see how using PDS ISAM files makes database
programming a snap.
Chapter 7: A Tour of BIOS and DOS
In this chapter, we make use of some of the assets that DOS
and BIOS give us. Among other things, we see how to
determine what equipment is in the computer, how much
space there is on a disk, and how to search through a hard
disk for files that match a given file specification (including
wildcards).
Chapter 8: Advanced Data Handling and Sorting
If you have an array full of data that you want to sort, this
chapter is for you. Just pass it to one of the quick sorting
routines here and you're all set. In addition, we'll explore just
about all the ways there are of organizing data in BASIC, and
develop a fast data search program.
Chapter 9: Debugging
This chapter gives us an introduction to the fundamentals of
debugging, both under QuickBasic and with the CodeView
standalone debugger.
Chapter 10: Welcome to Assembly Language
This primer gives us some exposure to assembly language.
Advanced programmers often need real speed and compact
ieee> Introduction
totuctionn xvii
Xvi

code, and there’s nothing that can compete with assembly


language. Here we take a look at assembly language
fundamentals so we'll be able to interface it to BASIC in the
next chapter.
Chapter 11: Connecting to Assembly Language
In this chapter, we see how we can tie assembly language
routines into BASIC, making them look exactly like
FUNCTIONS or SUBs. We make use of the new simplified
segment directives in MASM 5.1 or later versions.
Appendix: DOS and BIOS Reference
This appendix lets you tap the power of DOS and BIOS.
Throughout the book, we’ll make use of DOS and BIOS
services, and we'll list them all in this appendix.

What You’ll Need


That’s it. We're just about ready to start. To make use of the programs in this
book, you'll need a Microsoft BASIC product, such as the Microsoft BASIC com-
piler (BC.EXE), QuickBASIC (QB.EXE or extended QuickBASIC, QBX.EXE), or
the BASIC PDS (BC.EXE Version 7.0, 7.10, or later).
In addition, if you want to make use of the material in Chapters 10 and 11 on
powering up your BASIC programs with assembly language, you'll need the Micro-
soft macro assembler (MASM.EXE), Version 5.1 or later. We are going to use this
or later versions because they support the simplified segment directives, which
have made changes in mixed-language programming nothing short of
revolutionary.
You should also know that many of the programs in this book access BIOS and
DOS directly with the INTERRUPT( ) and INTERRUPTX( ) routines. By doing
this, we unlock a vast reserve of computing power — just about everything the
computer can do can be done with these routines. If you’re using QuickBASIC
(QB.EXE or QBX.EXE), however, you must use the /L switch like this with pro-
grams that use INTERRUPT( ) or INTERRUPTX( ):

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
%

E os y Siva? Aa Johto aleanced


L, race nl pu a:

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.

The INPUT Statement


Listing 1-1 shows the first example program of the book.

Listing 1-1. Averaging Program. -


PRINT “This program takes the average of three numbers."
INPUT “The numbers"; A!, B!, C! :
PRINT “Thank You. The average is:";(A! + B! + C!)/3.0

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.

Listing 1-2. Averaging Program Variation.


INPUT “Please type three numbers”; Al, 6!.-¢!
“Thank You. The average is:";C(A! + Bi + C!)/3
PRINT

Then the prompt on the screen would look like this:

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.

Redo from start


The numbers?

This doesn’t exactly match the idea of user friendliness, nor does it promote a profes-
sional image.

The INPUT$ Function


Although the INPUT$( ) function is usually used to read strings from files, you can use it
to read keys from the keyboard as well. Unfortunately, you have to specify exactly how
many characters to read. Here’s an example, where we read (exactly) 10 characters and
find the first occurrence of the letter e:

PRINT “Please type a string of 10 characters.”


Instring$S = INPUT$(10) ‘Note: no echoing on screen.
VALUZ = INSTRCInstring$,"e")
IF VALUZ = 0 THEN
PRINT "Thank you. There was no ‘e' in that string.”
ELSE
PRINT “Thank you. The first ‘e' was character" VALUZ
END IF

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:

PRINT "Please type a string of 10 characters."


InstringS = INPUTS(10) ‘Note: no echoing on screen.
> Professional Input 5

Now we can search the string for the first e, if there was one, and print out its location:

PRINT "Please type a string of 10 characters."


Instring $ = INPUT$C10) "Note: no echoing on screen.
—> VALUEX = INSTR(Instring$,"e")
IF VALUEX = 0 THEN
PRINT "Thank you. There was no ‘e' in that string."
ELSE
PRINT “Thank you. The first ‘e' was character" VALU%
END IF

However, requiring a specific number of characters (carriage returns do not terminate


input) is a big drawback. And, even though you can read input without echoing it on the
screen, INPUT$( ) cannot return extended ASCII codes. That rules out function keys,
arrow keys, Alt keys, and so on.

See our function InKeyNoEcho$( ), developed a little later on, for a solution to
these problems.

The LINE INPUT Statement


On the other hand, the LINE INPUT statement is a very useful one. All it does is to
read a string of characters from the keyboard. The string is terminated once you type a
carriage return. Here’s an example:

LINE INPUT “Please type a string:";Instring$


PRINT “Thank you. That string was";LENCInstring$);"characters long.”

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:

PRINT "Type a character."

DO
InChar$S = INKEYS.
LOOP WHILE InChar$S = ""

PRINT “Thank you. That character was:";InChar$

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:

PRINT "Type a character." .


—> dO a ae
InChar$ = INKEYS
LOOP WHILE InChar$ = “”

PRINT "Thank you. That character was:";inChar$s

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

GetArrowKey$—Reads Arrow Keys


In this example, let’s write a function to read the arrow keys on the computer’s
numeric keyboard and return their values in an easy-to-interpret way. For example, if the
right arrow key was pressed, the return value might be r. If the up arrow key was pressed,
we could return a value of u. This lets us use the arrows keys easily in our programs,
without having to memorize scan codes. Here are the possible return values from
GetArrowKey$( ):
r Right arrow key pressed
] Left arrow key pressed
u Up arrow key pressed
d Down arrow key pressed
h Home key pressed
. End key pressed
GetArrowKey$( ) can be used like any other function. When you use it, it waits for an
arrow key to be pressed and then returns the corresponding letter. Let’s pass an argument
called WarningBeep% to GetArrowKey$( ). If you set WarningBeep% to a nonzero value,
GetArrowKey$( ) will beep when you press any key but an arrow key.
This function is pretty simple. We start by setting up a continuous loop, waiting for
keys:

FUNCTION GetArrowKeyS (WarningBeep%)

bo

LOOP WHILE 1

Now we can accept input from INKEY$:

FUNCTION GetArrowKeyS (WarningBeepz)

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:

FUNCTION GetArrowKey$ (WarningBeep%)

bO
dO
InStr$ = INKEY$
LOOP WHILE InStr$ = ""

> Code = ASC(RIGHTSC{InStr$, 1))


IF LENCInStr$) = 2 THEN
*

Echeck for arrow key]

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).

Listing 1-3. Get Arrow Key$ Function.


FUNCTION GetArrowKey$S (WarningBeepZ)

DO
DO
InStr$S = INKEYS
LOOP WHILE InStr$ = ""

Code = ASCCRIGHTS(InStr$, 1))


IF LENCInStr$) = 2 THEN
~ SELECT CASE Code
CASE &H4D
GetArrowKeyS = "r"
EXIT FUNCTION
CASE &H4B
GetArrowKey$ = "Ll"
EXIT FUNCTION
CASE &H48
GetArrowKey$ = “u”
EXIT FUNCTION
CASE &H50
> Professional Input 9

Listing 1-3. Get Arrow Key$ Function. | 2 of 2


GetArrowKey$ = "q"
EXIT FUNCTION
CASE &H47
GetArrowKey$ = “h"
EXIT FUNCTION
CASE &H4F
GetArrowKey$ = “e"”
EXIT FUNCTION
> END SELECT
IF WarningBeep% THEN BEEP
ELSE
IF WarningBeep% THEN BEEP
END IF
LOOP WHILE 1

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:

Length of Means that


returned string the output is
0 Null string (“ ”). No character has been typed since the
keyboard was last checked.
1 The ASCII code of a struck key. For example, if the q key
was struck, IKeyNoEcho$ would equal q.
2 Extended ASCII code. These keys don’t normally echo on
the screen anyway (such as arrow keys or function keys).
The first character is CHR$(0) and the second is the keys’s
scan code (check BASIC documentation for a list of scan
codes).

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.

Using BIOS and DOS Interrupts


By far the strongest way of augmenting the power of BASIC is to use the resources
available to us in the computer’s operating system, and we’re going to use them fre-
quently. Each interrupt is a prewritten program already in memory, ready for us to use.
The commands you use at DOS level (e.g., COPY, TIME, VER, XCOPY, or FORMAT) all
make use of the built-in interrupts — and now we, as BASIC programmers, can too. It’s
like adding a whole new language to our programming capabilities.
The way we pass and receive data to and from interrupt routines is by using the BASIC
INTERRUPTC( ) routine, and the 80 x 86’s registers. The microprocessor handles data in
16-bit registers, and you can think of them as the computer’s built in variables. The ones
we'll see most are named ax, bx, cx, and dx, and they store data in the computer’s CPU
(see Figure 1-1).
> Professional Input 11

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.

INTERRUPT() and INTERRUPTX() are the same, except that INTER-


RUPTX( ) allows you to use a few more registers. Use INTERRUPTX( ) to
give you more control.

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:

InRegs.flag bit Flag


11 Overflow Flag
10 Direction Flag
Enable Interrupts Flag
Trap Flag
Sign Flag
Zero Flag
Auxiliary Carry Flag
Parity Flag
WW
AN
A
ON Carry Flag

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):

DIM InRegs AS RegType, OutRegs AS RegType


— InRegs.ax = &H0200

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:

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &HO0200
— InRegs.dx = ASC("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:

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &HO200
InRegs.dx = ASCC"A")
— CALL INTERRUPT(&H21, InRegs, 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.

All programs that use INTERRUPT( ) or INTERRUPTX( ) must be loaded with


the /L switch if you’re using QuickBASIC (QB.EXE or QBX.EXE) like this: QB
PROG /L or QBX PROG /L.
16 > Advanced BASIC

A DOS Keyboard Service

In InkeyNoEcho$( ), we'll rely on a DOS service, namely interrupt @H21 service 6.


This service does exactly what we want; it reads keys from the keyboard but does not
echo them on the screen. INT &H21, service 6 console I/O without echo, is shown below
(also see the appendix):

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:

DIM InRegs AS RegType, OutRegsAS RegType


InRegs.ax = &H0600 —
InRegs.dx = &HFF :
CALL INTERRUPT(C&H21, InRegs, OutRegs)

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:

OutRegs.flag bit Flag


ih Overflow Flag
10 Direction Flag
9 Enable Interrupts Flag
8 Trap Flag
> Professional Input 17

OutRegs.flag bit Flag


it Sign Flag
> 6 Zero Flag
% Auxiliary Carry Flag
2 Parity Flag
0 Carry Flag

That means we can check whether a character was waiting like this:

DIM InRegs AS RegType, OutRegs AS Regtype


InRegs.ax = &H0600
InRegs.dx = &HFF
CALL INTERRUPTC(&H21, InRegs, OutRegs)

REM No character ready if zero flag set

> IF COutRegs.flags AND 2°6) THEN


InKeyNoEcho$ = “"

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:

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H0600
InRegs.dx = &HFF

CALL INTERRUPT(&H21, InRegs, OutRegs)

REM No character ready if zero flag set

IF (COutRegs.flags AND 2°6) THEN


InkKeyNoEcho$ = ""
FESE
> IF (OutRegs.ax AND &HFF)
InKeyNoEcho$ = CHRS(OutRegs.ax AND &HFF)
18 b> Advanced BASIC

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:

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H0600
InRegs.dx = &HFF

CALL INTERRUPT(&H21, InRegs, OutRegs)

REM No character ready if zero flag set

IF COutRegs.flags AND 2'6) THEN


InKeyNoEcho$ = ""
ELSE
IF COutRegs.ax AND &HFF) <> O THEN
InKeyNoEcho$ = CHR$COutRegs.ax AND &HFF)
ELSE ‘Need one more call
InRegs.ax = &H0600
InRegs.dx = &HFF
CALL INTERRUPT(&H21, InRegs, OutRegs)
InKeyNoEcho$ = CHR$(O) + CHR$COutRegs.ax AND &HFF)
END IF
END IF

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

Listing 1-4 shows the whole function.

Listing 1-4. InKeyNoEchoS


) Function
(. 1 of2
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
si AS INTEGER
di AS INTEGER
eer> Professional Input 19

Listing 1-4. InKeyNoEchoS() Function. 2 of 2


flags AS INTEGER
END TYPE

DECLARE SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

FUNCTION InKeyNoEcho$

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H0600
InRegs.dx = &HFF

CALL INTERRUPT(&H21, InRegs, OutRegs)

REM No character ready if zero flag set

IF COutRegs.flags AND 276) THEN


InKeyNoEcho$ = ""
ELSE
IF COutRegs.ax AND &HFF) <> QO THEN
InKeyNoEcho$ = CHRS$C(OutRegs.ax AND &HFF)
ELSE ‘Need one more call
InRegs.ax = &HO600
InRegs.dx = &HFF
CALL INTERRUPT(&H21, InRegs, OutRegs)
InKeyNoEcho$ = CHR$(O) + CHRSCOutRegs.ax AND &HFF)
END IF
END IF

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.

Writing a Professional Input Function


Let’s see what a professional input function that reads integer values from the keyboard
might look like. We can call it, say, GetInteger%( ); this function should screen numeric
input to avoid mistakes, along with the embarassing consequences (e.g., “Redo from
start”). That means that it has to check for possible overflows and only accept legal
characters.
20 P& Advanced BASIC

If an inappropriate number was typed, we can design GetInteger%(_) to simply erase


that number and start over, moving the cursor back to where it was when GetInteger%(_)
was called. If we’ve set up the screen in some careful way, this function won't ruin it. Also,
like our function GetArrowKey$( ), we can pass a parameter named WarningBeep%. If
WarningBeep% is nonzero, we can have GetInteger%(_) beep if a noninteger is entered.
Let’s write this function. First, we save the present cursor position and then set up a
DO...LOOP WHILE 1 loop, which loops forever (the only way to leave GetInteger%(_) is
by typing an integer):

FUNCTION GetInteger% (WarningBeep%)

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:

FUNCTION GetInteger% (WarningBeep%)

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

FUNCTION GetInteger% (WarningBeep%)

CursorRow% = CSRLIN
CursorCol% = POS(0)
DO
LINE INPUT InString$

IF LENCInString$) < = 6 THEN

CNumber may be ok]

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:

FUNCTION GetInteger% (WarningBeep%)


CursorRows% = CSRLIN
CursorCol% = POS(0)
DO
LINE INPUT InString$

IF LENCInString$) < = 6 THEN


_ FOR i = 1 TO LENCInString$)
Char$S = MID$C(InString$, i, 1)
IF Char$ < = "0" AND Char$ = “9" THEN

Add this digit to running total

ELSE

Check if it's a + or a ~- Cok in the first position)

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:

FUNCTION GetInteger% (WarningBeep%)

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

FUNCTION GetInteger% (WarningBeep%)

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:

FUNCTION GetiInteger% (WarningBeep%)

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

IF SUM& < 32767 THEN ‘Overflow?


GOSUB StartOver
EXIT FOR
END IF
~ IF i = LENCInString$) THEN
IF NegFflag% THEN SUM& = -SUM&
GetInteger% = SUM&
EXIT FUNCTION
END IF
ELSE

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:

FUNCTION GetInteger% (WarningBeep%)

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

IF SUM& > 32767 THEN - ‘Overflow?


GOSUB StartOver
EXIT FOR
END IF
IF i = LENCInString$) THEN
IF NegFlag% THEN SUM& = ~SUM&
GetInteger% = SUME&
EXIT FUNCTION
END IF
ELSE
> IF (Char$ = "=" OR Char$ = “+") AND i = 1 THEN

CCharacter is an acceptable sign]

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:

FUNCTION GetInteger% (WarningBeepz)


CursorRow% = CSRLIN
CursorCol% = POS(O)
dO
LINE INPUT InString$

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

Character was not “O"-"“9" or "+" or “-" -- start over.

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:

FUNCTION GetiInteger% (WarningBeep%)

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.

Listing 1-5. Get Integer% Function.


DECLARE FUNCTION GetInteger% (WarningBeep%)

FUNCTION GetInteger% (WarningBeepz)

CursorRow% = CSRLIN
CursorCol% = POS(0)
DO
LINE INPUT InString$

NegFlag% = 0
28 P& Advanced BASIC

Listing 1-5. Get Integer% Function.


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(CharS)-ASCC"0"))*10°(LENCInString$) - i)
If SUM& > 32767 THEN ‘Overflow?
GOSUB StartOver
EXIT FOR
END IF
IF i = LENCInString$) THEN
If NegFlag% THEN SUM& = -SUM&
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
GOSUB StartOver
EXIT FOR
END IF 'IF Legal sign
END IF "IF Char$ > = "0" AND Char$ < = "9"...
NEXT i :
ELSE
GOSUB StartOver
END IF ‘IF LENCInString$) < = 6...

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.

Interpreting Our Keyboard Input


Our parsing example is going to be pretty simple. We're just going
to write a reverse
Polish calculator program. It will accept and calculate problems of
variable length, such as
these:
> Professional Input 29

253 454~ [Answer: 1]


3.1 2.1 - 10 * 5 + [Answer: 6]
In other words, in the first example, 2 3 + 4 -, 2 is placed on the calculator’s stack, then
3 is added to it, giving 5. Next, 4 is subtracted from that value, leaving 1.
Let's see how to make this work. Our parsing example is not going to be exactly
bulletproof. In a real program, parsers must be able to tolerate multiple spaces, different
delimiters, upper and lower case characters, trailing or leading spaces, and any number of
other difficulties. However, that would make our parsing example very long; all we want
to demonstrate here is the essential idea of parsing, which is to break input up into its
component parts. In this program, we're just going to place the input into a string and
parse it in an elementary way by peeling successive terms off from the left.

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$:

— LINE INPUT "Expression to calculate:"; 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 +):

LINE INPUT “Expression to calculate:"; CurrentString$


>= 5 THEN Five characters is absolute min.
IF LENCCurrentString$)

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

5 +, NextTerm$(CurrentString$) would return 3.1, and CurrentString$ would be trun-


cated to. 2.) - 10 * 5 +.
Let’s get the first term (i.e., the 2 in 2 3 +) and load it into a variable named RPStack! as
the reverse Polish calculator’s stack value. We can convert the string returned by Next
Term$( ) into a numeric value with the BASIC function VAL( ):

— DECLARE FUNCTION NextTerm$ (StringToParse$)

LINE INPUT “Expression to calculate:"; CurrentString$

IF LEN(CurrentString$) >= 5 THEN "Five characters is absolute min.


> RPStack! = VAL(NextTerm$(CurrentString$)) ‘Get ist term.
+
.

**
END IF

PRINT “Result:", RPStack!

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$ < > “”:

DECLARE FUNCTION NextTerm$ (StringToParse$)


LINE INPUT “Expression to calculate:"; CurrentString$

IF LENCCurrentString$) >= 5 THEN "Five characters is absolute min.


RPStack! = VAL(NextTerm$(CurrentString$)) ‘Get 1st term.
+ dO
¢ NewTerm! = VALCNextTerm$(CurrentString$))
: Operator$ = NextTerm$(CurrentString$)

> LOOP WHILE CurrentString$ <> “"


END IF

We can perform the actual operations themselves in a SELECT CASE


statement:

DECLARE FUNCTION NextTerm$ (StringToParse$)

LINE INPUT “Expression to caltculate:"; CurrentString$

IF LENCCurrentString$) >= 5 THEN "Five characters is absolute min.


RPStack! = VAL (Next Term$(CurrentString$)) "Get Ist term.
> Professional Input 31

dO
NewTerm! = VALCNextTerm$(CurrentString$))
Operator$ = NextTerm$(CurrentString$)
> SELECT CASE Operators
CASE hg HF

RPStack! = RPStack! + NewTerm!


CASE woe

RPStack! = RPStack! - NewTerm!


CASE tee

RPStack! = RPStack! * NewTerm!


CASE 7"
RPStack! = RPStack! / NewTerm!
CASE ELSE
PRINT “Bad operator: "ss Operators
END
= END SELECT
LOOP WHILE CurrentString$ <> ""
END IF :

The SELECT CASE statement in BASIC was designed to replace a “ladder”


or series of IF... THEN...ELSE statements. Use SELECT CASE wherever pos-
sible instead of multiple IF statements—your code will execute faster.

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:

DECLARE FUNCTION NextTerm$S (StringToParseS)

LINE INPUT “Expression to calculate:"; CurrentString$

IF LENCCurrentString$) >= 5 THEN ‘Five characters is absolute min.

RPStack! = VAL(NextTerm$(CurrentString$))- ‘Get 1st term.

DO
NewTerm! = VAL(NextTerm$(CurrentStringS$))
Operator$ = NextTerm$(CurrentString$)
SELECT CASE Operator$
CASE “+"
RPStack! = RPStack! + NewTerm!
CASE oe

RPStack! = RPStack! - NewTerm!


CASE Ue aa

RPStack! = RPStack! * NewTerm!


TASE b
RPStack! = RPStack! / NewTerm!
CASE ELSE
32 Pb Advanced BASIC

PRINT “Bad operator:"; Operator$


END
END SELECT
LOOP WHILE CurrentString$ <> “"
END IF
PRINT “Result:", RPStack!

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:

DECLARE FUNCTION NextTermS (StringToParse$)


LINE INPUT "Expression to calculate:”; CurrentString$

IF LENCCurrentString$) >= 5 THEN "Five characters is absolute min.

RPStack! = VAL(CNextTerm$(CurrentString$)) ‘Get ist term.

DO
NewTerm! = VALCNextTerm$(CurrentString$))
Operator$S = NextTerm$(CurrentString$)
SELECT CASE Operators
CASE tat

RPStack! = RPStack! + NewTerm!


CASE "="
RPStack! = RPStack! - NewTerm!
CASE "*"
RPStack! = RPStack! * NewTerm!
CASE %/7*
RPStack! = RPStack! / NewTerm!
CASE ELSE
PRINT “Bad operator:"; Operators
END
END SELECT
LOOP WHILE CurrentString$
END IF

PRINT “Result:", RPStack!

FUNCTION NextTerm$ (StringToParse$)


> TermLength% = INSTR(StringToParse$, "") - 1 'Find a space?

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):

DECLARE FUNCTION NextTerm$ (StringToParse$)

LINE INPUT “Expression to calculate:"; CurrentString$

IF LENCCurrentString$) >= 5 THEN "Five characters is absolute min.

RPStack! = VAL(NextTerm$(CurrentString$)) ‘Get ist term.

DBO
NewTerm! = VAL(NextTerm$(CurrentString$))
Operator $ = NextTerm$(CurrentString$)
SELECT CASE Operators
CASE ett

RPStack! = RPStack! + NewTerm!


CASE “="
RPStack! = RPStack! - NewTerm!
CASE At pe tt

RPStack! = RPStack! * NewTerm!


CASE “s"
RPStack! = RPStack! / NewTerm!
CASE ELSE
PRINT “Bad operator: "; Operator$
END
END SELECT
LOOP WHILE CurrentString$ <>

END IF
PRINT “Result:", RPStack!

FUNCTION NextTermS (StringToParse$)


TermLength% = INSTR(StringToParse$, “ny — 1 'Find a space?
= LEN(StringToParseS)-~ TermLength% - 1
> NewStringLen%
.
.

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:

DECLARE FUNCTION NextTerm$ (StringToParse$)

LINE INPUT “Expression to calculate:"; CurrentString$

IF LENC(CCurrentString$) >= 5 THEN "Five characters is absolute min.

RPStack! = VAL(NextTerm$(CurrentString$)) "Get 1st term.

dO
NewTerm! = VAL(NextTerm$(CurrentString$))
Operator$ = NextTerm$(CurrentString$)
SELECT CASE Operator$
CASE Ag tt

RPStack! = RPStack! + NewTerm!


CASE LL en A)

RPStack! = RPStack! -— NewTerm!


CASE Wn fe Ht

RPStack! = RPStack! * NewTerm!


CASE "“/"
RPStack! = RPStack! / NewTerm!
CASE ELSE
PRINT “Bad operator:"; Operator$
END
END SELECT
LOOP WHILE CurrentString$ <> ""
END IF

PRINT “Result:", RPStack!

FUNCTION NextTerm$ (StringToParse$)


TermLength% = INSTR(StringToParse$, "") - 1 'Find a space?
NewStringLen% = LEN(StringToParse$)- TermLength% - 1
> IF TermLength% = -1 THEN 'No -- end of input string
TermLength% = LEN(StringToParse$)
NewStringlenz = 0
END IF

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

Listing 1-6. Input Parsing Function.


DECLARE FUNCTION NextTerm$ (StringToParse$)

LINE INPUT “Expression to calculate:"; CurrentString$

IF LENCCurrentString$) >= 5 THEN "Five characters is absolute min.

RPStack! = VAL(NextTerm$(CurrentString$)) 'Get ist term.


DO
NewTerm! = VAL(NextTerm$(CurrentString$))
Operator$ = NextTerm$(CurrentString$)
SELECT CASE Operators
CASE bit tbh

RPStack! = RPStack! + NewTerm!


CASE "“-"
RPStack! = RPStack! - NewTerm!
CASE ign

RPStack! = RPStack! * NewTerm!


CASES" /*
RPStack! = RPStack! / NewTerm!
CASE ELSE
PRINT “Bad operator:"; Operator$
END
END SELECT
LOOP WHILE CurrentString$ <> "“
END IF

PRINT “Result:", RPStack!

FUNCTION NextTerm$ (StringToParse$)


TermLength% = INSTR(StringToParse$, "") - 1 ‘Find a space?
NewStringLen% = LEN(StringToParse$)- TermLength’ - 1
IF TermLength% = -1 THEN 'No ~- end of input string
TermLength% = LEN(StringToParse$S)
NewStringLenz = 0
END IF
NextTerm$ = LEFT$(StringToParse$, TermLength%) ‘Next term
>
StringToParse$ = RIGHT$(StringToParse$, NewStringLenz)
END FUNCTION

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 :

ated ens mete


aa

§ % (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.

Designing Our Window System


We have to design our window system to be able to work with multiple windows; it’s
rare for a program that uses windows to use only one window. This means that we have to
have some easy way of referring to a particular window. We don’t want to have a different,
and lengthy, subroutine for each window (i.e., CALL InitWindowl( ), CALL InitWin-
dow2( ), CALL ShowWindow1l1( ), etc.).
The best way (and the way that’s used by professional windows packages as well as the
OS/2 Presentation Manager) is to use window handles, that is, a number that we can assign
to each window. If we design our window system to work with up to, say, eight windows,
then a window's handle will be its index number among those eight (see Figure 2-1).

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%( ).

Initializing Our Window System


The purpose of WindowGethandle%( ) is to initialize a window; this is always the
necessary first call. When you set up and design a window (what size to make it, where it
will be on the screen, and so on), you use WindowGetHandle%( ). This function must
return the window’s handle, which we can make an integer ranging from 1 to 8.
When we're programming later, we'll keep track of this handle; now we have a “name”
for the window, and we'll pass this value when we make any window call. In this way, the
window system knows which of the up to eight possible windows we're referring to.
Note that initializing a window does not make it appear on the screen. The standard
window technique is to allow them to be invisible while we work on them, or move them
around or whatever, which lets us perform actions behind the scenes. To display a win-
dow after we’ve initialized it, we can call WindowShow(Handle%), where Handle% is the
window’s handle. To hide it again, we can call WindowHide(Handle%).
To initialize a window, we'll need its screen position. Let’s pass the screen coordinates
of the top left corner of the window to WindowGetHandle%( ). In BASIC, (1,1) is the top
left corner of the screen and (25,80) is the bottom right (see Figure 2-2).

(1,1) Columns increase >


Rows
Increase

Figure 2-2. The Screen


> Windows 41

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:

MyHandles = WindowGetHandlex (TopR%, TopC%, NRow%, NCol%, Attr%)

where this is what each parameter means:


TopR% INTEGER Screen row of window’s top left corner
(1 = top of screen)
TopC% INTEGER Screen column of window’s top left corner
(1 = extreme left of screen)
NRow% INTEGER Number of rows the window is long
NCol% INTEGER Number of columns the window is wide
Attr% INTEGER Screen attribute to use displaying the
window.
To use these values, WindowGetHandle%( ) is going to have to fill internal window
variables with the values you've given it. All the other subprograms in the window system
can then communicate through COMMONs, allowing them to read these variables. If
you're not familiar with the idea behind the COMMON statement, it’s simple: a COM-
MON lets the various modules in your program communicate with each other. For
example, if you had three variables, A%, B%, and C%, you could share them among all
your modules by including a line like this in the beginning of each one (see Figure 2-3).

“COMMON SHARED /MyCommon/ A%, B%, C%

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.

Breaking your programs up into manageable pieces is called Modular pro-


gramming and it makes program development and debugging much easier.
The COMMON statement is the backbone of modular programming and, as
such, is exceptionally powerful.

Filling the Window Arrays


Let’s start setting up the arrays we'll need to hold the data for our windows. For example,
we'll need an array to hold the screen attribute of each window. Let’s call that array
Attribute( ). If we want eight windows, the index of Attribute( ) has to range from 1 to 8. At
some later date, we may want more than eight windows, so let’s not make that number
firm. Instead, we can start with a constant named MaxWindows, which we set to 8:

FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCoL%, Attr%)

~ 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:

FUNCTION WindowGetHandle% (TopR%, TopCz, NRow%, NCol%, Attr%)

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:

FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCol%, Attr%)

CONST Max Windows = 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

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:

FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCol%, Attr%)

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

FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCol%, Attr%)

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:

FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCol%, Attr%)

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:

FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCol%, Attr%)

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.

Listing 2-1. WindowGetHandle Function. ie) a4


FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCol%, Attr%)

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

BE) (Lae oo WindowGetHandle Function. 2 of 2


DECLARE FUNCTION WindowGetHandle% (TopR%, TopC%, NRow%, NCoL%, Attr%)

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

BotCol(i) = Topt% + NCoL%


Rows(i) = NRow%
Cols€i)d = NCoL%
Attribute(i) = Attr%
OnFlag(i) = 0
FOR j = 1 TO Rows(i)
Text(i, j) = SPACES(Cots(i))
NEXT j
EXIT FOR
END IF
NEXT i

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.

A Window Initializing Example


To prepare our example of WindowGetHandle%( ), let’s make sure we know how to
set up all the values it requires. That list looks like this:

TopR% INTEGER Screen row of window’s top left corner


(1 = top of screen)
TopC% INTEGER Screen column of window’s top left corner
(1 = extreme left of screen)
NRow% INTEGER Number of rows the window is long
NCol% INTEGER Number of columns the window is wide
Attr% INTEGER Screen attribute to use displaying the
window.

In particular, we have to supply a screen attribute. we pass this as an INTEGER, but


only the bottom 8 bits matter; this byte determines the colors of the window and its text.
For example, we can select green characters on a blue background, or yellow character on
a red background. (Note: Set bit to 1 to turn on that particular color.) A screen attribute
byte looks like Figure 2-4.
48 P Advanced BASIC

Red Green Blue Intens. Red Green Blue

Background Color Foreground Color


Figure 2-4
By setting the attribute byte, we can select the mix of red, green, and blue for both the
foreground color (the color of the character) and the background color (the color of the
widow behind the text). Also, we can set bit 3 for high intensity display, and bit 7 to make
the character blink. A red foreground on a green background would have an attribute
byte of 00100100 binary, or @H24 (See Figure 2-5):
Background Foreground
Color Color

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:

DECLARE FUNCTION WindowGetHandle% (TopRow%, TopCol%, NumRow%, NumCol%, Attr%)

Handle1% = WindowGetHandle%(1, 1, 5, 5, &H24) <—

PRINT “Window's handle is: ", Handlet%

Handle2% = WindowGetHandle%(10, 10, 5, 5, &H61) <

PRINT “Window's handle is: ", Handle2%

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:

SUB WindowShow (Handle%)

REM Save the old section of screen

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:

SUB WindowShow CHandlex)

REM Save the old section of screen


~ TR = TopRow(Handle%)
TC = TopCol (Handle%)

FOR 4 = 1 TO Rows (Handle%)


> temp1$ = ""
temp2$ = ""
FOR j = 1 TO Cols(CHandle%)

temp1$ = temp1$ + CHRS(SCREEN(TR + 4 - 1, Te + j= 1))


eis _temp2s = tempeS + CHRS(SCREEN(TR + i - 1, TC + j > 1, 13)
J
NEXT i
——————————— eS
> Windows 51

Then, for each row, we store the completed temporary strings in the backup arrays
OldText( ) and OldAttrb( ):

SUB WindowShow (CHandle%)

REM Save the old section of screen

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:

SUB WindowShow (Handle)

REM Save the old sectsion of screen

TR = TopRow(Handle%)
TC = TopCol(HandleZ)

FOR i = 1 TO Rows (Handlez)


tempi$ = ''"
temp2$ = ""
FOR j = 1 TO Cols(Handtez)
temp1$ = temp1S + CHRS(SCREENC(TR + i - 1, TC + j - 1))
temp2$ = temp2$ + CHRSCSCREEN(TR + 1 - 1, TC + j - 1, 1))
NEXT j
OldText(Handlex, i) = temp1$
OldAttrb(Nandle%, 1) = temp2$
NEXT i

REM Now print the new text

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):

SUB WindowShow CHandle%)

> CurRow% = CSRLIN


CurCol% = POS(0)

REM Save the old section of screen

TR = TopRow(Handle%)
TC = TopCol(Handle%)

FOR ij = 1 TO Rows (Handte%)


;
temp1$ = ""
temp2$ = ""
FOR j = 1 TO Cols (Handte%)
temp1$ = temp1$ + CHRSCSCREENCTR + 4 - 1, TC + j= 1))
temp2$ = temp2$ + CHRSCSCREEN(TR + eA ete Fa
> Windows 53

NEXT j
OldText(Handle%, i) = temp1$
OldAttrb(Handlex, i) = temp2$
NEXT i

REM Now print the new text

LOCATE TR, TC ‘Start at top left of window

FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols (Handle)

CPut the character on the screen]

LOCATE TR + i = 1, TC + j ‘Locate next character position.


NEXT j
LOCATE TR + i, TC ‘Skip to next row
NEXT ji

Then, we set up the registers and use interrupt @H21 service 9:

SUB WindowShow (Handlez)

~ DIM InRegs AS RegType, OutRegs AS RegType

CurRow% = CSRLIN
CurCol% = POS(O)

REM Save the old sectsion of screen

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

REM Now print the new text

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%.

Setting flags — such as OnFlag( )—in a SHARED COMMON lets modules in a


program communicate with each other without having to pass parameters
back and forth through separate calls.

That’s it. The window is now visible. Listing 2-2 shows the whole subprogram
WindowShow( ).

Listing 2-2. WindowShow( ) Subprogram—Displays Windows.


DECLARE SUB WindowShow (Handle%)

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

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

Listing 2-2. WindowShow( ) Subprogram—Displays Tate folie


DIM InRegs AS RegType, OutRegs AS RegType
CurRow% = CSRLIN
CurCol% = POS(0)

REM Save the old section of screen

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

REM Now print the new text

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

REM Show window is on.

OnFlag(Handle%) = 1

LOCATE CurRow%, Curcol%

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.

Displaying a Test Window


them). First,
Here’s how to display two windows on the screen (there will be no text in
call WindowShow( )
we initialize the windows with WindowGetHandle%( ) and then we
with the appropriate handles:
56 P& Advanced BASIC

DECLARE FUNCTION WindowGetHandle% (TopRow%, TopCol%, NumRow%, NumCol%, Attr%)


DECLARE SUB WindowShow (Handle%)

Handle1% = WindowGetHandle%(10, 10, 5, 5, &H24)


Handle2% = WindowGetHandle%(20, 20, 3, 13, &H61)

CALL WindowShow(Handle1Z) <—


CALL WindowShow(Handle2%)

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)

DIM InRegs AS RegType, OutRegs AS RegType

END SUB
OWS > Windows 57
OF

Next we loop over the rows in the displayed window:

CurRow% = CSRLIN
CurColz% = POS(O0)

DIM InRegs AS RegType, OutRegs AS RegType

REM Print the old text

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)

DIM InRegs AS RegType, OutRegs AS RegType

REM Print the old text

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

Listing 2-3. WindowHide Subprogram—Hides Windows.


DECLARE SUB WindowHide (Handlez)

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

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)

DIM InRegs AS RegType, OutRegs AS RegType

REM Print the old text

LOCATE TR, TC
InRegs.cx = 1

FOR i = 1 TO Rows (Handle%)


FOR j = 1 TO Cols(Handte%)
InRegs.ax = &H900 + ASCCMIDSCOLdText(Handle%, i), j, 1))
InRegs.bx = ASCCMIDS(OLdAttrb(Handle%, 510 eatsORE Be
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR * 4 - 1, 1C + j
NEXT j
LOCATE TR + i, TC
NEXT i ;
REM Show window is off
ay OnFlag(Handle%) = 0

LOCATE CurRow%, CurCol%

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!

Hiding Our Test Window

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:

DECLARE FUNCTION WindowGetHandle% (TopRow%, TopCol%, NumRow%, NumCol%, Attr%)


DECLARE FUNCTION WindowPrint% CHandle%, Row%, Col%, PString$)
DECLARE SUB WindowShow (CHandlez)
DECLARE SUB WindowHide (Handle)

Handle1% = WindowGetHandle%(10, 10, 5,5, &H24)


Handle2% = WindowGetHandlex(10, 10, 3, 13, &H61)

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

Printing Text in a Window


Now let’s do something with the windows we've created. We can develop a subpro-
gram named WindowPrint( ) that prints text in them. This process is easy: all we really
need to specify is the position in the window at which to start printing (making [1,1] the
top left corner of the window), and the text we want to print.
Because a string may not fully fit in a window, there should also be some indication of
success or failure. Let’s make WindowPrint( ) a function: WindowPrint%( ). If it can print
the string passed to it in the window, it will return a value of 1; otherwise, it will return 0.

In general, if there is the possibility of failure inside a subprogram, you should


convert it to a function, which will let it report success or failure.

With that in mind, here’s how we might use WindowPrint%( ):

> IF WindowPrint% CHandle%, PrntRow%, PrntCol%, PrntStr$) THEN


PRINT “String printed in window successfully.”
ELSE : ‘
PRINT “Error. Could not print string in window."
END IF ; : :

|NOTE: | Note: We are both using WindowPrint%( ) and checking its return value in the
same line; this technique is reminiscent of programming in C.

Note the inputs in our code above:

Handle% INTEGER This window’s handle as returned from


WindowGetHandle%(_). Valid handles are
nonzero and in the range 1-8.
PrntRow% INTEGER The row in the window at which to start
printing. Use local window coordinates;
i.e., the top left corner is (1, 1).
PrntCol% INTEGER The column in the window at which to
start printing. Use local’ window coordi-
nates; i.e., the top left corner is (1, 1).
PrntStr$ STRING The string to be printed starting at location
(PrntRow%, PrmtCol%) in the window.
Ene a a i eh > did
Windows
eA 61

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.”

PRINT “Error. Could not print string in window."


END IF

This would generate the following output (see Figure 2-7).

Now is the time.

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):

FUNCTION WindowPrint% (Handle%, PrntRow%, PrntCol%, PrntStr$)

CurRow% = CSRLIN
CurCol% = POS(O)

WindowPrint% = 1

REM Make non-destructive copies

~ PrintRow% = PrnatRows
PrintCol% = ProtCol%
PrintString$ = Prntstrs

Now we can (optimistically) assign a return value of 1 — indicating success — to


WindowPrint% itself, and set up the loop over the window’s rows in which we'll insert
the characters.

pipes Success is usually indicated by non-negative values in programming.

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$ <>“”).

FUNCTION WindowPrints CHandle%, PrntRow%, PrntCol%, PrntStr$)

CurRows% = CSRLIN
CurCol% = POS{O0)

WindowPrint% = 1

REM Make non-destructive copies

PrintRow% = PrntRow%
PrintCol% = PrntCol%
PrintString$ = PrntStr$

as bO

LOOP WHILE (PrintRow% Rows(Handle%)) AND (PrintString$ <>")

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$ <> “”),

FUNCTION WindowPrint% (Handle%, PrntRow%, PrntCol%, PrntStr$)

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% = CSRLIN
CurCol% = POS(O)

WindowPrint% = 1

REM Make non-destructive copies

PrintRow% = PrntRow%
PrintCol% = PrntCol%
PrintString$S = PrntStr$

dO
> dO

CPut character in window]

PrintCol% = Printcol% + 1
LOOP WHILE (PrintCol% Cols(Handle%)) AND CPrintString$ <> "")

PrintRow% PrintRow% + 1
PrintCol% “ou 4

LOOP WHILE (PrintRow% Rows(Handle%)) AND (PrintStrings "")

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).

Now is the time


for all —

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( )):

FUNCTION WindowPrint% (Handle%, PrntRow%, PrntCol%, PrntStr$)

DIM InRegs AS RegType, OutRegs AS RegType

CurRows CSRLIN
CurCol% POS (0)
WindowPrint% = 1

REM Make non-destructive copies

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:

FUNCTION WindowPrint% (Handle%, PrntRow%, PrntCol%, PrntStr$)

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% = CSRLIN
CurCol% = POS(0)

WindowPrint% = 1

REM Make non-destructive copies

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

LOOP WHILE (PrintRow% <=Rows(Handte%)) AND CPrintString$<>"")

~ IF PrintString$S <>"" THEN WindowPrint% = 0

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.

Listing 2-4. WindowPrint% Function—Prints Text in a Window.


(Handle%, PrntRow%’, PrntCol%Z, Prntstr$)
DECLARE FUNCTION WindowPrint%

TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
66 P& Advanced BASIC

Listing 2-4. WindowPrint% Function—Prints Text in a Window.


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 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

FUNCTION WindowPrint% (Handle%, PrntRow%, PrntCol%, PrntStr$)


DIM InRegs AS RegType, OutRegs AS RegType

CurRow% CSRLIN
CurCol% Hof Post)

WindowPrint% = 1

REM Make non-destructive copies

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

LOOP WHILE (PrintRow% <= Rows(Handte%)) AND CPrintString$ <> "")


IF PrintString$S <> "" THEN WindowPrint% = 0

IF OnFlag(Handle)%
THEN
arn > Windows 67

Listing 2-4. WindowPrint% Function—Prints Text in a Window.


REM Print the new text

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

LOCATE CurRow%Z, CurColZ%

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.

Printing in Our Test Window


Here’s a sample program that puts WindowPrint%( ) to the test. We just print “Hello ”?

starting at (1,1) in the window, and “Test.” starting at (5,1):

DECLARE FUNCTION WindowGetHandle% (TopRow%, TopCol%, NumRow%, NumCol%, Attra’)


DECLARE SUB WindowShow (HandleZ%) y
DECLARE FUNCTION WindowPrint% (Handle%, Row%, Col%, PrintString$)

Handle% = WindowGetHandle%(10, 10, 5, 5, &H24)

CALL WindowShow(HandleZ)
68 Pb Advanced BASIC

Check% = WindowPrint%(Handle%, 1, 1, “Hello”)

Check% = WindowPrintz(Handle%, 5, 1, "Test.")

IF Check% = O THEN PRINT “Error."

The (Row, Column) coordinates we pass to WindowPrint%(_ ) are with respect


to the window — that is, (1, 1) is the upper left-hand corner of the window.

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 ;

Note the simple inputs to WindowMove%C ):


Handle% INTEGER This window’s handle as returned from
WindowGetHandle%(_ ). Valid handles are
non-zero and in the range 1-8.
NewTopRow% INTEGER The new screen row (1 - 24) of the top left
corner of the window after it’s been moved.
NewTopCol% INTEGER The new screen column (1 - 79) of the
topleft corner of the window after it’s been
moved.

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 NewTopRow% + Rows(€Handle%) > 25 OR NewTopCol% + Cols(Handle%) >80 THEN


WindowMove% = 0
EXIT FUNCTION
END IF
2
ry

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%))

< (BotRow(Handle%), BotCol(Handle%))


Figure 2-12
To refill those arrays looks like this:

WindowMove% = 1

IF NewTopRowZ < 1 OR NewTopCol% < 1 THEN


WindowMove% = 0
EXIT FUNCTION
END IF

IF eu ronkoud + Rows(Handlex) > 25 OR NewTopCol% + cols(Handtex)>80 THEN


WindowMove% = 0
EXIT FUNCTION.
END IF

— 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:

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% = CSRLIN
CurCol% = POS(0)

WindowMove% = 1

IF NewTopRow% < 1 OR NewTopCol% < THEN


WindowMove% = 0
EXIT FUNCTION
END IF

IF NewTopRow% + Rows(Handle%)> 25 OR NewTopCola% + Cols(Handle%)>80 THEN


WindowMove% = 0
EXIT FUNCTION
END IF

=> OldTopRow = TopRow(Handlez)


OldTopCol =TopCol(Handle%)
TopRow(Handle%) = NewTopRow%
TopCol(Handle%) = NewTopCol%
BotRow(Handle%) = NewTopRow% + Rows(Handle%)
BotCol(Handle%) = NewTopCol% + Cols(HandleZ)

IF OnFlag(Handle%) = O THEN EXIT FUNCTION


REM Restore old section of screen
> TR = OldTopRow
TC = OldTopCol
LOCATE TR, TC
InRegs.cx = 1
FOR i = 1 TO Rows(Handle%)
FOR j = 1 TO Cols(Handle%)
= &H900 + ASCCMILD$(OLdText(Handle%, i), j, 1))
InRegs.ax
InRegs.bx = ASCCMID$(OLdAttrb(Handte%, i), j, 1))
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR # 152 4, TC + 4
NEXT j
LOCATE TR + i, TC
NEXT i

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( ):

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% = CSRLIN
CurCol% = POSs(0)

WindowMovez = 1

IF NewTopRow% < 1 OR NewTopCol% < 1 THEN


WindowMove% = 0
EXIT FUNCTION
END IF

IF NewTopRow% + Rows(Handtez%) > 25 OR NewTopCol% + Cols(Handle%)>80 THEN


WindowMove% = 6
EXIT FUNCTION
END IF

OldTopRow = TopRow(Handle%)
OldTopCol =TopCol (Handle%)
TopRow(Handlez) = NewTopRow%
TopCol(CHandle%) = NewTopCot%
BotRow(Handlex) = NewTopRow% + Rows (Handle%)
BotColCHandle%) = NewTopCol% + Cols (HandleX)

IF OnFlagtHandteZ%) = QO THEN EXIT FUNCTION.

REM Restore old section of screen

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:

Listing 2-5. WindowMove% Function—Moves Windows on Screen. 1 of 3


DECLARE FUNCTION WindowMove% (Handle%, NewTopRow%, NewTopCol%)

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

DECLARE SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

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
/WindowB/ Attribute() AS INTEGER, OnFlag() AS INTEGER, _
COMMON SHARED
Text() AS STRING, OldText() AS STRING, OldAttrb() AS STRING

FUNCTION WindowMove% (Handle%, NewTopRow%, NewTopCol%)


DIM InRegs AS RegType, OutRegs AS RegType

CurRow% = CSRLIN
Curcol% = POSs(0)

WindowMove% = 1
IF NewTopRow% < 1 OR NewTopCol% < 1 THEN
WindowMove% = 0
EXIT FUNCTION
74 & Advanced BASIC

Listing 2-5. WindowMove% Function—Moves Windows on Screen. 2 of 3


END IF

IF NewTopRow% + Rows(Handle%) > 25 OR NewTopCol% + Cots(Handle%)>80 THEN


WindowMovez% = 0
EXIT FUNCTION
END IF

OldTopRow = TopRow(Handle%)
OldTopCol =TopCol(Handle%)
TopRow(Handte%) = NewTopRow%
TopCol(Handle%) = NewTopCol%
BotRow(Handlez) NewTopRow% + Rows (Handle%)
BotCot (Handle) NewTopCol% + Cols(€Handle%)

IF OnFlag(Handle%) = 0 THEN EXIT FUNCTION

REM Restore old section of screen

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

Listing 2-5. WindowMove% Function—Moves Windows on Screen. xe) ae)


CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + i= 1, TC + j
NEXT j
LOCATE TR + i, TC
NEXT ji

LOCATE CurRow%Z, CurCol%


END SUB

And that’s it; we’ve been able to move our window(s) around the screen. Let’s see this
in action.

Moving Our Test Window

Here’s an example program, showing how to use WindowMove%( ):

DECLARE FUNCTION WindowGetHandle% (TopRow%, TopCol%, NumRow%, NumCol%, AttrZ)


DECLARE SUB WindowShow (Handle%)
DECLARE FUNCTION WindowMove% (Handle%, NewTopRow%, NewTopCol%)

Handtet% WindowGetHandlez(10, 10, 5, 5, &H24)


Handle2z nou WindowGetHandle%(20, 20, 3, 13, &H61)

CALL WindowShow(Handle1%)
CALL WindowShow(Handle2Z)

PRINT “Press any key to move window 2.”


DO
LOOP WHILE INKEY$S = “"

Check = WindowMove%(Handle2%, 5, 20) «+

IF CheckZ% = 0 THEN PRINT “Error.”

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

DECLARE SUB WindowDelete (Handlez)

CurRow% = CSRLIN

CurCol% = POS(0)

DIM InRegs AS RegType, OutRegs AS RegType

REM Restore old text if necessary.


IF OnFlag(Handlex) THEN

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

LOCATE CurRow%, CurCol%

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

In other words, to delete a window, we just have to set Rows(Handle%) equal to 0,


where Handle% is the handle we've been passed. This is very easy — we only have to add
one line. Listing 2-6 shows the whole subprogram.

Listing 2-6. WindowDelete Subprogram—Deletes Window.


DECLARE SUB WindowDelete (CHandle%>

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 CIintNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

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

SUB WindowDelete CHandle%)


CurRowZ% = CSRLIN
CurCol% = POS(Q)

DIM InRegs AS RegType, OutRegs AS RegType

REM Restore old text if necessary.

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

Listing 2-6. WindowDelete Subprogram—Deletes Window. 2 of 2


FOR j = 1 TO Cols(Handle%)
InRegs.ax = &H900 + ASCCMIDSCOLdText(Handlex, i), j, 1))
InRegs.bx = ASCCMIDSCOLdAttrb(Handle%, jira Pop w)
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + i - 1, TC + j
NEXT j
LOCATE TR + 4, TC
NEXT i
END IF

LOCATE CurRow%, CurCol%

> Rows(Handlez) = 0 ‘WindowGetHandle checks only this.


END SUB

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.

Deleting Our Test Window


Here’s an example showing WindowDelete( ) in action:

DECLARE FUNCTION WindowGetHandle% (TopRow%, TopCol%, NumRow%, NumCol%, Attr%)


DECLARE SUB WindowShow (Handle%)
DECLARE SUB WindowDelete (HandleZ)

Handle1% = WindowGetHandle%(10, 10, 5, 5, &H24)


Handle2% = WindowGetHandle%(10, 10, 3, 13, &H61)

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

Windows in the BASIC PDS


The BASIC Professional Development System provides some window tools in its
BASIC ToolBox ready for use (if you own a copy of the PDS). To see how they work, let’s
put together a window with the word “Hello” in it, and flash it on the screen.
Microsoft provides some preassembled assembly language routines for extra speed in
displaying and hiding windows, and this code is in a file named UIASM.OBJ (which
comes with the PDS). If you’re using QBX.EXE, you'll have to put this .OBJ file into a
Quick library before using it; if you’re using BC.EXE (version 7.0 or later), you can put
this .OBJ file into a library file and link it in.
Let’s start with QBX.EXE. First, you have to create a Quick library, UIASM.QLB, like
this:

LINK /Q UIASM.OBJ QBX.LIB, UIASM.QLB,, QBXQLB.LIB;

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:

QBX WINDOWER /L UILASM

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.

WINDOWER.BAS—A PDS Window Program


Let’s write the program WINDOWER.BAS to put a window on the screen. We have to
start our program with a number of definition files, like this:

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"

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ Gloltem() AS MenultemType
COMMON SHARED /uitools/ GloWindow() AS windowType
COMMON SHARED /uitools/ GloButton() AS buttonType
COMMON SHARED /uitools/ GloEdit() AS EditFieldType
COMMON SHARED /uitools/ GloStorage AS WindowStorageType
COMMON SHARED /uitools/ GloWindowStack() AS INTEGER
COMMON SHARED /uitools/ GloBuffer$()

DIM GLoTitle(MAXMENU) AS MenuTitleType


DIM GLoltem(MAXMENU, MAXITEM) AS MenultemType
DIM GloWindow(MAXWINDOW) AS windowType
DIM GloButton(MAXBUTTON) AS buttonType
DIM GlLoEdit (MAXEDITFIELD) AS EditFieldType
DIM GloWindowStack(MAXWINDOW) AS INTEGER
DIM GloBufferS(MAXWINDOW + 1, 2)
82 b> Advanced BASIC

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'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ GloItem() AS MenultemType
COMMON SHARED /uitools/ GloWindow() AS windowType
COMMON SHARED /uitools/ GloButton() AS buttonType
COMMON SHARED /uitools/ GloEditt) AS EditFieldType
COMMON SHARED /uitools/ GloStorage AS WindowStorageType
COMMON SHARED /uitools/ GloWindowStack() AS INTEGER
COMMON SHARED /uitools/ GloBuffer$()

DIM GloTitleCMAXMENU) AS MenuTitleType


DIM Gloltem(MAXMENU, MAXITEM) AS MenultemType
DIM GloWindow(MAXWINDOW) AS windowType
DIM GLoButton(MAXBUTTON) AS buttonType
DIM GLoEdit (MAXEDITFIELD) AS EditFietdType
DIM GloWindowStack(MAXWINDOW) AS INTEGER
DIM GloBufferS(MAXWINDOW + 1, 2)
CALL Menulnit
CALL WindowInit
CALL MouseShow

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

Sizewin% If TRUE, window can be resized


Modalwin% TRUE means that selecting outside the window
results in a beep
Borderchar% 0 — No border
1 > Single line border
2 — Double-line border
Title$ Title of the window, “” — no title displayed
And here are the values we'll use for our window:

" SINCLUDE: 'C:\BC7\WINDOW.BI'


" SINCLUDE: 'C:\BC7\MENU.BI'
* SINCLUDE: ‘C:\BC7\MOUSE.BI'
" S$INCLUDE: 'C:\BC7\GENERAL.BI'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ GloItem() AS MenultemType
COMMON SHARED /uitools/ GloWindow() AS windowType
COMMON SHARED /uitools/ GloButton() AS buttonType
COMMON SHARED /uitools/ GloEdit() AS EditFieldType
COMMON SHARED /uitools/ GloStorage AS WindowStorageType
COMMON SHARED /uitools/ GloWindowStack() AS INTEGER
COMMON SHARED /uitools/ GloBuffer$()

DIM GloTitle(MAXMENU) AS MenuTitleType


DIM GloIltem(MAXMENU, MAXITEM) AS MenultemType
DIM GloWindow(MAXWINDOW) AS windowType
DIM GloButton(MAXBUTTON) AS buttonType
DIM GLoEdit (MAXEDITFIELD) AS EditFieldType
DIM GloWindowStack(MAXWINDOW) AS INTEGER
DIM GloBufferS(MAXWINDOW + 1, 2)

CALL MenulInit
CALL WindowInit
CALL MouseShow

WindowOpen(1, 10, 20, 15, 30, 2, 1, 2, 1, 15, TRUE, TRUE, TRUE,


CALL
TRUE, O,-.°"?

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

Listing 2-7. Using Windows in BASIC PDS.


SINCLUDE: 'C:\BC7\WINDOW.BI'
SINCLUDE: 'C:\BC7\MENU.BI'
SINCLUDE: 'C:\BC7\MOUSE.BI'
SINCLUDE: 'C:\BC7\GENERAL.BI'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ Gloitem() AS MenultemType
COMMON SHARED /uitools/ GloWindow() AS windowType
COMMON SHARED /uitools/ GloButton() AS buttonType
COMMON SHARED /uitools/ GloEdit() AS EditFieldType
COMMON SHARED /uitools/ GloStorage AS WindowStorageType
COMMON SHARED /uitools/ GloWindowStack() AS INTEGER
COMMON SHARED /uitools/ GloBuffer$()

DIM GloTitleCMAXMENU) AS MenuTitleType


DIM GloIltemC(MAXMENU, MAXITEM) AS MenultemType
DIM GloWindow(MAXWINDOW) AS windowType
DIM GloButton(MAXBUTTON) AS buttonType
DIM GloEdit (MAXEDITFIELD) AS EditFieldType
DIM GloWindowStack (MAXWINDOW) AS INTEGER
DIM GloBufferSCMAXWINDOW + 4, 2)
CALL MenulInit
CALL WindowlInit
CALL MouseShow

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|

wei by“Wh nbow?


re08
>The Mouse 87

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%/( ) initializes the mouse driver software that you loaded


above.

In many programs, use of a mouse is optional and depends on whether or not


a mouse and mouse driver are installed. You should know that, even if a
mouse is optional, you can call and use the mouse functions and subprograms
here without a problem. If there is no mouse, you'll simply see no mouse
“events” like cursor movements or button presses. (And you should make
sure you don't loop forever, waiting for such events.) In other words, using the
mouse subprograms and functions does no harm if there is no mouse.

Initializing the Mouse


Initializing the mouse is a necessary first step towards using the mouse. We can do that
with interrupt @H33, service 0. If this service returns a nonzero value, the mouse is
initialized; otherwise, the mouse cannot be used (because it’s not installed in the com-
puter or the mouse driver is missing). In that case, and if your program depends on the
use of a mouse, you should print out an error message and quit.
Once the mouse is initialized, we’re all set until the computer is turned off — we
don’t have to initialize it again (although doing so does no harm). Note that initializing
the mouse does not display the mouse cursor: to display the cursor use the subprogram
we'll develop later, named MouseShowCursor( ).

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:

DIM InRegs AS RegType, OutRegs AS RegType


— InRegs.ax = 0
CALL INTERRUPT(&H33, InRegs, OutRegs)
MouseInitialize% = OutRegs.ax
> The Mouse 89

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:

Listing 3-1. Mouselnitialize% ( ) Function.


DECLARE FUNCTION MouselInitializex ()

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

Mouse Initializing Window


Here’s how to use Mouselnitialize%( ):

REMExample of MouselInitialize

DECLARE FUNCTION MouseInitializez ()

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

Making the Mouse Cursor Visible


The next step in using the mouse is to show the mouse cursor on the screen. In text
mode, this cursor appears as a solid block (although you can change that with MouseSet-
Cursor( ), developed later in this chapter), and in graphics mode as an arrow. Let’s write a
small subprogram named MouseShowCursor( ) to do the work for us.

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:

DIM InRegs AS RegType, OutRegs AS RegType


~ InRegs.ax = 1
CALL INTERRUPTC(&H33, InRegs, OutRegs)

When you use MouseShowCursor( ), the cursor appears on the screen. Listing 3-2
shows the whole subprogram.

List 3.2. MouseShowCursor ( ) Subprogram.


DECLARE SUB MouseShowCursor ( )
TYPE Reg
Type
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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs


AS RegTtype)

SUB MouseShowCursor

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)
END SUB
gaa EO >The
agra, Mouse =
UR UOUSO 91

Using MouseShowCursor( )
Here’s how to put MouseShowCursor( ) to work:

REM Example of MouseShowCursor

DECLARE FUNCTION MouseInitialize% ()


DECLARE SUB MouseShowCursor ( )

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.

Hiding the Mouse Cursor


Now we'll hide the mouse cursor. There are times when the mouse cursor can be a
distraction on the screen, and we'll fix that problem here.

|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.

In fact, if you look through the menu programs carefully in Chapter 4,


Pull-down Menus, you'll see where we have to do exactly this to avoid the
problem.

Let’s write the subprogram to hide the mouse cursor, MouseHideCursor( ).

MouseHideCursor( )

Here we just make use of interrupt @H33 service 2, which hides the mouse cursor:

DIM InRegs AS RegType, OutRegs AS RegType


— InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

Listing 3-3 shows what the whole subprogram looks like, ready to roll.

Listing 3.3 MouseHideCursor Subprogram. se) a2


DECLARE SUB MouseHideCursor ( )

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

Listing 3.3 MouseHideCursor Subprogram. 2 of 2


DECLARE SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)
SUB MouseHideCursor

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

END SUB

Using MouseHide Cursor )


The example program show in Listing 3-4 displays the mouse cursor and then hides it
after you press any key:

Listing 3-4. MouseHide Cursor Program.


REM Example of MouseHideCursor

DECLARE FUNCTION MouseInitialize% ()


DECLARE SUB MouseShowCursor ()
DECLARE SUB MouseHideCursor ()
Check% =MouselInitializez

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

Reading Immediate Mouse Information


There is one way of getting information from the mouse — interrupt @H33 service 3
returns the immediate status of the left and right buttons, as well as the row and column
number of the mouse cursor’s position. When you call it, it returns this information
encoded in the bx, cx, and dx registers, providing us with a snapshot of what the mouse
is doing now.
Here’s how the registers are set on return:

bx 0 = no button down; 1 = right button down; 2 = left button down, 3 =


both buttons down
CX Screen column of mouse cursor (using pixel ranges)
dx Screen row of mouse cursor (using pixel ranges) Let’s write a subpro-
gram to report these things to us.

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% O = Right mouse button is up; 1 = Right mouse button is down


Left% 0 = Left mouse button is up; 1 = Left mouse button is down
Row% Current text-mode screen row of mouse cursor (Range | - 25)
Col% Current text-mode screen column of mouse cursor (Range 1 -
80)

This subprogram is designed to return a snapshot of the mouse’s present state. As we


saw, interrupt @H33 service 3 returns button information in OutRegs.bx; if this value is
1, the left button only is down. If it’s 2, the right button only is down. If 3, both are
down. (And if 0, neither are down.) Here’s how we can decode that information into the
parameters Right% and Left%:
ee
eee eee ae eR > Dd
Thebh
Mouse 95
hed

SUB MouseInformation (Right%, Left%, Row%, Col%)

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = 3
CALL INTERRUPT(&H33, InRegs, OutRegs)

Right% = 0
Left% = 0

> SELECT CASE OutRegs.bx


: CASE 1
: Left% = 1
CASE 2
Right% = 1
CASE 3
Leftz% = 1
Right% = 1
END SELECT

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:

SUB MouseInformation (Right%, Left%, Row%, Col%)

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = 3
CALL INTERRUPT (&H33, InRegs, OutRegs)

Right% = 0
Left% = 0

SELECT CASE OutRegs.bx


CASE 1
Left% = 1
CASE 2
Right% = 1
CASE 3
Left% = 1
Right% = 1
END SELECT

3 Row% Out Regs.dx 8 + 1


Colt% Out Regs.cx 8 + 1

And we're set. Listing 3-5 shows the whole subprogram.


96 P Advanced BASIC

ES dale corommmKeLUL-y-m lalcolguar-ltcelametele)elgelele-111m


DECLARE SUB MouseInformation (Right%, Left%, Row%Z, Col%)

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

SUB MouseInformation (Right%, Left%, Row%, Col%)

DIM InRegs AS RegType, OutRegs AS Reg Type


InRegs.ax = 3
CALL INTERRUPTC(&H33, InRegs, OutRegs)

Right% = 0
Left% = 0

SELECT CASE OutRegs .bx


CASE 1
Left% = 1
CASE 2
Rightz% = 1
CASE 3
Left% = 1
Right% = 1
END SELECT

Row% = OutRegs.dx
Col% = OutRegs.cx OO ee a
00

END SUB

Finding the Mouse’s Current State


This example program just reports the mouse state when you press a key:

REM Example of MouseInformation

DECLARE FUNCTION Mouselnitialize% ()


DECLARE SUB MouseShowCursc;: ()
DECLARE SUB MouseInformation (Right%, Left%, Row%, Col%)

Check% = MouseInitialize%

LOCATE 1, 1
IF Check% = 0 THEN
PRINT "Mouse driver not installed."
ELSE
>The Mouse 97

PRINT “Mouse Initialized."


CALL MouseShowCursor
PRINT “Position the mouse and press any key."
DO
LOOP WHILE INKEY$ = ""
CALL MouseInformation (Right%, Left%, Row%, Col%)

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

PRINT "Row: ", Row%, “Column: “, Col%

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.

Moving the Mouse Cursor


With service 4, we'll gain control over the mouse cursor. Up to this point, the only way
to make the mouse cursor move was to move the mouse (as soon as you show the mouse
cursor, it responds to mouse movements). However, we can develop a subprogram
named MouseMoveCursor( ) to position the mouse cursor as we want it.

MouseMoveCursor( )
It would be simplest if we could position the mouse by just passing the desired screen
row and column numbers like this:

CALL MouseMoveCursor (Row%, Col%)

To write MouseMoveCursor( ), we'll just make use of interrupt Q@H33 service +. We


have to pass the mouse cursor’s new location in dx (rows) and cx (columns), Because we
are using text-mode rows and columns, we first convert that location into pixel ranges
like this:

DIM InRegs AS RegType, OutRegs AS RegType

— 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

Listing 3-6. MouseMoveCursor Subprogram.


DECLARE SUB MouseMoveCursor (CRow%, Col%)

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

SUB MouseMoveCursor (Row%, Col%)

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.dx 8 * (Row% - 1)
InRegs.cx 8 * (Colz% - 1)
InRegs.ax 4
CALL INTERRUPTC& H33, InRegs, OutRegs)

END SUB

Moving to the Top Left of the Screen


In this example, we move the mouse cursor to screen position (1, 1) after you press a
key:

REM Example of MouseMoveCursor

DECLARE FUNCTION MouselInitializez ()


DECLARE SUB MouseShowCursor ()
DECLARE SUB MouseMoveCursor (Row%, Col%)

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.

Reading the Button Pressed Queue


It would be useful to develop the mouse equivalent of INKEY$. We already have
MouseInformation( ), but you must catch mouse events as they happen to use it. Instead,
it would be much better if they could be stored and we could read them as we require
them.
We can do that with service 5. This service lets us read the number of times a specific
button has been pressed since we last checked. It also gives you the row and column
screen position of mouse cursor the J/ast time the button was pressed. Pressing a mouse
button is usually more significant than just moving the mouse cursor around the screen.
For that reason, you can treat this service as the primary mouse input routine.
Let’s write a subprogram called, say, MouseTimesPressed( ), to connect service 5 to
BASIC. We should call MouseTimesPressed( ) when we start accepting input to clear the
mouse buffer, then loop over and call it periodically to see if anything else has happened,
much like INKEY$.

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:

CALL MouseTimesPressed (Button%, NumberTimes%, Row%, Col%)


> The Mouse 101

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:

SUB MouseTimesPressed (Button%, NumberTimes%, Row%, Col%)

DIMInRegs AS RegType, OutRegs AS RegType

— 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:

SUB MouseTimesPressed (Button%, NumberTimes%, Row%z, Col%)

DIM InRegs AS RegType, OutRegs AS RegType

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:

Listing 3-7. MouseTimesPressed Subprogram.


SUB MouseTimesPressed (Buttonz, NumberTimes%, Row%, Col”)
DECLARE
REM Button% should be 0 to return Left button info, 1 for right.

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

SUB MouseTimesPressed (Button%, NumberTimes%, Row%, Col%)

DIM InRegs AS RegType, OutRegs AS RegType

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

And now we can put it to work.

Counting Button Presses


This example program shows MouseTimesPressed( ) at work. All it does is to ask you
to press the right mouse button a number of times and then press any key on the
keyboard. When you do, this program reports the number of times you’ve clicked the
button, and the last position at which you did so.
> The Mouse 103

REM Example of MouseShowCursor

DECLARE FUNCTION MouselInitialize% ()


DECLARE SUB MouseShowCursor ()
DECLARE SUB MouseTimesPressed (Button%, NumberTimes%, Row%, Col%)

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.

Reading the Button Released Queue


We can use service 6 to write a subprogram that will give us button release informa-
tion; let’s call this subprogram MouseTimesReleased( ). It should give us information
about the number of times a particular button was released since we called it, and the
screen position of the mouse cursor when it was last released.
104 P Advanced BASIC

MouseTimesReleased ( )
Here’s how we might call MouseTimesReleased( ):

CALL MouseTimesReleased (Button%, NumberTimes%, Row%, Col%)

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.

This subprogram is very like MouseTimesPressed( ), except that we use interrupt


&H33 service 6, not 5:

DIM InRegs AS RegType, OutRegs AS RegType

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( ):

DIM InRegs AS RegType, OutRegs AS RegType

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

And that’s it. Listing 3-8 shows the whole subprogram.

Listing 3-8. MouseTimesReleased Subprogram.


DECLARE SUB MouseTimesReleased (Button%, NumberTimes%, Row%, Col%)

REM Button should be O to return Left button info, 1 for right.

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

SUB MouseTimesReleased (Button%, NumberTimes%, Row%, Col”)

DIM InRegs ASRegType, OutRegs AS RegtType

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

Now let’s put it to work!

Counting Button Releases


This example program is just like the one for MouseTimesPressed( ), except that it
indicates the number of times the button was released, not pressed:

REM Example of MouseShowCursor

DECLARE FUNCTION MouseInitialize% ()


106 P& Advanced BASIC

DECLARE SUB MouseShowCursor ()


DECLARE SUB MouseTimesReleased (Button%, NumberTimes%z, Rowz, Col%)

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%)

PRINT “You released it ";NumberTimes%; “times.”


PRINT “The Last time was at (";Row%Z;",";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.

Restricting the Mouse Horizontally


As mentioned, service 7 restricts the mouse cursor, and therefore mouse events, to a
specified range of columns. We can put together a subprogram to interface with this
service like this:

CALL MouseHorizontalRange (Right%, Left%) ,


> The Mouse 107

Where we fill the variables like this:

Right% Right column of allowed mouse cursor range. (Range: 1-80)


Left% Left column of allowed mouse cursor range. (Range: 1-80)

Now all that remains is to write the code.

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:

DIM InRegs AS RegType, OutRegs AS RegType

— 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.

Listing 3-9. MouseHorizontalRange Subprogram.


DECLARE SUB MouseHorizontalRange (Right%, Left%)

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

SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)


DECLARE

SUB MouseHorizontalRange (Right%, Left%)

DIM InRegs AS RegType, OutRegs AS RegType

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

Now let’s see it in action.

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:

REM Example of MouseHorizontalRange

DECLARE FUNCTION MouseInitializes ()


DECLARE SUB MouseShowCursor ()
DECLARE SUB MouseHorizontalRange (Right%, Left%)

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

Restricting the Mouse Vertically


After MouseHorizontalRange( ), MouseVerticalRange( ) is the logical next subpro-
gram. It restricts the mouse cursor to a specified vertical range of screen rows (1-25).
Together, these two subprograms can restrict mouse operation to a specific window of
your choosing. Here’s the way we might use it:

CALL MouseVerticalRange
(Top%, Bottom%)

where these are the inputs:

Top% Top row of allowed mouse cursor


movement (Range: 1-25)
Bottom% Bottom row of allowed mouse cursor
movement (Range: 1-25)

Developing this subprogram will be easy.

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:

DIM InRegs AS RegType, OutRegs AS RegType


— InRegs.cx = 8 * (Top% - 1)
InRegs.dx = 8 * (Bottom% - 1)
InRegs.ax = 8
CALL INTERRUPT(&H33, InRegs, OutRegs)

That’s all there is to it. Listing 3-10 shows the ready-to-use subprograms.

Listing 3-10. MouseVerticalRange ( ) Subprogram. sie) a2


DECLARE SUB MouseVerticalRange (Top%, BottomZ)

TYPE RegType
ax AS INTEGER
110 PB Advanced BASIC

Listing 3-10. MouseVerticalRange ( ) Subprogram. Ve) 72


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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

SUB MouseVerticalRange (Top%, Bottom%)


DIM InRegs AS RegType, OutRegs AS RegType

InRegs.cx = 8 * (Top% - 1)
InRegs.dx = 8 * (Bottom% - 1)
InRegs.ax = 8
CALL INTERRUPT(&H33, InRegs, OutRegs)
END SUB

Let’s see an example.

MouseVerticalRange ( ) at Work
In this program, we restrict the mouse cursor to the top half of the screen by calling
MouseVerticalRange(12, 1):

REM Example of MouseVerticalRange

DECLARE FUNCTION Mouselnitialize% ¢)


DECLARE SUB MouseShowCursor ( )
DECLARE SUB MouseVerticalRange (Bottom%, Topz)

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.

Setting the Mouse Cursor


Service QHA lets us set the text-mode (only) mouse cursor to whatever ASCII charac-
ter we want. To use it, we have to define two “masks”: the screen mask and the cursor
mask. Each mask consists of both a character and attribute byte, and understanding how
to use them takes some time and experimentation.

How to Use Screen and Cursor Masks

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:

(SMaskChar%, SMaskAttr2Z, CMaskChar%, CMaskAttr2)


CALL MouseSetCursor

Here are the inputs:

SMaskChar% Screen mask ASCII character


112 Advanced BASIC

SMaskAttr% Screen mask attribute; the screen


mask character and attribute are
ANDed with the character and
attribute already the on the screen.
For example, to overwrite the
character on the screen, set
SMaskChar% = SMaskAttr% = 0. To
preserve it, SMaskChar% =
SMaskAttr% = QHFE
CMaskChar% INTEGER Cursor mask ASCII character
CMaskAttr% INTEGER Cursor mask attribute; the cursor
mask character and and attribute are
XORed with the result of ANDing the
character at the present position with
the screen mask. That is, the cursor
mask determines the shape of the
mouse cursor.

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:

DIM InRegs AS RegType, OutRegs AS RegType

REM Watch out for overflow as we load the attribute into the high byte

—> IF SMaskAttr% AND 2°77 THEN


TempS% = -1 XOR &H7FFF "Set top bit
ELSE
Temps% = 0
END IF

The number -1 is stored in INTEGER format as @HFFFF (again, see Chapter 11 to


find out why). To leave just the top bit set, we XOR that with @H7FFE Then we do the
same for InRegs.dx and CMaskAttr%, producing TempC% for the cursor mask like this:

DIM InRegs AS RegType, OutRegs AS RegType

out for overflow as we load the attribute into the high byte
REM Watch

AND 2°7 THEN TempS% = -1 XOR G@H7FFF ‘Set top bit


IF SMaskAttr%
ELSE
Temps% = 0
END IF

+ IF CMaskAttr% AND 2°7 THEN


114 b> Advanced BASIC

TempC% = -1 XOR &H7FFF "Set top bit


ELSE
Tempc% = QO
END IF

Finally, we can load the masks into their registers like this:

DIM InRegs AS Regtype, OutRegs AS RegType

REM Watch out for overflow as we load the attribute into the high byte

IF SMaskAttr% AND 2°7 THEN


TempS% = -1 XOR &H7FFF'Set top bit
ELSE
Temps% = 0
END IF

IF CMaskAttr% AND 2°7 THEN


TempC% = -1 XOR &H7FFF "Set top bit
ELSE
Tempc% = 0
END IF

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.

Listing 3-11. MouseSetCursor ( ) Subprogram. ie) a


DECLARE SUB MouseSetCursor (SMaskCharZ, SMaskAttr%, CMaskChar%, CMaskAttr%)
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
& The Mouse 115

Listing 3-11. MouseSetCursor ( ) Subprogram. 2 of 2


dx AS._ INTEGER
bp _AS_ INTEGER
si _—AS:_“ INTEGER
di AS_ INTEGER
flags AS INTEGER
END TYPE

DECLARE SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

SUB MouseSetCursor (SMaskChar%, SMaskAttr%, CMaskChar%, CMaskAttr%)

DIM InRegs AS RegType, OutRegs AS RegType

REM Watch out for overflow as we load the attribute into the high byte

IF SMaskAttr% AND 2°7 THEN


TempS% = -1 XOR &H7FFF "Set top bit
ELSE
TempsSz% = 0
END IF

IF CMaskAttr% AND 2°7 THEN


TempC% = -1 XOR &H7FFF "Set top bit
ELSE
Tempctz% = 0
END IF

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.

Making the Mouse Cursor into a Dot


Here’s what MouseSetCursor( ) looks like in action:

example (ScMaskChardz, ScMaskAtr%, CurMaskChar%, CurMaskAtrz)


REM MouseSetCursor

DECLARE FUNCTION MouseInitialize% C2


DECLARE SUB MouseShowCursor ( )
(ScMaskCharz, ScMaskAtr%, CurMaskChar%, CurMaskAtr’)
DECLARE SUB MouseSetCursor

Check% = MouseInitialize%

LOCATE 1, 1
116 Pb Advanced BASIC

IF Check% = 0 THEN :
PRINT "Mouse driver not installed.”
ELSE
PRINT “Mouse Initialized."
CALL MouseShowCursor

REM Change mouse cursor to a single dot (ASCII 250)

CALL MouseSetCursor(0, 0, 250, 7)


END IF

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:

ASCII Number Character


24 Up arrow
25 Down arrow
26 Right arrow
27 Left arrow

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.

The Mouse in the BASIC PDS


The support for the mouse in the Professional Development System is not very
strong — in fact, it’s considerably weaker than what we've already developed.
>The Mouse 117

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%):

(rowl%, coll%) >

€ (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:

DECLARE SUB MouselInit ( )

CALL MouselInit

Then we show the mouse cursor:

DECLARE SUB MouseInit ()


DECLARE SUB MouseShow ()
CALL MouselInit
CALL MouseShow
118 Pb Advanced BASIC

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:

DECLARE SUB MouseInit ( )


DECLARE SUB MousePoll (row%, col%, \Button%, rButton%)
DECLARE SUB MouseShow ( )

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:

DECLARE SUB MouseInit (¢ )


DECLARE SUB MousePoll (row%, col%, LButton%, rButton%)
DECLARE SUB MouseShow (¢ )

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 @

ai "171s TL 4)daoa 7 tal a |


ae Sh ord Pia
, i Dy; TY b«Alth i fo a' s
Apelbeatt
ait
F
’ 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.

|NOTE: | This is an example of modular programming.

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.

Designing Our Menu System


We first have to consider what we want our menu system to look like on the screen.
The menubar, with the screen attribute we’ve chosen for it, should appear at the top of
the screen. If the user has a mouse, he or she should be able to simply click (left mouse
button, as is usual for menus) on a menu name in the menu bar. That menu then pops
open beneath it, with the screen attribute we’ve chosen for it (we should be able to pick a
different attribute for each menu and the menubar). On the screen, opening menu 1
might look like Figure 4-1.

MenuBar my

[Menut [Menu 2 [Menu3 [Mowe


Choice 1
Choice 2
Choice 3 < Menu 1
Choice 4
Choice 5
Choice 6
Figure 4-1
124 Pb Advanced BASIC

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.

Special Menu Considerations


Now let’s think about writing the code. From a programming standpoint, we want to
get our menus running with the minimum amount of trouble. We should have an initial-
ization program — MenulInitialize( ) — to set up the labels in the menubar and menus
themselves.
To show the menubar, we could have another subprogram, which we can call Menu-
Show( ). To hide it again (we may not always want the menu system to be on), we can
add MenuHide( ).
Now comes the tricky part: How do we get information about what menu selections
were made? It would be best if the menu system could somehow run independently of
our program and interrupt us when a menu choice was made. Unfortunately, that’s
beyond BASIC’s capabilities. You can’t have two program groups working at once under
DOS BASIC.
A practical solution would be to have the menu system provide all the input for our
program. In other words, whenever we needed any input, we could call a function
named, say, MenuGetEvent$( ). This could act like INKEY$, MouseTimesPressed( ),
MouseTimesReleased( ) and a menu input program all at once. If any keyboard, mouse,
or menu event were pending, MenuGetEvent$( ) would let us know what it was (which
means that we should check it frequently). :
We can set it up like this: if MenuGetEvent$() returns a string that is one or two
characters long, a character was typed, and we should treat the string just as you would a
string from INKEY$. If there was a mouse event, the string will be either mouseup or
> Pull-down Menus 125

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.

Initializing Our Menus


Using Menulnitialize%(_) is the necessary first step towards using menus. We want our
menu system to look like Figure 4-2.
MenuBar ai

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).

[Frits [Menu ® [Mena [Mena


Choice 1
Choice 2
Choice 3
Choice 4
Choice 5
Choice 6
Figure 4-3
The next menu name comes from MenuNames(2) and so on. The limits on the num-
ber of entries in MenuNames( ) and the size of each entry are set by the screen width and
the menu width. A maximum of eight characters for each menu name, and a maximum of
seven menus turns out to be a good compromise. Here’s the call to Menulnitialize( ) so
fats

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”

This would make menu 1 look like Figure 4-4.

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”

This would make it look like Figure 4-5.

[Fruits “| Menu2 [Menus © [Menu4


Peas
Corn
Broccoli
Figure 4-5
We also have to establish limits here on both string size and the number of menu
choices; a maximum of 15 characters in a menu choice, and a maximum of 24 choices (or
else the menu wouldn't fit on the screen), works pretty well.
128 » Advanced BASIC

This is how the call looks so far:

CALL MenuInitialize%(MenuNames( ), MenuChoices( )...)

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:

CALL MenuInitialize%(MenuNames( ), MenuChoices( ), BarAttrb%...)

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%( ):

CALL Menulnitialize%(MenuNames( ), MenuChoices( ), BarAttrb%, MenuAttrbs( ))

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).

Now let’s write Menulnitialize%(_).

Menulnitialize%( )
Here are the inputs we’ve worked out for Menulnitialize%( ):
> Pull-down Menus 129

MenuNames() STRING A one-dimensional array of menu name


strings. For example, MenuNames(1) might
equal File, so the first entry in the menubar
will read File. Each string can be a maximum
of eight characters long, and there is a max-
imum of seven strings (and hence seven
menus).
MenuChoices(.) STRING A two-dimensional array of menu choice
strings. For example, if MenuChoices(1,1) is
Open File..., then the first choice in the first
menu will be Open File.... Each choice can be
a maximum of 15 characters long. There is a
maximum of 24 choices per menu (otherwise
the menu won't fit on the screen).
BarAttrb% INTEGER The screen attribute of the menubar.
MenuAttrbs(_ ) INTEGER A one-dimensional array of screen attributes
for the menus themselves. For example, Menu-
Attrbs(1) is the screen attribute the menu sys-
tem is to use with the first menu.
Menulnitialize%(_) should return 1 if the menus were successfully set up, and 0 other-
wise. Like the window system, it will take some work to set things up.
First, let’s put together the COMMONS we'll be using. We can begin with the dimen-
sions of each menu. To make things easier for ourselves, we can treat each menu like a
window, and adapt some of our window code to display it. Thinking of the set of menus
as a set of windows, then, we start off like this:

COMMON SHARED /MenuA/ Rows( ) AS INTEGER, Cols€) AS INTEGER, _


TopRow( ) AS INTEGER, TopCol() AS INTEGER, _
BotRow( ) AS INTEGER, BotCol( ) AS INTEGER

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

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, Be
OldText( ) AS STRING, OldAttrb( ) AS STRING

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%:

COMMON SHARED /MenuA/ Rows ) AS INTEGER, Cots( ) AS INTEGER, te


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$

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:

FUNCTION Menulnitializex (MenuNames( ) AS STRING, MenuChoices( ) AS STRING, _


BarAttrb%’, MenuAttrbs( ) AS INTEGER)

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:

FUNCTION MenuInitializeX (MenuNames( ) AS STRING, MenuChoices( ) AS STRING,


BarAttrb%, MenuAttrbs( ) AS INTEGER)

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):

FUNCTION MenuInitialize% (MenuNames( ) AS STRING, MenuChoices( ) AS STRING, _


BarAttrb%, MenuAttrbs( ) AS INTEGER)

NumMenus% = UBOUND(MenuNames, 1)
IF NumMenus% > 7 THEN EXIT FUNCTION
132 Pb Advanced BASIC

IF UBOUND(MenuChoices, 2) > 24 THEN EXIT FUNCTION

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.

FUNCTION MenuInitializez (MenuNames( ) AS STRING, MenuChoices( >) AS STRING, os


BarAttrb%, MenuAttrbs( ) AS INTEGER)

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 :

Bar$S = Bar$S + SPACES$(80 - LEN(Bar$))

MenuInitialize% = 1

That’s it for Menulnitialize%(_). Listing 4-1 shows the whole function.

Listing 4-1. Menu Initialize% Function — Initializes Menu System. 108 2


DECLARE FUNCTION MenuInitializez (MenuNames( ) AS STRING, MenuChoices() AS _
STRING, BarAttrb%, MenuAttrbs( ) AS INTEGER)

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

Listing 4-1. Menu Initialize% Function — Initializes Menu System. 2 of 2


FUNCTION MenuInitializez (MenuNames( ) AS STRING, MenuChoices( ) AS STRING, _
BarAttrb%, MenuAttrbs( ) AS INTEGER)

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

Bar$S = Bar$S + SPACE$(80 - LEN(Bar$))

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.

Initializing a Test Menubar


1

Here's an example showing how to set up a menubar as we've designed


it with three
menu names in it:
> Pull-down Menus 135

DECLARE FUNCTION MenuInitializezx (MenuNames( ) AS STRING, MenuChoices( ) AS


STRING, BarAttrb%, MenudAttrbs( ) AS INTEGER)

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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

MenulInitialize%(MenuNames(), MenuChoices(), BarAttrb%,


= _
Check%
MenuAttrbs( ))
IF Check% = O THEN
PRINT “Error.”
ELSE
PRINT "Menu is set up.“
END IF

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.

Displaying the Menubar


Now we're ready to write MenuShow( ) and display the menubar across the top of the
the
screen. In addition, this subprogram clears any mouse events that were waiting in
mouse queue (so that when you get menu input, you'll be responding to what occurred
after the menubar appeared). Note that to make the menubar active after you've displayed
it, you have to call MenuGetEvent$( ) and then wait for input.
it
Before we do anything, we have to take the mouse cursor off the screen (we'll restore
136 > Advanced BASIC

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

DIM InRegs AS RegType, OutRegs AS RegType

CurRows = CSRLIN
CurCol% = POSCO)

REM Turn off mouse cursor

> 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

DIM InRegs AS Reglype, OutRegs AS RegType

CurRow% = CSRLIN
CurCol% Pos (0)

> REM Save the old top bar

: 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( ):

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% CSRLIN
CurCol% POS(0)

REM Save the old top bar

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

REM Now print the menu bar

|
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:

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% CSRLIN
CurCol% POs (0)

REM Save the old top bar

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$

REM Now print the menu bar

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)

REM Show menu bar is on.

MenusOnFlag% = 1

LOCATE CurRow%, CurCol%

REM Clear the mouse queue

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 (¢ )

TYPE Reg Type


ax AS INTEGER
bx AS INTEGER
cx INTEGER
dx INTEGER
bp INTEGER
si INTEGER
di INTEGER
flags INTEGER
END TYPE
> Pull-down Menus’ 139

Listing 4-2. MenuShow ( ) Subprogram—Displays the Menubar.


DECLARE SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

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

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% CSRLIN
CurCol% Pos(Q)

REM Turn off mouse cursor

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Save the old top bar

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$

REM Now print the menu bar

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

REM Turn mouse cursor back on


140 P& Advanced BASIC

Listing 4-2. MenuShow ( ) Subprogram—Displays the Menubar.


InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Show menu bar is on.

MenusOnFlag% = 1

LOCATE CurRow%, CurCol%

REM Clear the mouse queue

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

Now let’s see MenuShow( ) in action!

Showing Our Test Menubar


We can augment our Menulnitialize%( ) example so that the menubar is made to
appear by calling MenuShow( ) at the very end:

DECLARE FUNCTION MenuInitialize% (MenuNames( ) AS STRING, MenuChoices() AS cS


STRING, Attrib%, MenuAttrbs( ) AS INTEGER)
DECLARE SU8 MenuShow( )

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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

> CALL MenuShow

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.

Hiding the Menubar


This next subprogram, MenuHide( ), accompanies MenuShow( ). There are times
when you'll want to remove the menubar from the top of the screen, and MenuHide( )
lets you do it.
142 P» Advanced BASIC

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

DIM InRegs AS RegType, OutRegs AS RegType

REM Turn off mouse cursor

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

CurRow% = CSRLIN
CurColz = POoSsco)

see

Next, we replace the old text:

SUB MenuHide

DIM InRegs AS RegType, OutRegs AS RegType

REM Turn off mouse cursor

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

CurRow% = CSRLIN
CurCol% = Ppos(Q)

REM Print the old text

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

DIM InRegs AS RegType, OutRegs AS RegType

REM Turn off mouse cursor

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

CurRows CSRLIN
CurCol% POS(QO)

REM Print the old text

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

REM Turn mouse cursor back on

Ca
ae InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Show menu bar is off

MenusOnFlag% = O

LOCATE CurRow%, CurCol%

And we're done. Listing 4-3 shows the whole subprogram.

Listing 4-3. MenuHide( ) Subprogram — Hides the Menubar.


DECLARE SUB MenuHide()
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
144 Pb Advanced BASIC

Listing 4-3. MenuHide( ) Subprogram — Hides the Menubar.


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
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)

REM Turn off mouse cursor

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Print the old text

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

REM Turn mouse cursor back on

InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Show menu .bar is off

MenusOnFlag% = 0

LOCATE CurRow%, CurCol%

END SUB
> Pull-edown Menus’ 145

Now let’s put this new subprogram to work.

Hiding Our Test Menubar


This example just displays the menubar and then hides it when you press a key:

DECLARE FUNCTION MenulInitialize% (MenuNames() AS STRING, MenuChoices() AS _


STRING, BarAttrb%, MenuAttrbs() AS INTEGER)
DECLARE SUB MenuShow()
DECLARE SUB MenuHide()

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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

PRINT “Press any key to remove the menu bar."


dO
LOOP WHILE INKEYS$ = “"

> CALL MenuHide


146 P& Advanced BASIC

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.

Reading Menu Input


At this point, we have to get program input (keyboard, mouse, and menu). After we’ve
initialized the menu system and the menubar is displayed, we'll call MenuGetEvent$( ) to
receive that input. This function will wait until a key is pressed, a mouse button is pushed
or released, or a menu choice is made; then it will return and indicate what happened.

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:

InString$S = MenuGetEventS (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%)

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

“mousedown” Mouse button was pressed: Button% = 0 for left button;


Button% = 1 for right button.
Screen coordinates: (ScCRow%, ScCol%), using text-mode
ranges (1-25 and 1-80)
“mouseup” Mouse button was released: Button% = 0 for left button;
Button% = 1 for right button.
Screen coordinates: (ScRow%, ScCol%), using text-mode
ranges (1-25 and 1-80).
“menuopen” User opened a menu. Menu number in MenuNo%.
“menuclose” User closed a menu. Menu number in MenuNo%.
“menuchoice” A menu selection was made. MenuNo% = Menu number;
ChoiceNo% = the number of the choice in that menu.
Menu number and choice number correspond to the
array indices of the arrays you passed to Menu-
Initialize%( ).

If you're not interested in the mouseup, mousedown, menuopen, or menuclose events,


just call MenuGetEvent$( ) again until you read a key or the it returns a string of
menuchoice.
In that case, we can get the number of the menu chosen from MenuNo%, and number
of the choice made in that menu from ChoiceNo%:

_ 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

and MenuGetEvent$( ) returns values of MenuNo% = 1 and ChoiceNo% = 4, then the


choice made was MenuChoices(1, 4), or Peaches. In general, the choice made was Menu-
Choices(MenuNo%, ChoiceNo%).
We can use the Button%, ScRow%, and ScCol% variables to report mouse events:

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

: IF (a mouse button was used) THEN


: IF Cit was the Left mouse button) THEN
: If Cit was in a menu) THEN
: Cset menu arguments and EXIT]
: ELSE
Cset mouse arguments and EXIT]
‘ END IF
: ELSE "It was the right button
: Cset mouse arguments and EXIT]
: END IF
EXIT FUNCTION
: END IF
LOOP WHILE 1
> Pull-down Menus’ 149

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:

FUNCTION MenuGetEvent$ (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS RegType


MenuGetEvent$ = ""

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:

FUNCTION MenuGetEvent$S (MenuNoZ, ChoiceNo%, Button%, ScRow%, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS RegType


MenuGetEvent$ = "" :

dO
InChar$ = INKEYS

IF InChar$ <> "" THEN


MenuGetEvent$ = InChar$
IF MenusOnFlag% = 0 THEN EXIT FUNCTION "Menu system on?

END IF ‘Check key

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%:

FUNCTION MenuGetEvent$S (MenuNo%, ChoiceNo%, Button%, ScRow%z, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS Reglype


MenuGetEvent$ = ""

DO
InCharS = INKEYS

IF InChar$ <> "” THEN


MenuGetEvent$ = Inthar$
IF MenusOnFlag% = O THEN EXIT FUNCTION "Menu system on?
IF ASC(InChar$) = 27 THEN ‘Escape key
~ IF MenuNowOpen% = 0 THEN
3 EXIT FUNCTIO N

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%):

FUNCTION MenuGetEvent$S (MenuNoZ%, ChoiceNo%, Button%, ScRow%, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS RegType


MenuGetEvent$ = ""

DO
InChar$ = INKEYS

IF InChar$S <> "" THEN


MenuGetEvent$S = InChar$
IF MenusOnFlag% = 0 THEN EXIT FUNCTION ‘Menu system on?
> Pull-down Menus) 151

IF ASCCInChar$) = 27 THEN ‘Escape key


IF MenuNowOpen% = O THEN
EXIT FUNCTION
ELSE
CALL TurnOffMenu(MenuNow0pen%)
cat
2ee MenuNo% = MenuNowOpen%
MenuNowOpen% = 0
MenuGetEvent$ = "“menuclose"
EXIT FUNCTION
END IF
END IF "Escape key

END IF ‘Check key

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:

FUNCTION MenuGetEvent$S (MenuNo%, ChoiceNo%, Button%, ScRow%Z, ScCol%) STATIC

DIM InRegs AS RegType, OQutRegs AS RegType


MenuGetEvent$ = ""

DO
InChar$ = INKEY$

IF InChar$ <> "" THEN


MenuGetEvent$S = InChar$
IF MenusOnFlag% = 0 THEN EXIT FUNCTION "Menu system on?
IF ASCCInChar$) = 27 THEN ‘Escape key
IF MenuNowOpen% = 0 THEN
EXIT FUNCTION
ELSE
CALL TurnOf fMenu(MenuNow0penz)
MenuNo% = MenuNowOpens
MenuNowOpenz = 0
MenuGetEvent$ = “menuclose”
EXIT FUNCTION
END IF
END IF "Escape key
IF LENCInChar$) = 1 THEN
EXIT FUNCTION
we
ae ELSE
ScanCode = ASCC(RIGHTS(InChar$,1))
AltKey$ = ""
IF ScanCode >=16 AND ScanCode <=25 THEN AltKeyS=MIDSC"QWERTYUIOP",
ScanCode - 15,1)
IF ScanCode >=30 AND ScanCode <=38 THEN AltKeyS=MIDS("ASDFGHJKL",_
ScanCode ~ 29,1)
152 iad
pee Pb Advanced
ditches BASIC
sli a

IF ScanCode >=44 AND ScanCode <=50 THEN AltKeyS$=MIDSC"ZXCVBNM", _


ScanCode - 43,1)
IF AltKey$ = "" THEN EXIT FUNCTION

°
.

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

FUNCTION MenuGetEvent$S (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS RegType


MenuGetEvent$ = ""

dO
InChar$ = INKEYS

IF InChar$ <> "" THEN


MenuGetEvent$ = InChar$
IF MenusOnFlag% = 0 THEN EXIT FUNCTION "Menu system on?
IF ASC(InChar$) = 27 THEN ‘Escape key
IF MenuNowOpen% = 0 THEN
EXIT FUNCTION
ELSE
CALL TurnOffMenu(MenuNow0pen%)
MenuNo% = MenuNow0pen%
MenuNowOpen% = 0
MenuGetEvent$ = “menuclose“
EXIT FUNCTION
END IF
END IF "Escape key
IF LENCInChar$) = 1 THEN ,
EXIT FUNCTION
ELSE
ScanCode = ASC(RIGHT$(InChar$,1))
> Pull-down Menus’ 153

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

> IF MenuNowOpen% <> 0 THEN


4 FOR i = 1 TO Rows(MenuNowOpen%)
: IF AltKey$ = UCASES(MID$( Text (MenuNowOpen%,i),2,1)) THEN
MenuNo’ = MenuNowO0pen%
ChoiceNo% = ji
CALL TurnOffMenu(MenuNow0pen%)
MenuNowOpenz% = 0
MenuGetEvent$ = “menuchoice"
EXIT FUNCTION
END IF
NEXT i
END IF

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):

FUNCTION MenuGetEventS (MenuNo%, ChoiceNo%, Button%Z, ScRow%, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS RegType


MenuGetEvent$ = ""

DO
InChar$ = INKEY$

IF InChar$ <> "" THEN


MenuGetEvent$S = InChar$
IF MenusOnFlag% = QO THEN EXIT FUNCTION "Menu system on?
IF ASC(InChar$) = 27 THEN "Escape key
IF MenuNowOpen% = O THEN
EXIT FUNCTION
ELSE
CALL TurnOffMenu(MenuNow0penz)
MenuNo% = MenuNowOpend%
MenuNowOpen’ = 0
154 b Advanced
eee ce BASIC
hn
ti

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

IF MenuNowOpen% <> 0 THEN


FOR i = 1 TO Rows(MenuNow0pens)
THEN
If Altkey$S = UCASES (MIDS (Text (MenuNowOpen%,i),2,1))
MenuNo% = MenuNowOpen<
ChoiceNo% = i
CALL TurnOffMenu(MenuNow0pend)
MenuNowOpen% = 0
MenuGetEvent$S = “menuchoice”
EXIT FUNCTION
END IF
NEXT i
END IF

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:

REM *** Check for Left mouse button pressed

Button% = 0
InRegs.bx = 0
InRegs.ax = 5
CALL INTERRUPT(&H33, InRegs, OutRegs)

NumberTimes% = OutRegs.bx ‘Number of times Left button pressed


= OutRegs.dx \ 8 + 1 :
ScRow%
ScCol% = OutRegs.cx \ 8 + 1

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:

REM *** Check for left mouse button released

InRegs.bx = 0
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1

IF NumberTimes% <> 0 THEN


IF MenusOnFlag% AND (MenuNowOpen% <> 0) THEN

ECheck for menu event]


°
*

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:

REM *** Check for Left mouse button released

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

IF NumberTimes% <> 0 THEN


IF MenusOnFlag% AND (MenuNowOpen% <> 0) THEN
IF ScRow% => 2 AND ScRow% <= 2 + Rows (MenuNowOpen%) THEN
IF ScCol% >= 1 + 10 * (MenuNowOpen%-1) AND ScCol% <=
wed
as 10*(MenuNowOpen%) THEN a
: '
CMouse cursor was in an open menu when
left button released -- get menu choice]
> Pull-down Menus 157

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:

REM *** Check for Left mouse button released

InRegs.bx 0
InRegs.ax 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% OutRegs.cx \ 8 + 1

IF NumberTimes% <> 0 THEN


IF MenusOnFlag% AND (MenuNowOpen% <> 0) THEN
IF ScRow% >= 2 AND ScRow% <= 2 + Rows(MenuNowOpen%) THEN
IF ScCol% >= 1 + 10 * (MenuNowOpen%-1) AND ScCol% <= _
10*(MenuNowOpen%) THEN
=> ChoiceNo% = ScRow% - 1
CALL TurnOffMenu(MenuNowOpen%)
MenuNo% = MenuNowOpen%
MenuNowOpen% = 0
MenuGetEvent$ = "menuchoice”
EXIT FUNCTION
ELSE
MenuNo% = MenuNowOpenz%
CALL TurnOffMenu(MenuNowOpenz)
MenuNowOpen% = 0
MenuGetEvent$S = “menuclose”
EXIT FUNCTION
158 Pb Advanced BASIC

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.

Listing 4-4. MenuGetEvent$ Function — Reads Menu Events.


DECLARE FUNCTION MenuGetEvent$ (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%)
DECLARE SUB TurnOnMenu (MNumber%) :
DECLARE SUB TurnOffMenu (MNumber%)

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

Listing 4-4. MenuGetEvent$ Function — Reads Menu Events.


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$

FUNCTION MenuGetEvent$ (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS RegType

MenuGetEvent$ = ""

bo

InChar$ = INKEYS

IF InChar$ <> "" THEN


MenuGetEventS = InChar$
IF MenusOnFlag% = @ THEN EXIT FUNCTION "Menu system on?
IF ASC(InChar$) = 27 THEN ‘Escape key
IF MenuNowOpen% = 0 THEN
EXIT FUNCTION
ELSE
CALL TurnOffMenu(MenuNowOpens)
MenuNo% = MenuNowOpens
MenuNowOpen% = 0
MenuGetEvent$S = "menuclose"
EXIT FUNCTION
END IF
END IF ‘Escape key
IF LENCInChar$) = 1 THEN
EXIT FUNCTION
ELSE
SC = ASCCRIGHT$(InChar$,1))
AltKey$ = ""
IF SC >= 16 AND SC <= 25 THEN AltKey$ = MIDSC"QWERTYUIOP”,
Sc = 15,1)
IF $C >= 30 AND SC <= 38 THEN AltKey$S = MIDSC“ASDFGHJKL”,
SC = 29,1)
IF SC >= 44 AND SC <= 50 THEN AltKey$ = MIDSC"ZXCVBNM”,
sc - 43,1)
IF AltKeyS = “" THEN EXIT FUNCTION

IF MenuNowOpen% <> O THEN


FOR i% = 1 TO Rows(MenuNowOpenz)
160 P& Advanced BASIC

Listing 4-4. MenuGetEventS Function — Reads Menu Events.


IF AltKey$ = UCASESC(MID$(Text(MenuNow0pen%,i%),2,1)) THEN
MenuNox% = MenuNowOpenz
ChoiceNoz = iz
CALL TurnOf fMenu(MenuNow0penzZ)
MenuNowOpen% = 0
MenuGetEvent$ = "“menuchoice"
EXIT FUNCTION
END IF
NEXT i%
END IF

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

REM *** Check for left mouse button pressed

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

IF NumberTimes% <> 0 THEN


IF MenusOnFlag% AND (ScRow% = 1) THEN
MenubarCandidate% = ScCol% 110 + 1
IF MenubarCandidate% <= NumMenus% THEN
MenuNo% = MenubarCandidate%
ChoiceNo% = 0
MenuNowOpen% = MenubarCandidate%
CALL TurnOnMenu(MenubarCandidate%)
MenuGetEvent$ = “menuopen"
EXIT FUNCTION
END IF
ELSE
MenuGetEvent$S = “mousedown"
EXIT FUNCTION
> Pull-down Menus” 161

Listing 4-4. MenuGetEventS Function — Reads Menu Events.


END IF
END IF

REM *** Check for Left mouse button released

InRegs.bx 0
InRegs.ax "tou 6

CALL INTERRUPT(&H33, InRegs, OutRegs)

NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScColZ% = OutRegs.cx \ 8 + 1

IF NumberTimes% <> 0 THEN


IF MenusOnFlag% AND (MenuNowOpen% <> 0) THEN
IF ScRow% >= 2 AND ScRow% <= 2 + Rows(MenuNowOpen%) THEN
IF ScCol% >= 1 + 10 * (MenuNowOpen%-1) AND ScCol% <=
10*(MenuNowOpen%) THEN
ChoiceNo% = ScRow% - 1
CALL TurnOffMenu(MenuNow0pen%)
MenuNo% = MenuNow0pen%
MenuNowOpen% = 0
MenuGetEvent$S = “menuchoice”
EXIT FUNCTION
ELSE
MenuNo% = MenuNowOpen%
CALL TurnOffMenu(MenuNow0pen%)
MenuNowOpen% = 6
MenuGetEvent$ = “menuclose”
EXIT FUNCTION
END IF
ELSE
MenuNo% = MenuNowOpen%
CALL TurnOffMenu(MenuNow0pends>
MenuNowOpen% = 0
MenuGetEvent$S = "menucltose”
EXIT FUNCTION
END IF
ELSE
MenuGetEventS = "mouseup”
EXIT FUNCTION
END IF ‘Menu open
END IF ‘NumberTimes% <> 0

Check for right mouse button pressed


REM ***

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

Listing 4-4. MenuGetEventS Function — Reads Menu Events.


EXIT FUNCTION
END IF

REM *** Check for right mouse button released

InRegs.bx = 1
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1

IF NumberTimes% <> 0 THEN


MenuGetEvent$S = “mousedown"
EXIT FUNCTION
END IF

LOOP WHILE 1

END FUNCTION

SUB TurnOnMenu (MNumber%)

DIM InRegs AS RegType, OutRegs AS RegType

CurRowZ CSRLIN
CurCol% hou POS(O)

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Save the old section of screen

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

REM Now print the new text

TR TopRow(MNumber%)
sn Rou TopCol (MNumber%)
LOCATE TR, TC
InRegs.cx = 1
> Pull-down Menus’ 163

Listing 4-4. MenuGetEvent$ Function — Reads Menu Events.


FOR i = 1 TO Rows(MNumber%)
ORS = Text(MNumber%, i)
FOR j = 1 TO Cols(MNumber%)
InRegs.ax = &H900 + ASCCMIDSCORS, j, 1))
InRegs.bx = Attribute (MNumber%)
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + 1 - 1, TC + j
NEXT j
LOCATE TR + i, TC
NEXT ji

InRegs.ax = 1
CALL INTERRUPT(8&H33, InRegs, OutRegs)

LOCATE CurRow%, CurCol%

END SUB

SUB TurnOffMenu (MNumber%)

CurRow% = C SRLIN
CurCol% = POoSs(0)

DIM InRegs AS RegType, OutRegs AS RegType

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Print the old text

TR TopRow(MNumber%)
TC = TopCol(MNumberZ)
LOCATE TR, TC
InRegs.cx = 1

FOR i = 1 TO Rows (MNumber’)


ORS OldText(MNumber%, i)
ocs nt OldAttrb(MNumberz%, i)
FOR j = 1 TO Cols(MNumber2%)
InRegs.ax = &H9OO + ASCCMIDSCORS, j, 1))
InRegs.bx = ASC(MIDS(OCS, j, 1))
CALL INTERRUPT(&H10, InRegs, OutRegs)
LOCATE TR + 7 - 1, TC + 3
NEXT j
LOCATE TR #1, TC
NEXT i

InRegs.ax = 1
CALL INTERRUPT(&H33, InRegs, OutRegs)

LOCATE CurRow%, CurCol%

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.

Waiting for Input from Our Test Menus


Here’s how to use MenuGetEvent$( ) to read keyboard/mouse/menu input:

DECLARE FUNCTION MenuInitializez (MenuNames() AS STRING, MenuChoices() AS _


STRING, Attrib%, MenuAttrbs() AS INTEGER)
DECLARE SUB MenuShow()
DECLARE FUNCTION MouseInitialize% ()
DECLARE SUB MouseShowCursor ()
DECLARE FUNCTION MenuGetEvent$(MenuNoZ%, ChoiceNo%, Button%, Row%, Col%)

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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

Check% = Menulnitializex(MenuNames(), MenuChoices(), BaraAttrb%,


MenuAttrbs())
os
IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.” ;
END IF

CALL MenuShow
> Pull-down Menus’ 165

Check% = MouselInitialize%

IF Check% = 0 THEN PRINT “Cannot open mouse."

CALL MouseShowCursor

PRINT “Make a menu selection.”

DO
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
LOOP UNTIL Check$ = "“menuchoice”

PRINT “That selection was ",MenuChoices(MenuNo%, ChoiceNo%)

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

IF Check% = 0 THEN PRINT “Cannot open mouse."

CALL MouseShowCursor

The whole example program sets things up except for the meat of the last few lines:

CALL MenuShow

Check% = MouseInitialize%

IF Check% = 0 THEN PRINT “Cannot open mouse.”

CALL MouseShowCursor

PRINT “Make a menu selection.”

> dO
ChoiceNoz, Button%, Row%, Col%)
Check$ = MenuGetEvent$(MenuNo%,
LOOP UNTIL Check$ = “menuchoice”

selection was ", MenuChoices(MenuNo%, ChoiceNoz)


PRINT “That
166 P Advanced BASIC

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.

Reading Menu Input without Waiting


This is the INKEY$ of our menu system. It’s just like MenuGetEvent$( ), except that it
doesn’t wait for something to happen; if there are no keys, menu events, or mouse events
waiting, MenuCheckEvent$ is set to “” and the function returns at once.
On the other hand, if there’s something in the mouse queue or there’s a key waiting,
MenuCheckEvent$( ) handles them exactly as MenuGetEvent$( ) would. The only dif-
ferences in the values they return is that MenuCheckEvent$( ) can return a null string,
“” but MenuGetEvent$( ) never can. Here’s how to use it:

InString$ = MenuGetEvent$ (MenuNoX%, ChoiceNo%, Button%, ScRow%, SeCol%)

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

“mousedown” Mouse button was pressed: Button% = 0 for left


button; Button% = 1 for right button.
Screen coordinates: (ScRow%, ScCol%), using text-
mode ranges (1-25 and 1-80)
“mouseup” Mouse button was released: Button% = 0 for left
button; Button% = 1 for right button.
Screen coordinates: (ScRow%, ScCol%), using text-
mode ranges (1-25 and 1-80).
“menuopen” User opened a menu. Menu number in MenuNo%.
“menuclose” User closed a menu. Menu number in MenuNo%.
“menuchoice” A menu selection was made: MenuNo% = Menu
number; ChoiceNo% = the number of the choice in
that menu. Menu number and choice number
correspond to the array indices of the arrays you
passed to Menulnitialize%( ).
This function is almost exactly identical to MenuGetEvent$( ), which we’ve already
covered. There are only two differences. First, the DO...LOOP WHILE 1 loop in Men-
uGetEvent$( ) has been removed. If you recall, that looked like this in the outline of
MenuGetEvent$( ):

°
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.

Listing 4-5. MenuCheckEvent$—Reads Events without Waiting.


DECLARE FUNCTION MenuCheckEvent$ (MenuNo%, ChoiceNoz, Button%Z, ScRowZ, ScColZ)
DECLARE SUB TurnOnMenu (MNumber%)
DECLARE SUB TurnOffMenu (CMNumberZ)

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

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

COMMON SHARED /MenuB/ Attribute() AS INTEGER, Text) AS STRING,


OldText( ) AS STRING, OLdAttrb() AS STRING Ss
COMMON SHARED /MenuC/ Bar$, BarAttrb, MenusOnFlag%, NumMenus%, OldBar$,
OldBarAttrb$ ei
»> Pull-down Menus’ 169

Listing 4-5. MenuCheckEventS$—Reads Events without Waiting.


FUNCTION MenuCheckEvent$S (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%) STATIC

DIM InRegs AS RegType, OutRegs AS RegType

MenuCheckEvent$ = ""

InChar$ = INKEYS

IF InChar$ <> “" THEN


MenuCheckEvent$ InChar$
IF MenusOnFlag% O THEN EXIT FUNCTION "Menu system on?
IF ASCCInChar$) 27 THEN
uouou ‘Escape key
IF MenuNowOpen% = 0 THEN
EXIT FUNCTION
ELSE
CALL TurnOffMenu(MenuNow0pens)
MenuNo% = MenuNowOpen%
MenuNowOpensz = 0
MenuCheckEvent$ = “menuclose"
EXIT FUNCTION
END IF
END IF "Escape key
IF LENCInChar$) = 1 THEN
EXIT FUNCTION
ELSE
SC = ASCC(RIGHTS(CInChar$,1))
AltKey$ = ""
IF SC >= 16 AND SC <= 25 THEN AltKey$ = MIDS("QWERTYUIOP”, _
SO = 15,7)
IF SC <= 30 AND SC <= 38 THEN AltKey$ = MIDSC"ASDFGHJKL", _
SC = 29,1)
IF SC <= 44 AND SC <= 50 THEN AltKey$ = MIDS("ZXCVBNM", _
SC. -—- 45,1)
IF AltKey$ = "" THEN EXIT FUNCTION

IF MenuNowOpen% <> O THEN


FOR i% = 1 TO Rows(MenuNow0pendz)
IF AltKey$ = UCASES(MID$( Text (MenuNowOpen%,i%),2,1)) THEN
MenuNo% = MenuNowOpens
ChoiceNo% = i%
CALL TurnOffMenu(MenuNow0penz)
MenuNowOpen% =0
MenuCheckEvent$ = “menuchoice"
EXIT FUNCTION
END IF
NEXT 1%
END IF

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

Listing 4-5. MenuCheckEventS—Reads Events without Waiting.


END IF
IF MenuNowOpen% <> 0 THEN CALL TurnOffMenu(MenuNow0penzZ)
MenuNoz% = i%
ChoiceNos = 0
MenuNowOpen% = i%
CALL TurnOnMenu(iz)
MenuCheckEvent$ = “menuopen"
END IF
NEXT i% "For iZ = 1 TO NumMenus%
END IF "Check char length
EXIT FUNCTION
END IF "Check key

REM *** Check for left mouse button pressed

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

IF NumberTimes% <> 0 THEN


IF MenusOnFlag% AND (ScRow% = 1) THEN
MenubarCandidate% = ScCol% \ 10 + 1
IF MenubarCandidate% NumMenus% THEN
MenuNo% = MenubarCandidate%
ChoiceNo% = 0
MenuNowOpen% = MenubarCandidate%
CALL TurnOnMenu(MenubarCandidate%)
MenuCheckEvent$S = “menuopen"
EXIT FUNCTION
END IF
ELSE
MenuCheckEvent$ = “mousedown"
EXIT FUNCTION
END IF
END IF

REM *** Check for left mouse button released

InRegs.bx = 0
InRegs.ax = 6
CALL INTERRUPT (&H33, InRegs, OutRegs)

NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 14
ScCol% = OutRegs.cx \ 8 + 4

IF NumberTimes% <> 0 THEN


IF MenusOnFlag% AND (MenuNowOpen% <> 0) THEN ;
IF ScRow% >= 2 AND ScRow% <=2 + Rows (MenuNowOpen%)
IF ScCol% THEN
>= 1 + 10 * (MenuNowOpen%-1)
AND ScCol%<=
10*(MenuNowOpen%) THEN pe
> Pull-down Menus. 171

Listing 4-5. MenuCheckEvent$—Reads Events without Waiting.


ChoiceNo% = ScRow% - 1
CALL TurnOffMenu(MenuNow0pen%)
MenuNoz% = MenuNowOpen%
MenuNowOpens’ = 0
MenuCheckEvent$ = “menuchoice"
EXIT FUNCTION
ELSE
MenuNo% = MenuNowOpen%
CALL TurnOffMenu(MenuNowOpen%)
MenuNowOpen% = 0
MenuCheckEvent$ = “menuclose"
EXIT FUNCTION
END IF
ELSE
MenuNo% = MenuNowOpens
CALL TurnOffMenu(MenuNow0penz)
MenuNowOpen% = 0
MenuCheckEvent$S = “menuclose”
EXIT FUNCTION
END IF
ELSE
MenuCheckEvent$ = “mouseup”
EXIT FUNCTION
END IF ‘Menu open
END IF 'NumberTimes% <> 0

REM *** Check for right mouse button pressed

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

IF NumberTimes% <> O THEN


MenuCheckEvent$ = “mousedown”
EXIT FUNCTION
END IF

REM *** Check for right mouse button released

InRegs.bx 41
InRegs.ax 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

NumberTimes% = OutRegs.bx
ScRow% = OutRegs.dx \ 8 + 1
ScCol% = OutRegs.cx \ 8 + 1

IF NumberTimes% <> O THEN


MenuCheckEventS = "“mousedown”
EXIT FUNCTION
END IF
172 Pb Advanced BASIC

Listing 4-5. MenuCheckEvent$—Reads Events without Waiting.


END FUNCTION

SUB TurnOnMenu (MNumberZ)

DIM InRegs AS RegType, OutRegs AS RegType

CurRow% CSRLIN
CurCols% ou POS(0)

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Save the old section of screen

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

REM Now print the new text

TR = TopRow(MNumber%)
TC = TopCol (MNumber%)
LOCATE TR, TC
InRegs.cx = 1

FOR 1 = 1 TO Rows (MNumber%)


ORS = Text(MNumber%, i)
FOR j = 1 TO Cols (MNumber%)
InRegs.ax = &H900 + ASCCMIDSCORS, j, 1))
InRegs.bx = Attribute (MNumber%)
CALL INTERRUPT(8&H10, InRegs, OutRegs)
LOCATE TR + i - 1, TC + j
NEXT j
LOCATE TR + i, TC
NEXT i

InRegs.ax = 1
CALL INTERRUPT(8&H33, InRegs, OutRegs)

LOCATE CurRow%, CurCol%

END SUB

SUB TurnOffMenu (MNumber%)


> Pull-down Menus’ 173

Listing 4-5. MenuCheckEventS$—Reads Events without Waiting.


CurRow% CSRLIN
CurCol% iu Pos(0)

DIM InRegs AS RegType, OutRegs AS RegType

InRegs.ax = 2
CALL INTERRUPT(&H33, InRegs, OutRegs)

REM Print the old text

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)

LOCATE CurRow%, CurCol%

END SUB

That’s it. Let’s see how to use MenuCheckEvent$( ) with an example.

Getting Input from Our Test Menus


Here’s how to use MenuCheckEvent$( ):

(MenuNames( ) AS STRING, MenuChoices€) AS _


DECLARE FUNCTION MenulInitialize%
STRING, Attrib%, MenuAttrbs() AS INTEGER)
DECLARE SUB MenuShow( )
FUNCTION MouseInitialize% ()
DECLARE
SUB MouseShowCursor ()
DECLARE Button%, Row%, Col%)
MenuCheckEventS(MenuNoZ, ChoiceNo%,
DECLARE FUNCTION

DIM MenuNames(1 TO 3) AS STRING


TO 3, 1 TO 5) AS STRING
DIM MenuChoices(1
DIM MenuAttrbs(1 TO 3) AS INTEGER
174 Pb Advanced BASIC

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

Check% = Menulnitialize%(MenuNames( ), MenuChoices( ), BarAttrb%,


MenuAttrbs( ))

IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.”
END IF

CALL MenuShow

Check% = MouseInitializez%

IF Check% = 0 THEN PRINT “Cannot open mouse."

CALL MouseShowCursor

bo
Check$ = MenuCheckEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
LOOP UNTIL Check$ = “menuchoice”

PRINT "That selection was “,MenuChoices(MenuNoz, ChoiceNo%)

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

Marking a Menu Selection


We can develop a subprogram to mark menu choices with a check mark; let’s call this
subprogram MenuMarkChoice( ). With it, you can show that a choice is toggled on —
this subprogram will place a checkmark in front of the choice in the menu. (The next
time the menu is opened, the user will see the checkmark.) For example, if Spellchecking
is the entry in menu 6, choice 4, calling MenuMark(6, 4) will place a check in front of it.
In general, we will be able to use MenuMarkChoice( ) like this:

CALL MenuMarkChoice (MenuNumber%, ChoiceNumber%)

These are the input parameters:


MenuNumber% The menu number of the choice to mark.
ChoiceNumber% The choice number to mark in that menu.

This subprogram is simplicity itself compared to MenuGetEvent$( ) or MenuCheck-


Event$( ). All it does is to make a checkmark (CHR$(251)) the first character in the
menu row holding that choice. Since the rows of the menus are stored as strings in Text(i,
j), where i is the menu number and j is the choice number, that looks like this:

MIDSC(Text(MenuNumber%, ChoiceNumber%),1,1) = CHR$(251)

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!)

Listing 4-6. MenuMarkChoice Subprogram—Marks Menu Choice. 1,0f 2


DECLARE SUB MenuMarkChoice (MenuNumber%, ChoiceNumberZ)

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

Listing 4-6. MenuMarkChoice Subprogram—Marks Menu Choice. yao) a4


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 MenuMarkChoice (MenuNumber%, ChoiceNumberZ)

—+ MIDSCText(MenuNumber%, ChoiceNumber%),1,1) = CHR$(251)

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( ).

Marking Items in Our Test Menus


In this example, we can toggle any of the menu choices on. (However, we have no way
of toggling them off yet.)

DECLARE SUB MenuMarkChoice (MenuNumber%, ChoiceNumber%)

DECLARE FUNCTION MenuInitialize% (MenuNames( ) AS STRING, MenuChoices( ) AS


STRING, Attrib%, MenuAttrbs( ) AS INTEGER) &
DECLARE SUB MenuShow ( )
DECLARE FUNCTION MouseInitialize% ()
DECLARE SUB MouseShowCursor ( )
DECLARE FUNCTION MenuGetEvent$S (MenuNo%, ChoiceNo%, Button%z, Row%, Cot%)
DIM MenuNames(1 TO 3) AS STRING
DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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

Check% = Menulnitializez(MenuNames( ), MenuChoices(), BarAttrb%,


MenuAttrbs( ))

IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT “Menu is set up.”
END IF

CALL MenuShow

Check% = MouseInitialize%

IF Check% = 0 THEN PRINT “Cannot open mouse."

CALL MouseShowCursor

PRINT "Select menu choices to be marked. Type q to quit."

DO
Check$ = MenuGetEventS(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
IF Check$S = “menuchoice” THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASES(Check$) = “Q*

All we do here is to ask the user to select menu choices to toggle:

PRINT “Select menu choices to be marked. Type q to quit.”

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):

PRINT “Select menu choices to be marked. Type q to quit.”


178 PB Advanced BASIC

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.

Unmarking a Menu Selection


This subprogram just “unmarks” a choice ina menu. That is, if the choice is checked,
MenuUnMarkChoice( ) removes the checkmark, showing that that choice is now tog-
gled off. We can use it like this:

CALL MenuUnMarkChoice (MenuNumber%, ChoiceNumber%)

These are the inputs:


MenuNumber% The menu number of the choice to unmark.
ChoiceNumber% The choice number to unmark in that menu.
Like MenuMarkChoice( ), the listing here is very simple. The whole thing is contained in
just this line:

MIDSCText(MenuNumber%, ChoiceNumber%),1,1) = “"

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

Listing 4-7. MenuUnMarkChoice—Unmarks a Menu Choice.


DECLARE SUB MenuUnMarkChoice (MenuNumber%, ChoiceNumber%)

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

Listing 4-7. MenuUnMarkChoice—Unmarks a Menu Choice.


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, PS
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$ ie
SUB MenuUnMarkChoice (MenuNumber%, ChoiceNumber%)

MID$S(Text(MenuNumber%, ChoiceNumber%),1,1) = “ "

END SUB

That’s it. Let’s look at an example.

Unmarking Items in Our Test Menus


Here’s how to use both MenuMarkChoice( ) and MenuUnMarkChoice( ):

(MenuNames( ) AS STRING, MenuChoices() AS


DECLARE FUNCTION MenuInitialize%
STRING, Attrib%, MenuAttrbs¢ ) AS INTEGER)
DECLARE SUB MenuShow ( )
DECLARE FUNCTION MouselInitialize% ()
DECLARE SUB MouseShowCursor ( )
(MenuNo%, ChoiceNo%, Buttonz, Row%, Col%)
DECLARE FUNCTION MenuGetEvent$
SUB MenuMarkChoice (MenuNumber%, ChoiceNumber%)
DECLARE
SUB MenuUnMarkChoice(MenuNoZ, ChoiceNos)
DECLARE

DIM MenuNames(1 TO 3) AS STRING


MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM
DIM MenuAttrbs(1 TO 3) AS INTEGER

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

IF Check% = 0 THEN PRINT "Cannot open mouse.”

CALL MouseShowCursor

PRINT "Select menu choices to be marked. Type q to quit.”

DO
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%, Row%, Col%)
IF Check$ = “menuchoice” THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%r)
LOOP UNTIL UCASES(Check$) = “Q" .

PRINT "Now select menu choices to be UNmarked. Type q to quit.”

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:

PRINT “Select menu choices to be marked. Type q to quit."

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:
“ ”

PRINT “Select menu choices to be marked. Type q to quit."

60
Check$ = MenuGetEvent$(MenuNo%, ChoiceNox%, Button%, Row%, Col%)
IF Check$ = "menuchoice" THEN CALL MenuMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASES(Check$) = “q"

+> PRINT "Now select menu choices to be UNmarked. Type q to quit.”

oe DO
Check$ = MenuGetEvent$(MenuNo%, ChoiceNo%, Button%Z, Row%, Col%)
IF Check$ = “menuchoice" THEN CALL MenuUnMarkChoice(MenuNo%, ChoiceNo%)
LOOP UNTIL UCASESC(Check$) = "Q"

Again, you type q or Q to quit.


There’s only one more menu routine that we should develop; if you’re using Menu-
GetEvent$( ) or MenuCheckEvent$( ), you'll get menu input in the variables MenuNo%
and ChoiceNo%:

InString$S = MenuGetEvent$ (MenuNo%, ChoiceNo%, Button%, ScRow%, ScCol%)

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$( ):

ReadString$ = MenuReadChoice$ (MenuNo%, ChoiceNo%)

Here are the inputs that we can supply:


MenuNo% The menu number (as passed to Menulnitialize%(_)) of
the choice you’re interested in.
ChoiceNo% The choice number of the choice in the menu you're
interested in.
And here’s what we want back:
MenuReadChoice$ MenuNo% = 0 MenuReadChoice$ = Menu name n
from the menu bar (where n is the
number passed in ChoiceNo%).
MenuNo% = 1 MenuReadChoice$ = Choice name n
in menu m (where n is the number
passed in ChoiceNo% and m is the
number passed in ChoiceNo%).
The programming here is straightforward; all we do is check whether we
have to
return a choice name or a menu name, and then do so. For a menu name,
that looks like
this:

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))

> MenuReadChoice$ " RTRIMSCLTRIMS(Text(MenuNo%, ChoiceNo%)))


END IF

Listing 4-8 shows the whole function.

Listing 4-8. MenuReadChoice$ Function.


DECLARE FUNCTION MenuReadChoice$ (MenuNo%, ChoiceNo%)

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$

FUNCTION MenuReadChoice$S (MenuNo%, ChoiceNoz)

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

Now let’s see MenuReadChoice$( ) at work.


184 b> Advanced BASIC

Reading Items from Our Test Menus


The example program looks like this:

DECLARE FUNCTION Menulnitialize% (MenuNames( ) AS STRING, MenuChoices¢€ ) AS _


STRING, BarAttrb%, MenuAttrbs(€ ) AS INTEGER)
DECLARE FUNCTION MenuReadChoice$ (MenuNo%, ChoiceNoz)

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 T0 5) AS STRING, AC1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER, BC(1 TO 3) AS INTEGER

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

Check% = MenuInitialize%(MenuNames( ), MenuChoices( ), BarAttrb%,


MenuAttrbs( ))

IF Check% = 0 THEN
PRINT “Error.”
ELSE
PRINT "Menu is set up."
END IF

PRINT “Choice 1 of menu 1 is: ", MenuReadChoice(1, 1)


PRINT “Menu Number 1 is: ", MenuReadChoice(0, 1)

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.

Menus in the BASIC PDS


The BASIC PDS Toolbox also has a set of menu functions, and they’re similar to what
we've developed above. If you have the PDS, it’s worth taking a look at how these
functions work. Let’s put together a sample program of the type we developed for Menu-
GetEvent$( ) one that will place a menu on the screen and report which menu selection
you've made.

|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:

QBX MENUTEST \L UIASM

In addition, you have to load MENU.BAS, WINDOWBAS, MOUSE.BAS, and GEN-


ERAL.BAS, since the PDS menu system uses code in all of these.

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
*
*

Next, we have to set up these COMMONs and dimension these arrays:

* $INCLUDE: 'C:\BC7\GENERAL.BI’
" SINCLUDE: 'C:\BC7\MOUSE.BI'
" SINCLUDE: 'C:\BC7\MENU.BI"
* SINCLUDE: 'C:\BC7\WINDOW.B1'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType co


COMMON SHARED /uitools/ GloTitle( ) AS MenuTitleType .
COMMON SHARED /uitools/ GloItem( ) AS MenulItemType
DIM GloTitle(MAXMENU) AS MenuTitleType
DIM GloItem(MAXMENU, MAXITEM) AS MenultemType
°*

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'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle( ) AS MenuTitleType
COMMON SHARED /uitools/ Gloltem( ) AS MenultemType

DIM GloTitleCMAXMENU) AS MenuTitleType


DIM Gloltem(MAXMENU, MAXITEM) AS MenultemType

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER “eT
te

And, we can fill them as we have before:


SU > Pull-down
COWN Menus’ 187
OnUS 157

SINCLUDE: "C:\BC7\GENERAL.BI'
SINCLUDE: "C:\BC7\MOUSE.BL'
SINCLUDE: "C:\BC7\MENU.BI!
SINCLUDE: "C:\BC7\WINDOW.BI!

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle( ) AS MenuTitleType
COMMON SHARED /uitools/ GloItem( ) AS MenultemType

DIM GloTitle(MAXMENU) AS MenuTitleType


DIM GloItem(MAXMENU, MAXITEM) AS MenultemType

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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:

CALL MenuSet(Menu%, Item%, State%, Text$, Accesskey%)

These are the inputs:


Menu% Menu number.

Item% Choice number in that menu (0 = menu name).


188 P» Advanced BASIC

State% 0 = Choice is “disabled” — i.e., not a valid choice; 1


= Choice is “enabled” — this is the usual state; 2 =
Choice is “enabled” and toggled with a checkmark.

Text$ The menu or choice name itself.

Accesskey% The character position in Text$ of the letter to treat


as standing for this choice. This letter is highlighted
in the menu.
This means that we'll have to loop over MenuSet( ), calling it once for each menu
choice. Here, then, is how we can load the data from our arrays into the PDS menu
commons:

* SINCLUDE: ‘'C:\BC7\GEERAL.BI'
* SINCLUDE: "C:\BC7\MOUSE.BI'
" SINCLUDE: ‘'C:\BC7\MENU.BI'
" SINCLUDE: ‘C:\BC7\WINDOW.BI'

COMMON SHARED /uiteools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitlec ) AS MenuTitleType
COMMON SHARED /uitools/ GlolItem( ) AS MenultemType

DIM GloTitLe(MAXMENU) AS MenuTitleType


DIM GloIltem(MAXMENU, MAXITEM) AS MenultemType

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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:

CALL MenuColor(Fore%, Back%, High%, Disab%, Curfore%, Curback%, Curtiz)

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'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ Gloltem() AS MenultemType
190 Pb Advanced BASIC

DIM GloTitleCMAXMENU) AS MenuTitleType


DIM GloItem(MAXMENU, MAXITEM) AS MenultemType

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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 MenuColor(14, 1, 12, 8, 14, 3, 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

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ Gloltem() AS MenultemType

DIM GloTitle(MAXMENU) AS MenuTitleType


DIM Gloltem(MAXMENU, MAXITEM) AS MenultemType

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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%

1, 12, 8, 44, 3; 12)


CALL MenuColor(14,

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:

' SINCLUDE: 'C:\BC7\GENERAL.BI'


" SINCLUDE: 'C:\BC7\MOUSE.BI'
" SINCLUDE: 'C:\BC7\MENU.BI'
" SINCLUDE: ‘C:\BC7\WINDOW.BI'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ Gloltem() AS MenultemType

DIM GloTitle(MAXMENU) AS MenuTitleType


DIM GloIltem(MAXMENU, MAXITEM) AS MenulteamType

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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 MenuColor(14, 1, 12, 8, 14, 3, 12)

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.

Listing 4-9. Using the BASIC PDS Menu System.


SINCLUDE: ‘C:\BC7\GENERAL.BI'
SINCLUDE: 'C:\BC7\MOUSE.BI'
SINCLUDE: 'C:\BC7\MENU.BI’'
SINCLUDE:
«-/-2e2«
se
‘C:\BC7\WINDOW.BI'

COMMON SHARED /uitools/ GloMenu AS MenuMiscType


COMMON SHARED /uitools/ GloTitle() AS MenuTitleType
COMMON SHARED /uitools/ Gloltem() AS MenultemType

DIM GloTitle(MAXMENU) AS MenuTitleType


DIM Gloltem(MAXMENU, MAXITEM) AS MenultemType

DIM MenuNames(1 TO 3) AS STRING


DIM MenuChoices(1 TO 3, 1 TO 5) AS STRING
DIM MenuAttrbs(1 TO 3) AS INTEGER

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

Listing 4-9. Using the BASIC PDS Menu System. yo) 2


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% = &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 MenuColor(14, 1, 12, 8, 14, 3, 12)


CALL MenuPreProcess

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.

A Mouse-driven Paint Program


Our first graphics project is going to be a full power mouse-driven paint program.
Even if you don’t have a mouse, keep in mind that this program is really a vehicle to
introduce the various BASIC graphics routines to us, so it’s valuable to work through in
any case.
Since we're going to use the mouse, we have to start by defining the TYPE Reglype, as
well as InRegs and OutRegs as before:

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
*

Paint programs without mice are exercises in futility; there is no satisfactory


way to use a paint program from the keyboard.

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

DIM InRegs AS RegType, OutRegs AS RegType

> GOSUB Initialize


*

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

DIM InRegs AS RegType, OutRegs AS RegType

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

DIM InRegs AS RegType, OutRegs AS RegType


202 & Advanced BASIC

=
(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";

The menubar options in our full function paint program include:

Exit Exit the program


Color Set the drawing (foreground color)
Bkgrnd Set the background color
Draw Draw pixel by pixel
Line Draw a line
Box Draw a box
Circle Draw a circle
Paint Fill a shape in with solid color
Save Save image to disk
Load Load image from disk

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”;

> InRegs.ax = 0 ‘Initialize mouse


CALL INTERRUPT(&H33, InRegs, OutRegs)

InRegs.ax = 1 "Show mouse cursor


CALL INTERRUPT(&H33, InRegs, OutRegs)

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

InRegs.ax = 0 "Initialize mouse


CALL INTERRUPT(&H33, InRegs, OutRegs)

InRegs.ax = 1 "Show mouse cursor


CALL INTERRUPT(&H33, InRegs, OutRegs)
DrawFlag% 0
LineFlag% = 0
bot
as BoxFlag% = 0
CircleFlag% = 0
PaintFlag% = 0
RETURN

Now we're ready for GetLeftButtonPress.

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

-LOOP WHILE OutRegs.bx = 0


~ RowX = OutRegs.dx \ 8 + 1
2 IF Row% = 1 THEN
: MenuSelectionMadex% = 1
ELSE
MenuSelectionMade% = 0
END IF
RETURN

That’s it. Now we're ready to start handling menu selections.

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

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 Back% = 0
COLOR Fore%, BackZ

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:

GET (0, 8)-(639, 199), Storage

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.

TIP: BASIC’s BSAVE statement is extraordinary powerful; it is the quickest way of


loading data to and from the disk that BASIC supports. Although we are using
it here to save a screen image, it is more commonly used to save blocks of
data quickly, especially arrays.
nn nnn ES
210 P& Advanced BASIC

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:

DEF SEG = VARSEG(Storage(1))

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:

‘DEF SEG = VARSEG(Storage(1)) _


BSAVE “PAINT.DAT", VARPTR(Storage(1)), 62532
DEF SEG
$A
ee > Graphics
Graphics 211
211

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

Just as BSAVE is BASIC’s quickest way of saving data on disk, so BLOAD is


the fastest way of loading it.

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

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 INTERRUPTC&H33, InRegs, OutRegs)
CASE 10
InRegs.ax = 2 *Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
DEF SEG = VARSEG(Storage(1))
BLOAD "“PAINT.DAT", VARPTR(Storage(1))
DEF SEG
PUT (0, 8), Storage, PSET
> InRegs.ax = 1 'Show mouse cursor
CALL INTERRUPTC&H33, InRegs, OutRegs)
END SELECT
RETURN

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

DIM InRegs AS RegType, OutRegs AS RegTtype


DIM Storage(31266) AS INTEGER

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

CSet current pixetd

Inkegs.bx = 0 "Left Button Releases


InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0

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

DIM InRegs AS RegType, OutRegs AS RegType


DIM Storage(31266) AS INTEGER

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):

LINE (X17, Y¥1) — (X2, Y2), Colorz

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%

> InRegs.bx = 0 "Get left button releases to clear queue


\ _ InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

DO

InRegs.bx = 0 'Left Button Releases


InRegs.ax = 6 :
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
RETURN

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

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)
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:

LINE (X1, ¥1) - (X2, Y2), Color%, B <

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%

InRegs.bx = 0 "Get Left button releases to clear queue


InRegs.ax = 6
CALL INTERRUPT(&H33, eal OutRegs)

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

And that’s all there is to it.

Subroutine DrawCircle

We can also draw circles in BASIC with the CIRCLE statement:

CIRCLE (X%, Y%), Radius!, Color%

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%

InRegs.bx = 0 "Get left button releasesto clear queue


InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

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

And that’s all there is to drawing circles.

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:

PAINT (X%, Y%)

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

PAINT (X%, Y%), BorderColor’

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 ~

The Paint Program Listing


That’s it for the paint program. Listing 5-1 shows the whole program, line by line.

Listing 5-1. A Mouse-driven Paint Program. sie) ae)


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

DIM InRegs AS RegType, OutRegs AS RegType


DIM Storage(31266) AS INTEGER

GOSUB Initialize
DO
GOSUB GetLeftButtonPress

IF MenuSelectionMade% THEN
> Graphics 223

Listing 5-1. A Mouse-driven Paint Program.


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

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";

InRegs.ax = 0 ‘Initialize mouse


CALL INTERRUPT(&H33, InRegs, OutRegs)

InRegs.ax = 1 "Show mouse cursor


CALL INTERRUPT(&H33, InRegs, OutRegs)
DrawFlag% = 0
LineFlagz = 0
BoxFlagz = 0
CircleFlag% = 0
PaintFlag% = 0
RETURN

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

Listing 5-1. A Mouse-driven Paint Program.


SELECT CASE Choice%
CASE 1
END
CASE 2
Fore% = Forez + 1
IF fForez% > 15 THEN Fore% = 0
COLOR Forerz, Back%
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOCATE 1, 9
PRINT “Color (Se
InRegs.ax = 1 ‘Show mouse cursor
CALL INTERRUPTC(&H33, InRegs, OutRegs)
CASE 3
Back% = Back% + 1
IF Back% > 7 THEN Back% = 0
COLOR Forez, 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 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)
CASE 10
InRegs.ax = 2 ‘Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
DEF SEG = VARSEG(Storage(1))
BLOAD “PAINT. DAT", VARPTR(Storage(1))
DEF SEG
PUT (0, 8), Storage, PSET
InRegs.ax = 1 "Show mouse cursor
: CALL INTERRUPTC&H33, InRegs, OutRegs)
END SELECT
RETURN

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

Listing 5-1. A Mouse-driven Paint Program.


X% = OutRegs.cx
YR = OutRegs.dx
InRegs.ax = 2 "Mide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
PSET (X%, YX)
InRegs.ax = 1 "Show mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
InRegs.bx = Q "Left Button Releases
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0

RETURN

DrawLine:
X% = OutRegs.cx
Y% = OutRegs.dx
XOLdX% = X%
YOld% = Y%

InRegs.bx = 0 "Get Left button releases to clear queue


InRegs.ax = 6
CALL INTERRUPTC(&H33, InRegs, OutRegs)

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%

0 ‘Get Left button releases to clear queue


InRegs.bx =
InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

po
InRegs.ax = 3
CALL INTERRUPT(&H33, InRegs, OutRegs)
226 & Advanced BASIC

Listing 5-1. A Mouse-driven Paint Program.


YNew% = OutRegs.dx
XNew% = OutRegs.cx
InRegs.ax = 2 "Hide mouse cursor
CALL INTERRUPT(&H33, InRegs, OutRegs)
LINE (X%, Y¥%)-CXOlLd%, YOLd%), Back%, B
LINE (X%, Y4)-(XNew%, YNew%), Forez%, 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, InRegs, OutRegs)
LOOP WHILE OutRegs.bx = 0
RETURN

DrawCircle:
X% OutRegs.cx
¥% wot OutRegs.dx
XOlLdZ% X%
YOld% = Y%

InRegs.bx = 0 "Get Left button releases to clear queue


InRegs.ax = 6
CALL INTERRUPT(&H33, InRegs, OutRegs)

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:

DRAW “BUSO NL25..."


228 & Advanced BASIC

Next, we move diagonally down and to the right by 12 pixels, and then straight down by
20:

DRAW “BUSO NL25 F12 D20..."

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.

Sprites and Animation


A screen sprite is a small graphics image that you can put on the screen any place you
like; for instance, in our example program, we design a small rocket ship, complete with
fire coming out of the tail. After this image is designed and stored, you can place it
anywhere you want on the screen, and we'll animate the image, sending it blasting up the
screen.
Just as we did with our paint program, we can cut the task of designing and using
screen sprites up into subroutines, which we call from the main body of the program.
230 & Advanced BASIC

Let’s call this program SPRITE.BAS.


We begin by designing the sprite itself. To make that process easier, we can use an
array of type STRING, and, since color values can range from 0 to 15, we use the
appropriate hex digit to design the sprite pixel by pixel. For example, here’s how we start
SPRITE.BAS with the design of the rocket ship:

DIM SpriteArray(1 TO 16) AS STRING

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"

By restricting ourselves to color values ranging from 0 to 15 (that is, 0 to &HF),


we are excluding the advanced VGA mode, which can take color values from 0
to &HFF.

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:

DIM SpriteArray(1 TO 16) AS STRING

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 <_

DIM SpriteArray(1 TO 16) AS STRING


DIM GetArray(1) AS INTEGER -

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 ~

Redimensioning dynamic arrays with REDIM is a new and powerful capability


in BASIC. You use REDIM primarily when memory space is at a premium —
you can shrink or expand the sizes of your arrays to fit what’s available. The
BASIC Professional Development System goes even farther wtih its SET-
MEM ) function, giving you still more control over memory.
232 P& Advanced BASIC

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

DIM SpriteArray(1 TO 16) A S STRING


DIM GetArray(1) AS INTEGER

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

DIM SpriteArray(1 TO 16) A S STRING


DIM GetArray(1) AS INTEGER

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

= PuT (50, 50), GetArray


.

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.

Listing 5-2. Sprite. BAS—Draw and Animate Screen Sprites.


* SDYNAMIC

DIM SpriteArray(1 TO 16) AS STRING


DIM GetArray(1) AS INTEGER

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

Listing 5-2. Sprite. BAS—Draw and Animate Screen

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.

Listing 5-3. DrawSprite Subroutine.


DrawSprite:
SCREEN ScreenMode%
XStart 10
yStart wou 10
FOR i = 1 TO UBOUND(CA, 1)
FOR j = 1 TO LENCSpriteArray(i))
CurrentColor% = ASCCUCASES(CMID$(SpriteArray(i), j, 1)))
IF CurrentColor% <= ASC("9") THEN
CurrentColor% = CurrentColor% = ASc("0")
236 & Advanced BASIC

Listing 5-3. DrawSprite Subroutine.


ELSE
CurrentColor% = CurrentColor% - ASCC("A") + 10
END IF
PSET (XStart + j - 1, YStart + i - 1), CurrentColor%
NEXT j
NEXT i
RETURN

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)

SELECT CASE ScreenModeZ%


CASE 1
SpriteSize = C4+INTCCCRows+1)*2+7)/8)*1*(Cols+1) + 1) \ 2:
CASE 2, 11
SpriteSize = (4+INTCCCRows+1)*14+7)/8)*1*(Cols41) + 1) \ 2:
CASE 7, 8, 9, i2
SpriteSize = C4+INTCCCRows+1)*1+7)/8)*4*(Cols+1) + 4) \ 2:
CASE 10
SpriteSize = (4+INTC(CCRowst+1)*14+7)/8)*2*(Cols+1) + 1) \ 2:
CASE 13
SpriteSize = (4+INTC( CRows+1)*8+7)/8)*1*(Colst1) + 4) \ 2:
CASE ELSE
SpriteSize = 0
END SELECT

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)

SELECT CASE ScreenMode%


CASE 1
SpriteSize = C4+INTCCCRows4#1)*2+7)/8)*1*(Cols#1) + 1) \ 2:
CASE 27-11
SpriteSize = (4+INT(CCRows+1)*147)/8)*1*(Cols+1) + 1) \ 2:
CASE 7¢, 8, 9, 12
SpriteSize = C4+INTCCCRows#1)*147)/8)*4*(Cols+1) * 4) \ 23
CASE 10
SpriteSize = C4+INTC(CRows+1)*1+7)/8)*2*(Cols+1) + 1) \ 2:
CASE 13
SpriteSize = C4+INTCCCRowst+1)*8+7)/8)*1*(Cols+1) + 1) \ 2:
CASE ELSE
SpriteSize = 0
END SELECT
= REDIM GetArray(1 TO SpriteSize) AS INTEGER

Now we just have use GET to store the sprite, and we're done. Listing 5-4 shows the
completed subroutine.

Listing 5-4. GetSprite Subroutine


GetSprite:

Cols = LEN(SpriteArray(1))
Rows = UBOUND (SpriteArray, 1)

SELECT CASE ScreenMode%


CASE 1
+ 1) \ 2:
SpriteSize = COFINTCCCRows+1)*247)/8)*1*(Colst1)
CASE 2, 11
+ 1) \ 2:
SpriteSize = CAF INTC CCRows+1)*147)/8)*1*(Colst+1)
CASE 7, 8, 9, 12
+ 1) N23
SpriteSize = C4FINTCCCROWS41)*1472/8)*4*(Colsti)
CASE 10
+ 17 25
SpriteSize = COFINTCCCRowst1)*147)/8)*2*(Colst+1)
CASE 13
+ 4) \ 2:
SpriteSize = COFINTCCCRowS+1)*847)/8)*1*(Colst1)
CASE ELSE
SpriteSize = 0
END SELECT
REDIM GetArray(1 TO SpriteSize) AS INTEGER
(10, 10)-(10 + Rows, 10 + Cots), GetArray
= GET
RETURN
238 -& Advanced BASIC
cached
ca ae ER nn

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.

Listing 5-5. SavesSprite( ) Subroutine.


‘SaveSpri -
+ BSAVE “ rE. DAT , 2 * UBOUND(GetArray, 1)

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

That’s all there is to it.

The SPRITE.BAS Listing


Listing 5-7 shows the whole program SPRITE.BAS, including the animation loop at the
end of the main body of the program:
> Graphics 239

Listing 5-7. SPRITE.BAS—Draw and Animate Screen Sprite.


* SDYNAMIC
DIM SpriteArray(1 TO 16) AS STRING
DIM GetArray(1) AS INTEGER

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

PuT (50, 50), GetArray

FOR i = 1 TO 100
PuT (100, 100 - i), GetArray
PuT (100, 100 - i), GetArray
NEXT i

PuT (100, 0), GetArray

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

Listing 5-7. SPRITE.BAS—Draw and Animate Screen Sprite.


NEXT i
RETURN

GetSprite:
Cols LEN(SpriteArray(1))
Rows Ro UBOUND (SpriteArray, 1)

SELECT CASE ScreenMode%


CASE 1
SpriteSize = (4+INT(((CRows+1)*2+7)/8)*1*(Cols+1) + 1) \ 2:
CASE 2, 11
SpriteSize = (4+INTC(CCRows+1)*1+7)/8)*1*(Cols+1) + 1) \ 2:
CASE 7, 8, 9, 12
SpriteSize = (4+INT(((Rows+1)*1+7)/8)*4*(Cols+1) + 1) \ 2:
CASE 10
SpriteSize = (4+INTCCCRowst+1)*1+7)/8)*2*(Cols+1) + 1) \ 2:
CASE 13
SpriteSize = (4+INTC(CRows+1)*8+7)/8)*1*(Cols+1) + 1) \ 2:
CASE ELSE
SpriteSize = 0
END SELECT
REDIM GetArray(1 TO SpriteSize) AS INTEGER
GET (10, 10)-(10 + Rows, 10 + Cols), GetArray
RETURN

SaveSprite:
DEF SEG = VARSEG(GetArray(1))
BSAVE “SPRITE.DAT", VARPTR(GetArray(1)), 2 * UBOUND(GetArray, 1)
DEF SEG
RETURN

LoadSprite:

DEF SEG = VARSEG(GetArray(1))


BLOAD “SPRITE.DAT", VARPTR(GetArray(1))
DEF SEG
RETURN

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.

Presentation Graphics in the BASIC PDS


If you have the BASIC PDS, there’s another type of graphics
you should be aware of,
especially if you do any business programming, called Presentation
Graphics. This pack-
Graphics 241

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:

Region Peanut Butter Consumption (tons)


North 219
East 19
South 119
West 319

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):

QBX PIE /L CHRTBEFR

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'

DIM Regions(4) AS STRING


DIM Consumption(4) AS SINGLE
DIM Exploded(4) AS INTEGER

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:

" SINCLUDE: 'C:\BC7\FONTB.BI'


" SINCLUDE: 'C:\BC7\CHRTB.BI1'

DIM Regions(4) AS STRING


DIM Consumption(4) AS SINGLE
DIM Exploded(4) AS INTEGER

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'

DIM Regions(4) AS STRING


DIM Consumption(4) AS SINGLE
DIM Exploded(4) AS INTEGER

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'

DIM OurChart AS ChartEnvironment cod


DIM Regions(4) AS STRING
DIM Consumption(4) AS SINGLE
DIM Exploded(4) AS INTEGER

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:

" SINCLUDE: 'C:\BC7\FONTB.BI'


SINCLUDE: ‘C:\BC7\CHRTB.BI'

DIM OurChart AS ChartEnvironment


DIM Regions(4) AS STRING
DIM Consumption(4) AS SINGLE
DIM Exploded(4) AS INTEGER

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)

3 CALL DefaultChart(OurChart, cPie, cPercent) }


oe
ue
$e > Graphics
ANC 245
OAS

Finally, we fill the Title fields in the data structure OurChar


t, and we’re ready to place
the chart on the screen:

“ SINCLUDE: "Cs\BC7\FONTB.BI'
* SINCLUDE: "C:\BC7\CHRTB.BI'

DIM OurChart AS ChartEnvironment


DIM Regions(4) AS STRING
DIM Consumption(4) AS SINGLE
DIM Exploded(4) AS INTEGER

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)

CALL DefaultChart(OurChart, cPie, cPercent)

OurChart.MainTitle.Title = "Peanut Butter Consumption"


OurChart.MainTitle.TitleColor 15
OurChart.MainTitle.Justify = eCenter
OurChart.SubTitle.Title “Consumption (Tons)"
OurChart.SubTitle.TitleColor = 11
OurChart.SubTitle.Justify = cCenter
OurChart.ChartWindow.Border 3 cYes

To actually display it, we call ChartPie( ) with these arguments:

CALL ChartPie (OurChart, Regions(), Consumption(), Exploded(), 4)

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

Listing 5-8. A PDS Pie Chart.


" SINCLUDE: 'C:\BC7\FONTB.BI'
" SINCLUDE: ‘'C:\BC7\CHRTB.BI'

DIM OurChart AS ChartEnvironment


DIM Regions(4) AS STRING
DIM Consumption(4) AS SINGLE
DIM Exploded(4) AS INTEGER

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)

OurChart.MainTitle.Title = “Peanut Butter Consumption”


OurChart.MainTitle.TitleColor = 15
OurChart.MainTitle.Justify = cCenter
OurChart.SubTitle.Title = “Consumption (Tons)"
OurChart.SubTitle.TitleColor = 11
OurChart.SubTitle.Justify = cCenter
OurChart.ChartWindow.Border = cYes

> CALL ChartPieCOurChart, Regions( ), Consumption( ), Exploded( ), 4)


SLEEP

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

" SINCLUDE: 'C:\BC7\FONTB.BI'


* SINCLUDE: 'C:\BC7\CHRTB.BI'

DIM OurChart AS ChartEnvironment


DIM Regions(4) AS STRING
DIM Consumption(4) AS SINGLE

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)

> CALL DefaultChart(OurChart, cBar, CPlain)

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.)

Listing 5-9. A PDS Bar Graph. 1 of 2


* SINCLUDE: *C:\BC7\FONTB.BI'
* SINCLUDE: ‘C:\B8C7\CHRTB.BI'

DIM OurChart AS ChartEnvironment


DIM Regions(4) AS STRING
DIM Consumption (4) AS SINGLE

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)

CALL DefaultChart(OurChart, cBar, CPlain)


248 P& Advanced BASIC

Listing 5-9. A PDS Bar Graph. 2 of 2


OurChart.MainTitle.Title "Peanut Butter Consumption”
OurChart.MainTitle.TitleColor 45
OurChart.MainTitle.Justify cCenter
OurChart.SubTitle.Title = “Consumption (Tons)"
OurChart.SubTitle.TitleCcolor 11
OurChart.SubTitle.Justify = cCenter
OurChart.ChartWindow.Border 7 cYes

OurChart.XAxis.AxisTitle.Title *Consumption"
OurChart.YAxis.AxisTitle.Title "Region"

CALL Chart(OurChart, Regions(), Consumption(


), 4)
SLEEP

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'

DIM OurChart AS ChartEnvironment


DIM Regions(4) AS STRING
DIM Consumption(4) AS SINGLE

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)

CALL DefaultChart(OurChart, eLine, cLine)

Then we call Chart( ) again (Chart( ) plots bar, column, an d line


charts depending on
what you've asked for with DefaultChart( )). (See Listing 5- 10.)
eee > AHS
Graphics 249
24D

Listing 5-10. A Line Chart.


SINCLUDE: 'C:\BC7\FONTB.BI'
‘ SINCLUDE: 'C:\BC7\CHRTB.BI'

DIM OurChart AS ChartEnvironment


DIM Regions(4) AS STRING
DIM Consumption(4) AS SINGLE

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)

CALL DefaultChart(OurChart, cline, cLine)

OurChart.MainTitle.Title = “Peanut Butter Consumption"


OurChart.MainTitle.TitleColor = 15
OurChart.MainTitle.Justify = cCenter
OurChart.SubTitle.Title = "Consumption (Tons)"
OurChart.SubTitle.Titlecolor = 11
OurChart.SubTitle.Justify = cCenter
OurChart.ChartWindow.Border = cYes

OurChart.XAxis.AxisTitle.Title = “Region"
OurChart.YAxis.AxisTitle.Title = “Number”

> CALL Chart(OurChart, Regions(), Consumption(), 4)


SLEEP

And that’s it: our line chart is on the screen.


Now we’ve worked through pie charts, bar charts, and line charts. As you can see, the
PDS offers a good set of tools for presenting data.
Before we wrap up our tour of BASIC graphics, however, let’s include two advanced
functions to give us a little more power. The first one will tell us what video card is in the
computer, and the second one will tell us what the current X and Y pixel ranges are on
the screen.

What Video Card Do We Have?


It’s appropriate to end the Graphics chapter with a function that lets you investigate
what video adapter card is installed in the computer. This has always been a difficult
250 & Advanced BASIC

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):

GetVideoCard$ "MDA" = monochrome adapter card


“CGA” = color graphics adapter card
"EGA" = enhanced graphics adapter card
"PGA" = professional grahics adapter card
"VGA" = variable graphics array card

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$

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H1A00
CALL INTERRUPT(&H10, InRegs, OutRegs)
IF (OutRegs.ax AND &HFF) = &H1A THEN
Code = OutRegs.bx AND &HFF
SELECT CASE Code
CASE 1
GetVideoCard$S = “MDA
CASE 2
> Graphics 251

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$

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H1A00
CALL INTERRUPT(&H10, InRegs, OutRegs)
IF COutRegs.ax AND &HFF) = &HTA THEN
Code = OutRegs.bx AND &HFF
SELECT CASE Code
CASE 1
GetVideoCardsS “MDA

~ 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$

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H1A00
CALL INTERRUPT(&H10, InRegs, OutRegs)
IF COutRegs.ax AND &HFF) = &H1A THEN
Code = OutRegs.bx AND &HFF
SELECT CASE Code
CASE 1
GetVideoCard$ = "MDA"
CASE 2
GetVideoCards "CGA"

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

Listing 5-11. GetVideo Card$ Function. :


DECLARE FUNCTION GetVideoCard$ ( )
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 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)

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H1A00
CALL INTERRUPT(&H10, InRegs, OutRegs)
IF (OutRegs.ax AND &HFF) = SHIA THEN
Code = OutRegs.bx AND &HFF
SELECT CASE Code
CASE 1
a “MDA”
GetVideoCard$
CASE 2
“CGA”
GetVideoCard$
CASE 4 TO 5
EGA”
GetVideoCard$
CASE 6
GetVideoCardS = "PGA"
CASE 7 TO 8
"VGA"
GetVideoCard$ tl
END SELECT
EXIT FUNCTION
ELSE
InRegs.ax ul &H1200
InRegs.bx = &H10
CALL INTERRUPT(&H10, InRegs, OutRegs)
IF (OutRegs.bx AND &HFF) < > &H10 THEN
GetVideoCard$S = “EGA”
EXIT FUNCTION
ELSE
InRegs.ax = &HFOO
CALL INTERRUPT(&H10, InRegs, OutRegs)
IF (OutRegs.ax AND &HFF) = 7 THEN
GetVideoCardS = "MDA"
EXIT FUNCTION
ELSE
GetVideoCard$S = “CGA”
EXIT FUNCTION
END IF
END IF
END IF

END FUNCTION
254 }& Advanced BASIC

And here’s how to put GetVideoCard$( ) to work:

REM Example of GetVideoCard$()

DECLARE FUNCTION GetVideoCard$ ()

PRINT “The video card is: "; GetVideoCard$

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.

Finding the Screen’s Pixel Ranges


Let’s put together one last function to explore the graphics environment before finish-
ing up. Here we’re going to determine the number of pixels on the screen both horizon-
tally and vertically in the current screen mode. Let’s call this function
GetXYRanges%(XRange%, YRange%). Here are the outputs we might want this function
to produce:

XRange% Number of pixels horizontally in the current screen


mode
YRange% Number of pixels vertically in the current screen mode
GetXYRanges% INTEGER 0 = screen not in a graphics mode; INTEGER
1 = X (Y) range in XRange% (YRange%)

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

ES) dale Moda Pum C1-10,@ Mat-lale(-b/ ase lale (lak


DECLARE FUNCTION GetXYRanges% (XRangez, YRangexr)
REM This function returns the x and y pixel ranges for current graphics mode

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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

FUNCTION GetXYRanges% (XRange%, YRangez)

DIM InRegs AS RegTtype, OutRegs AS RegType


InRegs.ax = &HFOO
CALL INTERRUPT(&H10, InRegs, OutRegs)
Mode = (&HFF AND OutRegs.ax)

GetXYRanges% = 1

SELECT CASE Mode


CASE 4, 5,13, 19
XRangex = 320
YRangez% = 200
CASE 6, 10, 14
XRangex% = 640
YRangex = 200
CASE 8
XRangez = 160
YRange% = 200
CASE 9
XRange% = 320
YRangex = 200
CASE 15, 16
XRange% = 640
YRange% = 350
CASE 17, 18
XRange% = 640
YRange% = 480
CASE ELSE
GetXYRanges% = 0
END SELECT

END FUNCTION

There is no amazing programming going on here. Everything is already set up,


depending on the BIOS screen mode. Here’s how to use GetXYRanges%( ):
256 }& Advanced BASIC

REM Example of GetXYRanges


DECLARE FUNCTION GetXYRanges%(Xrangez, Yrange%)

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

THIS CHAPTER IS an exploration of the Indexed Sequential Access Method (ISAM)


system in the BASIC Professional Development System. If you don’t have the PDS, you
might still want to read along to get an idea of what’s available, especially if you’re
interested in databases. The ISAM system provides an easy way to set up a database
program from within BASIC (and the very fact that such a system is in the PDS shows the
depth of Microsoft’s commitment to BASIC). It’s a serious system, set up for serious
applications.

|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

— DIM MyFriend AS Friend

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

DIM MyfFriend AS Friend

~ 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

The Current Database Index


These indices themselves actually become part of the file (ISAM handles all this auto-
matically when you create a new index). However, only one index at a time can be the
current index. And whenever you make an index the current index, the apparent order of
the records in the file is changed to match its ordering.
Before you've created any indices, the NULL index is the current index (see Figure
6-6). If you ask for the first record in this file, you'll get record 1; the next record will be
record 2 and so forth, just mirroring the insertion order of the records.

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.

Our Database Program


Before actually writing any BASIC, we should mention that you'll need a little prepara-
tion before you can work with ISAM.
266 & Advanced BASIC

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.

There is a shorter program than PROISAMD.EXE named PROISAM.EXE,


which is all you need to run most ISAM program. Although using PRO-
ISAM.EXE lets you save RAM, it omits a few routines — including
CREATEINDEX.

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

DIM MyFriend AS Friend

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

DIM MyFriend AS Friend ;


— OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1
> Databasing 267

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:

OPEN "FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1


OPEN "FRIENDS.DAT” FOR ISAM Enemy “NotPals” AS #2

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

DIM MyFriend AS Friend

— OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1


*

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 :

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1

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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1

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..."

— CREATEINDEX 1, “FriendsByAge", 0, “Age"


«»

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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend “Pais” AS #1

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

PRINT “Sorting by age...”

CREATEINDEX 1, “FriendsByAge", 0, “Age”

— 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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1

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

PRINT “Sorting by age...”

CREATEINDEX 1, “FriendsByAge”, 0, "Age"

SETINDEX 1, “FriendsByAge”

— MOVEFIRST 1

DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

What we do is to move to the first record in the file:

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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1

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...”

CREATEINDEX 1, "FriendsByAge”, 0, “Age”

SETINDEX 1, “FriendsByAge”

MOVEFIRST 1

DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHELE NOT EOF(1)

PRINT “Sorting by height...”

+» CREATEINDEX 1, “FriendsByHeight”, 0, "Height"

*
rf

, except that now


The structure here is exactly parallel to the creation of FriendsByAge
this point, the database
we sort on the Height field and give the index a different name. At
file looks like Figure 6-12.
278 }& Advanced BASIC

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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1

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..."

CREATEINDEX 1, "FriendsByAge”, 0, “Age”

SETINDEX 1, “FriendsByAge"

MOVEFIRST 1

dO
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Sorting by height..."

CREATEINDEX 1, “FriendsByHeight”, 0, "Height"

— 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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend “Pais"™ AS #1

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

PRINT “Sorting by age..."

CREATEINDEX 1, “FriendsByAge", 0, "Age"

SETINDEX 1, “FriendsByAge"

MOVEFIRST 1

BO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Sorting by height..."

CREATEINDEX 1, "FriendsByHeight", 0, “Height”

SETINDEX 1, “FriendsByHeight"

MOVEFIRST 1

dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

— PRINT “Seeking a person with age of 33..."

— 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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend “Pals” AS #1

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..."

CREATEINDEX 1, “FriendsByAge", 0, "Age"

SETINDEX 1, "FriendsByAge"

MOVEFIRST 1

DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Sorting by height...”


:
> Databasing 283

CREATEINDEX 1, “FriendsByHeight", 0, "Height"

SETINDEX 1, “FriendsByHeight"

MOVEFIRST 1

dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Seeking a person with age of 33..."

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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend “Pals” AS #1

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

PRINT “Sorting by age...”

CREATEINDEX 1, “FriendsByAge", 0, “Age”

SETINDEX 1, "FriendsByAge”

MOVEFIRST 1

DO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Sorting by height..."

CREATEINDEX 1, “FriendsByHeight", 0, “Height”

SETINDEX 1, “FriendsByHeight”

MOVEFIRST 1

dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Seeking a person with age of 33..."

SETINDEX 1, “FriendsByAge"

MOVEFIRST 1

SEEKEQ 1, 33

IF EOFC(1) THEN
PRINT “No Match."
ELSE
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
END IF

— PRINT “Seeking people with height over seven feet..."

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

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1

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.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..."

CREATEINDEX 1, “FriendsByAge", 0, “Age”

SETINDEX 1, “FriendsByAge”

MOVEFIRST 1

dO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Sorting by height..."

CREATEINDEX 1, “FriendsByHeight”, 0, "Height"

SETINDEX 1, "FriendsByHeight"

MOVEFIRST 1

DO
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Seeking a person with age of 33..."


SETINDEX 1, “FriendsByAge"

MOVEFIRST 1

SEEKEQ 1, 33

If EOQFC1) THEN
PRINT “No Match.”
ELSE
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
END IF
> Databasing 287

PRINT “Seeking people with height over seven feet..."

SETINDEX 1, “FriendsByHeight”

MOVEFIRST 1

SEEKGT 1, 84

IF EOFC(1) THEN
PRINT "No Match."
ELSE
RETRIEVE 1, MyFriend
PRINT Myfriend.Name
END IF

PRINT “Deleting everyone over 40..."

ve].
ae SETINDEX 1, “FriendsByAge"

dO
SEEKGT 1, 40

IF NOT EOF(1) THEN


RETRIEVE 1, MyFriend
DELETE 1
END IF

LOOP UNTIL EOF(1)

PRINT “Here's who's teft...”

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.

Updating ISAM Files


Let’s say that Doug has a birthday, and his age changes from 33 to 34; we could delete
his record in FRIENDS.DAT and then INSERT a new one to correct his record, but the
ISAM system provides a far easier method.
To update records, we can use the UPDATE statement. In this case, all we have to do is
to make Doug’s record the current record, read it into a variable of TYPE Friend — such
as MyFriend — with RETRIEVE, change the Age field to 34, and then use the statement
UPDATE 1, MyFriend to update FRIENDS.DAT. This statement updates the current
record in file 1 using the values in MyFriend. The entire process looks like this:

TYPE Friend | :
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE :
DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend "Pals" AS #1

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

PRINT “Sorting by age..."

CREATEINDEX 1, "FriendsByAge", 0, "Age"

SETINDEX 1, “FriendsByAge"

MOVEFIRST 1

DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Sorting by height..."

CREATEINDEX 1, "“FriendsByHeight", 0, “Height”

SETINDEX 1, "“FriendsByHeight"

MOVEFIRST 1

DO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Seeking a person with age of 33...”

SETINDEX 1, "FriendsByAge”

MOVEFIRST 1

SEEKEQ 1, 33

IF EOFC1) THEN
PRINT “No Match.”
ELSE
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
END IF

“Seeking people with height over seven feet..."


PRINT

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

PRINT “Deleting everyone over 40..."

SETINDEX 1, “FriendsByAge”

bO
SEEKGT 1, 40

IF NOT EOFC1) THEN


RETRIEVE 1, MyFriend
DELETE 1
END IF

LOOP UNTIL EOF(1)

PRINT "Here's who's left...”

MOVEFIRST 1

dO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT "Changing Doug’s age to 34..."

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

Listing 6-1. Database Program Using ISAM.


TYPE Friend
Name AS STRING * 50
Age AS INTEGER
Height AS INTEGER
HomeTown AS STRING * 50
END TYPE

DIM MyFriend AS Friend

OPEN “FRIENDS.DAT" FOR ISAM Friend “Pals" AS #1

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...”

CREATEINDEX 1, “FriendsByAge", 0, “Age”

SETINDEX 1, “FriendsByAge”

MOVEFIRST 1

DO
RETRIEVE 1, MyFriend
PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Sorting by height..."

CREATEINDEX 1, "FriendsByHeight", 0, “Height”

SETINDEX 1, "“FriendsByHeight”

MOVEFIRST 1

DO
RETRIEVE 1, MyFriend
292 & Advanced BASIC

Listing 6-1. Database Program Using ISAM.


PRINT MyFriend.Name
MOVENEXT 1
LOOP WHILE NOT EOF(1)

PRINT “Seeking a person with age of 33..."

SETINDEX 1, "*FriendsByAge”

MOVEFIRST 1

SEEKEQ 1, 33

IF EOFC1) THEN
PRINT “No Match."
ELSE
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
END IF

PRINT “Seeking people with height over seven feet..."

SETINDEX 1, “FriendsByHeight”

MOVEFIRST 1

SEEKGT 1, 84

IF EOFC(1) THEN
PRINT "No Match."
ELSE
RETRIEVE 1, MyFfriend
PRINT MyFriend.Name
END IF

PRINT “Deleting everyone over 40..."

SETINDEX 1, “FriendsByAge"”

dO
SEEKGT 1, 40

IF NOT EOFC1) THEN


RETRIEVE 1, MyFfriend
DELETE 1
END IF

LOOP UNTIL EOF(1)

PRINT “Here's who's Left..."

MOVEFIRST 1

DO
RETRIEVE 1, Myfriend
PRINT MyFriend.Name
MOVENEXT 1
> Databasing 293

Listing 6-1. Database Program Using ISAM. 3 of 3


LOOP WHILE NOT EOF(1)

PRINT “Changing Doug's age to 34..."

. CREATEINDEX 1, "FriendsByName", 0, “Name”

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

Pi ted‘4 4 : ahiln-sinle call) wr aways dy Denhaido: ad seolo aw aa Seale — 7 ao § =

, $ 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)

; rae fy yay ote oul TY aa aatyel ray : ‘ fg 2} * 1 Bi Sk ral a | ud Coo

: —_ ete fab auibtn Lo Vey stat said zh ie an « ary!


‘ . +>
e =
ss _

“ss

,
Advanced Data
Handling and Sorting
eel

: sisd beonsvbA
~

pninoee bns pnilbns


> Advanced Data Handling and Sorting 297

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:

Type Symbol Bytes Range


INTEGER % 2 -~-32,/68 to 32,/67
LONG & 4 -2,147,483,648 to 2,147,483,647
SINGLE | 4 -3.402823E38 to -1.40129E-45
DOUBLE # 8 -1.79769313486232D308 to
-2.2250738585072D-308
CURRENCY @ 8 -$922337203685477.5808 to
$922337203685477.5807
STRING $ 32K Strings can range up to 32K characters
(bytes)

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

Savings@ = Savingsa@ ~- Renta


298 > Advanced BASIC

Savings® = Savingsa ~ Fooda


Savings@ = Savings@ - Billsa

PRINT “Money Left: $"; Savingsa

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.

The DATA Statement


The next step up in data handling is the DATA statement. Most BASIC programmers
are already familiar with DATA. Here’s an example in which we calculate the sum and
product of the numbers 1-10, stored in a DATA statement:

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

In this case, we define our data with the DATA statement:

DATA 152, 3,4, 55.6, 7, 6;°95 40


> Advanced Data Handling and Sorting 299

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:

— RESTORE "Start data over

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.

Listing 7-1. Array Example. 1 of 2


DIM Array(10, 2) AS CURRENCY <—
REM Fill Array(n,1) with today's sales:

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

REM Fill Array(n,2) with yesterday's sales:

Array(1, 2) = 9.67
300 P& Advanced BASIC

Listing 7-1. Array Example. ae) a2


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
Array(7, 2) = 91.36
Array(8, 2) = 12.73
Array(9, 2) = 16.12
Array(10, 2) = 7.98

PRINT ” SALES Cin $)”


PRINT “Yesterday Today”
PRINT “-----—6-=— 0 2 sees "
FOR i = 1 TO 10
PRINT Array(i, 1), ArrayCi, 2)
NEXT j
PRINT “%<-s------= 2 9 o==-+ "

Sum1a@ = 0
Sum2a = 0
FOR i = 170 10
Sum1a = Sum1a + ArrayCi, 1)
Sum2@ = Sum2@ + Array(i, 2)
NEXT i

PRINT Sumi, Sum2a; " = Total”

In this program, we're setting up an array of 10 rows and 2 columns to hold sales
values for the last two days:

DIM Array(10, 2) AS CURRENCY

REM Fill Array(n,1) with today's sales:

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

REM Fill Array(n,2) with yesterday's sales:

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

Figure 7-1 shows the array produced:


Col 1 Col 2
10.00 9.67 < Row1
53.00 3.5 < Row 2
7.17 8.97
9.67 10.00
87.99 78.33
1400 | 17.00
91.19 91.36
12.73 12.73
1.03 16.12
5.04 7.98
Figure 7-1
Now we can reach each day’s column of sales just by incrementing the column index.
In this format, we can perform parallel operations on parallel sets of data, like adding the
columns of sales to produce sums as in our example program:

DIM Array(10, 2) AS CURRENCY

REM FilLt Array(n,1)} with today's sales:

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

REM Fill Array(n,2) with yesterday's sales:

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

PRINT " SALES Cin $)"


PRINT “Yesterday Today”
PRINT “---<-=-~= wane"
FOR 1 = 1 TO 10
PRINT Array(i, 1), Array(i, 2)
NEXT 4 :
PRINT 88 encamoe thee oe ommom coo omconten cosHE

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

— DIM People(10) AS Person

And then we reference them like this:

TYPE Person
FirstName AS STRING * 20
LastName AS STRING * 20
END TYPE

DIM People(10) AS Person

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

4,.[ gates]? ) ey cite


Figure 7-2
otal
We find the next item in the list by referring to the pointer in the present item. At any
time, you can add another data item to the list, as long as you update the current pointer
to point to the new data item.
A prominent example of a linked list in your computer is the File Allocation Table
(FAT) on disks. This is a list of the clusters allocated to files for storage. Files are stored
cluster by cluster, and for each cluster on the disk, there is one entry in the FAT.

A cluster is the minimum size of disk storage allocation; on diskettes, clusters


are two sectors — 1,024 bytes — long. This means, incidentally, that the
amount of free diskette space is always reported in units of 1,024 bytes.

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

Vice President Major Moving “up”


the career
Director Captain track

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:

REM Linked List Example

— TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE

DIM People(10) AS Person

Now we fill the Rank fields — we can fill them in any order; the pointers will keep
them straight:

REM Linked List Example

TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE

DIM Peopte(10) AS Person

— 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:

REM Linked List Example

TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE

DIM People(10) AS Person


> Advanced Data Handling and Sorting 307

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.)

Listing 7-2. A Linked List Example. ae)


REM Linked List Example

TYPE Person
Rank AS STRING * 20
SuperiorPointer AS INTEGER
END TYPE

DIM Peopte(10) AS Person

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

Listing 7-2. A Linked List Example. yao) 4

InString$ = INKEYS
LOOP WHILE InString$S = ""
PRINT

SELECT CASE InString$ 'Get pointer to first record.


CASE 1"
Index = 1
CASE "2"
Index = 8
END SELECT

— 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.

Record 1 Record 2 Record 3

es Oia sea i ee
| Pointer _| aero aa
Figure 7-8

Record 1 Record 2 Record 3

| |
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).

Record 1 Record 2 Record 3

Increasing data values >


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).

Binary Tree’s Root > Record 2

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

First name = “Denise”


Age = 23
First name = “Ed”
Age = 46

First name = “Nick”


Age = 47
First name = “Dennis”
Age = 42
First name = “Doug”
Age = 33
First name = “Margo”
Age = 27
First name = “John”
Age = 41
First name = “Cheryl”
Age = 28

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:

REM Binary Tree example.

+ TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE

DIM People(10) AS Person

Then we fill each record:

REM Binary Tree example.

TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE

DIM People(10) AS Person

~ 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:

REM Binary Tree example.

TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE

DIM People(10) AS Person

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

— BinaryTreeRoot% = 5§ ‘Doug has about the median age

PRINT “Searching for a person 46 years otd..."

CurrentRecord%’ = BinaryTreeRoot%

Then we check to see if that person is 46 years old:

REM Binary Tree example.

TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE

DIM People(10) AS Person

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

BinaryTreeRoot% = 5 ‘Doug has about the median age

PRINT “Searching for a person 46 years old..."

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

REM Binary Tree example.

TYPE Person
FirstName AS STRING * 20
Age AS INTEGER
NextYoungerPerson AS INTEGER
NextOlderPerson AS INTEGER
END TYPE

DIM People(10) AS Person

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(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

BinaryTreeRoot% = 5 "Doug has about the median age

PRINT “Searching for a person 46 years old..."

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:

17S) 2 eee Oe)

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):

DIM Array(9) AS INTEGER <—

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:

DIM Array(9) AS INTEGER

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

PRINT "* i Array(i)"


PRINT "~~~ ae eee
FOR 7 = 1 7T0 9 wt
PRINT i, Array(i)
NEXT i
PRINT
PRINT “Sorting...”

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:

DIM Array(9) AS INTEGER

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 Arrayti)"


PRINT “mmm a ce "
FOR i = 1 T0 9
PRINT i, Array(i)
NEXT j
PRINT
PRINT “Sorting...”

NumItems% = UBOUND(Array, 1)
PartitionSize% = INTC(NumItems% + 1) / 2)
> Advanced Data Handling and Sorting 321

= bo

~ LOOP WHILE PartitionSize% > 0

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).

Current Partition aay pane Partition

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):

DIM Array(9) AS INTEGER

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

This is the element-by-element comparison. We'll go from Array(Low%) to


Array(High%) in the current partition, where Low% is the array index at the beginning of
this partition and High% is the index of the element at the end, comparing each element to
the corresponding one in the next partition:

DIM Array(9) AS INTEGER

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:

DIM Array(9) AS INTEGER

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"


PRIN Ser eer :
FOR i = 1 T0 9
PRINT i, Array(i)
NEXT i
PRINT
PRINT "Sorting..."
324 P Advanced BASIC

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.

Listing 7-3. Shell Sort Program. 1 of 2


DIM Array(9) AS INTEGER

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

PRINT " 7 Array(i)"


PRINT “=-- i

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

Listing 7-3. Shell Sort Program.


DO
NumPartitions% = (NumItems% + 1) / PartitionSize%
Low% = 1
FOR i = 1 TO NumPartitions% ~ 1
High% = Low% + PartitionSize% - 1
IF Hight > NumItems% - PartitionSize% THEN High% = a
NumItems% -— PartitionSize%
FOR j = Low% TO High%
IF Array(j>) > Array(j + PartitionSize%) THEN
SWAP Array(j), Array(j + PartitionSize%)
END IF
NEXT j
Low% = Lowk% + PartitionSize%
NEXT i
PartitionSize% = PartitionSize% 2
LOOP WHILE PartitionSizezx > 0

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).

Listing 7-4. Two-dimensional Shell Sort Program.


DIM Array(9, 4) AS INTEGER

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

Listing 7-4. Two-dimensional Shell Sort Program


PRINT "Sorting..."
NumItems% = UBOUND(Array, 1)
Pet%isionSize% = INT((NumItems% + 1) / 2)—>
po
NumPartitions%’ = (NumItems% + 1) / PartitionSize%
Low% = 1
FOR i = 1 TO NumPartitions% - 1
High% = Low% + PartitionSize% ~ 1
IF High% > NumIltems% - PartitionSize% THEN High% =
NumItems% - PartitionSizez
FOR j = Low% TO High%
IF Array(j, Col%) Array(j + PartitionSizez, Col%) THEN
>
SWAP Array(j, Col%) > Array(j + PartitionSizez, Col“)
END IF :
NEXT j
Low% = Low% + PartitionSize%
NEXT i
PartitionSize% = PartitionSize% \ 2
LOOP WHILE PartitionSizez > 0

PRINT :
PRINT " i Array(i,1)"
PRINT “=—— eee eee x
FOR i = 1 TO 9
PRINT i, Arrayfi, 1)
NEXT i ‘

And now we're able to sort two-dimensional arrays on a specified column.


That concludes our tour of shell sorts. Let’s turn to QuickSorts next.

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.

Recursion is one of the most powerful programming techniques. Its virtues


include making code compact and easier to debug. On the other hand, you
should know that the overhead involved in the successive calls can make it
more inefficient in memory usage and speed.

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):

CALL SortQuick(Array(>), SortFrom%, SortTo%)

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:

SUB SortQuick (Array( ) AS INTEGER, SortFromZ, SortTo%)

IF SortFrom% >= SortTo% THEN EXIT SUB


IF SortFrom% + 1 = SortTo% THEN 'Final case of recursion
IF Array(SortFrom%) > Array(SortTo%) THEN
SWAP Array(SortFrom%), Array(SortTo%)
END IF
328 P» Advanced BASIC

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:

SUB SortQuick (Array


€ ) AS INTEGER, SortFrom%, SortTo%)

IF SortFrom% >= SortTo% THEN EXIT SUB


IF Sortfrom% + 1 = SortTo% THEN ‘Final case of recursion
IF Array(SortFromZ) > Array(SortTo%) THEN
SWAP Array(SortFrom%), Array(SortToZ%)
END IF

cd ELSE "Have to split problem and call again


AtRandom = (SortFrom% + SortTo%) \ 2
Test = ArrayCAtRandom)
SWAP ArrayCAtRandom), Array(SortTo%)

~ bO
REM Split into two partitions

~> FOR i = SortFrom% TO SortTo% - 1


IF Array(i) > Test THEN EXIT FOR
NEXT j

> IF i < j THEN SWAP Array(i), Array(j)


= LOOP UNTIL i >= j
*
e =

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:

SUB SortQuick CArray{ ) AS INTEGER, SortFrom%Z, SortTo%)

IF SortFrom% >= SortTo% THEN EXIT sUB ;


IF SortFrom% + 1 = SortTo% THEN ‘Final case of recursion
IF Array(SortFrom%) > Array(SortTo%) THEN
an SWAP Array(SortFrom%Z), Array(SortTo%)
IF
i
> Advanced Data Handling and Sorting 329

ELSE "Have to split problem and call again


AtRandom = (SortFrom% + SortTo%) \ 2
Test = Array(AtRandom)
SWAP ArrayCAtRandom), Array(SortTo%)

DO
REM Split into two partitions

FOR i = SortFrom% TO SortTo% - 1


IF ArrayCi) > Test THEN EXIT FOR
NEXT i

= FOR j = SortTo% TO i + 1 STEP -1


If Array{j) < test THEN EXIT FOR
NEXT j

IF i < j THEN SWAP ArrayCi), Array(j)

LOOP UNTIL i >= j

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):

SUB SortQuick (Array() AS INTEGER, SortFrom%, SortTo%)

IF SortFrom% >= SortTo% THEN EXIT SUB


IF SortfFrom% + 1 = SortTo% THEN ‘Final case of recursion
IF Array(SortFrom%) > Array(SortTo%) THEN
SWAP Array(Sortfrom4), Array(€SortTo%)
END IF

ELSE "Have to split problem and call again


AtRandom = (SortFrom% + SortTos) \ 2
Test = Array(AtRandom)
SWAP Array(AtRandom), Array(SortToz)

dO

FOR i = SortFrom% TO SortTos - 1.


IF Array(i) > Test THEN EXIT FOR
NEXT i

FOR j = SortTo% 10 1 + 1 STEP -1


IF Array(j) < Test THEN EXIT FOR
NEXT j

IF i < j THEN SWAP Array (i), Array(j)

LOOP UNTIL i >= j

SWAP Array(SortTo%), Array(lid


330 & Advanced BASIC
th ae Sra ede nes che alia Mecca andy ects isl WOOT Bee

REM Now work on two partitions

a CALL SortQuick(Array( ), SortFrom%Z, i - 1)


CALL SortQuick(Array(), i + 1, SortTo%)

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.

Listing 7-5. QuickSort Program.


DECLARE SUB SortQuick (Array) AS INTEGER, SortFrom%, SortTo%)

DIM Array(9) AS INTEGER

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

CALL SortQuick(Array(}), 1, UBOUND (Array, 1))

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

SUB SortQuick (Array( ) AS INTEGER, SortFrom%, SortToZ)

IF SortFrom% >= SortTo% THEN EXIT SUB


IF SortFrom% + 1 = SortTo% THEN ‘Final case
IF Array(SortFromzZ) > Array(SortTo%) THEN
a SWAP Array(SortFrom%), Array (SortTo%)
D IF
> Advanced Data Handling and Sorting 331

Listing 7-5. QuickSort Program.


ELSE "Have to split problem
AtRandom = (SortFrom% + SortTo%) \ 2
Test = ArrayCAtRandom)
SWAP ArrayCAtRandom), Array(SortTo%)

dO

FOR i = SortFrom% TO SortTo% - 1


IF Array(i) > Test THEN EXIT FOR
NEXT i

FOR j- = SortTo% TO i + 1 STEP -1


IF Array(j) < Test THEN EXIT FOR
NEXT j

If i < j THEN SWAP Array(i), Array(j)

LOOP UNTIL i >= j

SWAP Array(SortTo%), Array(i)

CALL SortQuick(Array(), SortFrom%, i - 1)


CALL SortQuick(Array(), i + 1, SortTo%)

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%):

Listing 7-6. Two-dimensional QuickSort Program.


DECLARE SUB Sort@Quick (Array( ) AS INTEGER, SortFrom%, SortToz%, Col%)

DIM Array(9, 4) AS INTEGER

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

Listing 7-6. Two-dimensional QuickSort Program.


PRINT i, ArrayCi, 1)
NEXT iCol% = 1 |
CALL SortQuick(Array(>), 1, UBOUNDCArray, 1), Col%) <

PRINT
PRINT “Sorting..."
PRINT
PRINT “ ji Array(i,1)"
PRINT "==~ aaaa
FOR i = 1 TO 9
PRINT i, ArrayCi, 1)
NEXT i

SUB SortQuick CArray() AS INTEGER, SortFrom%, SortTo%, Col%)

IF SortFrom% >= SortTo% THEN EXIT SUB


IF SortFrom% + 1 = SortTo% THEN ‘Final case
IF Array(SortFrom%, Col%) > Array(SortTo%, Col%) THEN
SWAP Array(SortFrom%Z, Coi%), Array(SortTo%, Col%)
END IF

ELSE "Have to split problem


AtRandom = (SortFrom% + SortTo%) \ 2
Test = ArrayCAtRandom, Col%)
SWAP ArrayCAtRandom, Col%), Array(SortTo%, Col%)

DO

FOR i = SortFrom% TO SortTo% - 1


If ArrayC€i, Col%) > Test THEN EXIT FOR
NEXT i

FOR j = SortTox% TO i + 1 STEP -1


IF Array(j, Col%) < Test THEN EXIT FOR
NEXT j
IF i < j THEN SWAP ArrayCi, Col%), Array(€j, Col%)

LOOP UNTIL i >= j

SWAP Array(SortTo%, Col%), Arrayli, Col%)

CALL SortQuickCArray(), SortFrom%, i - 1, ColZ%>


CALL SortQuick(Array(), i + 1, SortTo%, Col%)

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

Searching Your Data


Now that we've ordered our data, it becomes much easier to search through. If the data
is unordered, we’d have no choice but to simply check one value after another until we
found a match, as shown in Listing 7-7.

ES) (lal yey @um OLale)ae(-1e-1e Mi


BY-1¢- Mct-v-1
ae ae
REM Unordered Search

DIM Array(9) AS INTEGER

Array(1) 9
Array(2)
Array(3)
Array(4)
Array(5)
Array(6)
Array(7)
Array(8)
Array (9) innnrnnunne
ON
UMW
ANAK

PRINT “Searching the unordered list for the value 1."

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:

REM Ordered Search

DIM Array(9) AS INTEGER

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%:

REM Ordered Search

DIM Array(9) AS INTEGER

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:

REM Ordered Search

DIM Array(9) AS INTEGER

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

Search this partition

+ LOOP WHILE Partition% > 0

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

REM Ordered Search

DIM Array(9) AS INTEGER

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
.
«

LOOP WHILE Partition% > 0


*
»

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:

REM Ordered Search

DIM Array(9) AS INTEGER

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%

LOOP WHILE Partition% > 0


.
.

But if the search value is smaller, on the other hand, we want to move to the lower
partition (which holds lower values):

REM Ordered Search

DIM Array(9) AS INTEGER

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).

Listing 7-8. An Ordered Search Program.


REM Ordered Search

DIM Array(9) AS INTEGER

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

REM Can only find straddled numbers, so add these tests:

IF Array(1) = SearchValue% THEN


PRINT "Value of"; SearchValue%; “in element 1"
ef
se END IF

IF Array(UBOUND(Array, 1)) = SearchVatue% THEN


coe ey "Value of"; SearchValue%; "in element"; UBOUNDCArray, 1)
> Advanced Data Handling and Sorting 339

And we're done with the ordered search.

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 _

ee Edis vw HRSA - 1ee


mod eon
laa le a on A tl gm
_

Heat " -b a

\7 A there “x ' ao ¥

) « die tow anes ne |


(
i ali ey po dea
gis wits
waiy odat' oe geet
gilt S11 Rad a
~~ j —~
‘ > é&ve Si i 2

1} rt ' or & ‘

gw’. i | ' 3 cy iar igena hia “eat


=<

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

BEFORE WE TACKLE assembly language, let’s take a look at an important part of


program development, one the advanced programmer should be familiar with, namely
debugging. Bugs are an unfortunate fact of life, especially when long or complex pro-
grams are involved. But the tools that modern programmer has to find and fix bugs have
never been better or more powerful.
We'll look at two debuggers in this chapter: the built-in debugger in QuickBASIC, and
the stand-alone debugger named CodeView. Each of these are appropriate at different
times. If you’re using QuickBASIC primarily, it’s natural to reach for the Debug window
when you need it. If you’re using the command-line compiler, you'll probably use
CodeView to debug your programs. Let’s start by getting the bugs out of a program under
QuickBASIC.

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):

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"

— 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

Finally, we can print out the result, name by name:

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"
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

Unfortunately, this is the result of the program:

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--__—_

<ShifttF1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> 00018:033

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

FOR i=i TO 10 Toggle Breakpoint F9


FOR j=i TO 10 Clear All Breakpoints
IF Name Break on Errors
Watchpoint...
Set Next Statement

END IF
NEXTj

Fi = Help Enter = Display Menu Esc = Cancel Arrow = Next Item

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).

File Edit View Search Run Debug Calls Options


Names(5) = “Frank”
Names(6) = “Todd”
Names(7) = “George”
&> Debugging 347

Name Add Watch


Name
Name Enter expression to be added to Watch window:

Poa Molina Berl od utsirens sew Tanol oy od


<Cancel> <Help>

Names(j) =Temp$
END IF
NEXTj
NEXT i
Immediate

<Shift+F1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> 00018:033

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.

File Edit View Search Run Debug Calls Options


ALPHABAS Names(i): -
ALPHABAS Names(j): eo
Names(8) = “Ralph”
Neen = “Leonard”
Names(10) = “Thomas”

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

<Shift+F 1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> 00018:033

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.

File Edit View Search Run Debug Calls Options


ALPHABAS Names(i):
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)
Names(j) = Names(j)
Names(j) = Temp$
END IF

<Shift+F1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> 00018:033

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

File Edit View Search Run Debug Calls Options


ALPHABAS Names(i):
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)
Names(j) = Names(j)
Names(j) = Temp$
END IF

<Shift+F 1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> 00018:033

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).

File Edit View Search Run Debug Calls Options Help


Add Watch...
ALPHABAS Nameeth Instant Watch... <—
ALPHABAS Names(j): Watchpoint...
Delete Watch...
Names(7) = “George” Delete All Watch
Names(8) = “Ralph”
Names(9) = “Leonard” Trace On
Names(10) = “Thomas” History On

FOR i= i On10 Toggle Breakpoint F9


FOR j =iTO 10 Clear All Breakpoints
F Name _ | Break on Errors
Watchpoint...
Set Next Statement

END IF
NEXT j
NEXT i
Immediate—£$—@£—M———————

F1 = Help Enter = Display Menu Esc = Cancel Arrow = Next Item

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

<Shift+F1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> _|00018:033

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:

DIM Names(10) ASSTRING


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 = 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 —__—_

<Shift+F1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> 00018:033

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

ALPHABAS Namesi(i): “John” = <—


ALPHABAS Names(j): “Edward” <—

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. ———

<Shift+F1=Help> <F6=Window> <F2=Subs> <F5=Run> <F8=Step> 00018:033

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:

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”
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).

METS) Clave mtsomPam BL-delelele(-vom-Nielar-lel-1¢r4


ale mmaagereleclai
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”

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

And this is the final result when we run it:

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):

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"

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:

D:\>BC /Z4 ALPHA;

Microsoft (CR) QuickBASIC Compiler Version 4.50


(C) Copyright Microsoft Corporation 1982-1988.
ALL rights reserved.
Simultaneously published in the U.S. and Canada.
43997 Bytes Available
42104 Bytes Free

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:

Dz\> LINK /Co ALPHA;

Microsoft (R) Overlay Linker Version 3.69 _


Copyright (C) Microsoft Corp 1983-1988. All rights reserved.

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

Obviously, there is a problem. We can debug ALPHA.EXE with CodeView by typing


CV ALPHA. The CodeView screen shown in Figure 8-11 appears:

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.

File Edit View Search Run Debug Calls Options Help


ocal
source 1 CS:IP ALPHABAS (ACTIVE
1: DIM Names(10) AS STRING
2:
3:
4:
5: Names(1) = “John”
6: Names(2) = “Tim”
7: Names(3) = “Edward”
8: Names 3)= “Samuel”
9: Names(5) = “Frank”
10: Names(6) = “Todd”
11: Names(7) = “George”
12: Names(8) = “Ralph”
ommandad
v

<F8=Trace> <F10=Step> <F5=Go> #<F6=Window> <F8=Display>

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

20: Names p= Names(j)


PA\s Names(j) = Temp$
22: END IF
23: NEXT j

<F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>

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

<F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>

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

20: Nentent} = Names(j)


21: Names(j) = Temp$
22: END IF
23: NEXTj
Command
>

<F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>


Figure 8-14

Every one of the CodeView windows may be independently scrolled, and, if an


expression goes off the screen, we can always use the scroll bars to display it
fully.

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

<F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>


Figure 8-15
Execution continues until it hits our breakpoint. Then we can examine the watch
window. As shown in Figure 8-16, Names(i) and Names(j) are still null strings, although
they should have been assigned to character strings already.
> Debugging 359

File Edit View Search Run Watch Options Calls Help


ee ete. md eee
Names(j)=*” <—
ource1 CS:IP ALPHABAS (ACTIVE)
12: Nemec = “Ralph”
13: Names(9) = “Leonard”
:a Names(10) = “Thomas”

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

<F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>

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

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”

~ 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.

And we can change it to FOR i = 1 TO 10. Unfortunately, the partially debugged


program still gives us the same output:

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”

16: FOR i=i TO 10


iz 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$-AM>-_
22: END IF
23: N j

<F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>

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

<F8=Trace> <F10=Step> <F5=Go> <F6=Window> <F3=Display>

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:

DIM Names(10) AS STRING

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

Listing 8-2. Debugged Alphabetizing Program


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”

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

And the output is correct as well:

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

< ng PAE: ‘an es era: ——


><. Aegean
eeicets
a ast ‘em:
Seem et ek ha eo GOOG nen (Qe
ingen. phe iS ea im
— —— mene

Hi ar’ aol Ss Cabaagyl a ior ue nioreiaiioe, EINE


ve Sievay cowreves, 1 be Garg p Saas cries iy OR tan
B42 ‘

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
&

20d bas 20ia to a oe


ore 7
a
ie 7 ; Lees

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.

The BIOS Interrupts


The BIOS interrupts are are given numbers 0 through GHI1E Each of these interrupts
corresponds to prewritten assembly language code that is already resident in memory,
and that we can reach through INTERRUPT( ) or INTERRUPTX( ). The low-level BIOS
interrupts, up to &H10, are not often used by programmers, however, with the exception
of interrupt 9. This is the hardware interrupt that reads keys from the keyboard. Every
time a key is struck, an interrupt 9 is executed. It’s possible to intercept this interrupt,
and handle these keystrokes yourself, which is the basis of many TSR pop-up programs.
Here’s what the interrupts 0 through GHF do:

Interrupt 0 Divide By 0 (Generated if a program divides by 0)


Interrupt 1 Single Step (Used by debuggers)
Interrupt 2 NonMaskable Hardware Interrupt (NMI)
Interrupt 3 Breakpoint (Used by debuggers)
Interrupt 4 Overflow
Interrupt 5 Print Screen
368 » Advanced BASIC

Interrupts 6 and 7 Reserved


Interrupt 8 Time of Day
Interrupt 9 Keyboard
Interrupt QHA Reserved
Interrupts GHB-GHF __ BIOS end interrupt routine, resets interrupt
handler

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:

Interrupt GH10 Service 0 Set Screen Mode


Interrupt @H10 Service 1 Set Cursor Type
Interrupt @H10 — Service 2 Set Cursor Position
Interrupt GH10 Service 3 Find Cursor Position
Interrupt @H10 Service 4 Read Light Pen Position
Interrupt @H10 Service 5 Set Active Display Page
Interrupt GH10 Service 6 Scroll Active Page Up
Interrupt @H10 Service 7 Scroll Active Page Down
Interrupt GH10 Service 8 Read Attribute/Character at Cursor
Position
Interrupt GH10 Service 9 Write Attribute/Character at Cursor
Position
Interrupt @GH10_ Service GHA Write Character ONLY at Cursor
Position
Interrupt @GH10 Service @HB Set Color Palette
Interrupt GH10 Service @HC Write Dot (Set Pixel)
Interrupt @GH10 = Service GHD Read Dot (Read pixel color)
Interrupt @H10 Service QHE Teletype Write to Active Page
Interrupt @H10 Service @HF Return Video State
Interrupt @H10 Service @H10 Set Palette Registers
Interrupt @H10 Service @H10 Function 0 Set Individual Palette
Register
> A Tour of BIOS and DOS’ 369

Interrupt @H10 Service @H10 Function 1 Set Overscan (Border)


Register
Interrupt @GH10 Service @H10 Function 2 Set All Palette Registers
Interrupt GH10_— Service @H10 Function 7 Read Individual Palette
Register
Interrupt GH10_— Service @H10 Function 8 Read Overscan (Border)
Register
Interrupt @GH10 Service @H10 Set DAC Register
Function @H10
Interrupt @GH10_— Service @H10 Set DAC Registers
Function G@H12
Interrupt GH10 Service @H10 Select Color Page Mode
Function @H13
Interrupt GH10 Service GH11 Character Generator
Interrupt GH10 Service G@H12 Alternate Select

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.

The 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:

BIOS Display § Number Adapter Maximum pages


mode lines of colors cards allowed
0 40x25 B&W text CGA,EGA,VGA 8
1 40x25 Color text CGA,EGA, VGA 8
2 80x25 B&W text CGA,EGA, VGA 4 (CGA) 8 (EGA,
VGA)
3 80x25 Color text CGA, EGA, VGA 4 (CGA) 8 (EGA,
VGA)
370 » Advanced BASIC

4 320x200 4 CGA, EGA, VGA ti


5 320x200 B&W CGA, EGA, VGA 1
6 640x200 2 (on or off) CGA, EGA, VGA 1
ri 80X25. Monochrome MDA, EGA, VGA 1 (MDA) 8 (EGA,
VGA)
8 160x200 16 PCjr 1
9 320x200 16 PCIE 1
A 640x200 1 PCjr 1
B Reserved for future use.
cs Reserved for future use.
D 320x200 16 EGA, VGA 8
E 640x200 16 EGA, VGA 4
F 640x350 monochrome EGA, VGA Z
10H 640x350 16 EGA, VGA 2
11H 640x480 2 VGA 1
12H 640x480 16 VGA 1
13H @320x200:256 VGA 1

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( ):

DIM InRegs AS RegTlype, OutRegs AS RegTtype


~ InRegs.ax = &HFOO
CALL INTERRUPT(&H10, InRegs, OutRegs)

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:

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &HFOO
CALL INTERRUPT(&H10, InRegs, OutRegs)
~ VideoMode% = &HFF AND OutRegs.ax _

And that’s all there is to it. Listing 9-1 shows the whole function.
> A Tour of BIOS and DOS 371

Listing 9-1. VideoMode% Function — Determines Video Mode.


DECLARE FUNCTION VideoModex ( )
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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

FUNCTION VideoMode%

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &HFQO
CALL INTERRUPT(&H10, InRegs, OutRegs)
VideoMode% = &HFF AND OutRegs.ax

END FUNCTION

And here’s a short program that puts VideoMode%( ) to work:

REM Example of VideoMode

DECLARE FUNCTION VideoMode% ( )

Mode% = VideoMode% e

PRINT “Mode ="; Mode%

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.

Determining Equipment Installed in Your Computer


a math
There’s a lot of power in interrupt @H11. For example, you can see whether
. This interrupt fills the ax
coprocessor, an internal modem, printer, or a mouse is installed
register like this, bit by bit:
372 » Advanced BASIC

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)

Let’s develop a small function, GetEquipment%( ), to return this information to us.


The actual code will be simplicity itself. We just make use of interrupt @H11, and, upon
return, the ax register will be packed full of bits indicating the equipment status. We can
set GetEquipment% to that value and return:

DIM InRegs AS RegType, OutRegs AS RegType


CALL INTERRUPT(8&H11, InRegs, OutRegs?)
> GetEquipment% = OutRegs.ax

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.

Listing 9-2. GetEquipment% — Returns Installed Equipment.


DECLARE FUNCTION GetEquipment% ( )

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

Listing 9-2. GetEquipment% — Returns Installed Equipment.


flags AS INTEGER
END TYPE

DECLARE SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)

FUNCTION GetEquipment%

REM Returns this word, bit by bit:


REM Bits Means
REM --~- a ec om sa cee ee, waliaadaceam caentotes rh te
Cstaeciuatheateatiaalaadoan
REM 15,14 Number of Printers installed
REM 13 Internal modem installed? (1 = yes)
REM 12 Not used
REM 11,10,9 Number of RS-232 cards attached
REM 8 Not used
REM 7,6 Number of diskette drives. 00 = 1 drive; 01 = 2 drives
REM 5,4 Video type. 01 = 40x25 color; 10 = 80x25 color; 11 = 80x25 mono
REM 3 Not used
REM 2 Mouse installed? (1 = yes)
REM 1 Math coprocessor installed? (1 = yes)
REM 0 Diskettes installed? (1 = yes)

DIM InRegs AS RegType, OutRegs AS RegType


CALL INTERRUPT(&H11, InRegs, OutRegs)
GetEquipment% = OutRegs.ax

END FUNCTION

This example program, for example, checks whether or not a math coprocessor is
installed:

REM Example of GetEquipment%

DECLARE FUNCTION GetEquipment% ()

Check% = GetEquipment%

IF (Check% AND 2) THEN


PRINT “You have a math coprocessor."
ELSE
PRINT “You do not have a math coprocessor."
END IF

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:

Interrupt @H13 Service 0 Reset Disk


Interrupt @H13 Service 1 Read Status of Last Operation
Interrupt @H13 __ Service 2 Read Sectors into Memory
Interrupt @H13 __ Service 3 Write Sectors to Disk
Interrupt @H13 Service 4 Verify Sectors
Interrupt @H13__ Service 8 Return Drive Parameters
Interrupt GH13_— Services @HA and SHB Reserved
Interrupt @H13_— Service @HC Seek
Interrupt @H13_— Service @HD Alternate Disk Reset
Interrupt @H13_— Services @HE Reserved
and &HF
Interrupt @H13 Service @H10 Test Drive Ready
Interrupt @H13_— Service @H11 Recalibrate Hard Drive
Interrupt @H13 @H12 - GH14 _ Diagnostic Services
Interrupt @H13__ Service &H19 Park Heads PS/2 Only

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 @H14 AH=0 Initialize RS232 Port


Interrupt @H14 AH=1 Send Character Through Serial Port
Interrupt @H14 Ali=2 Receive Character From Serial Port
Interrupt @H14 AH=3 Return Serial Port’s Status
> A Tour of BIOS and DOS 375

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 @H16 Service 0 Read Key from Keyboard


Interrupt GH16 Service 1 Check if Key Ready to be Read
Interrupt GH16 Service 2 Find Keyboard Status

Interrupt @H17 handles the printer:

Interrupt @H17 Service 0 Print character in al


Interrupt @H17 Service 1 Initialize Printer Port
Interrupt G@H17 Service 2 Read Printer Status into ah

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):

Interrupt GHIA Service 0 Read Time of Day


Interrupt GHIA Service 1 Set Time of Day

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 @H1B_ Keyboard Break Address


Interrupt @GH1C_ Timer Tick Interrupt
Interrupt GH1D_ Video Parameter Tables
Interrupt GH1E Diskette Parameters
Interrupt GH1F Graphics Character Definitions

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.

The DOS Interrupts


The DOS interrupts go from &H20 to GH3E The first of these, interrupt &H20, is
called at the assembly language level when a program is done and wants to return control
to DOS. This is the interrupt that places the C\> prompt back on the screen: Interrupt
S&H20 Program Terminate. With the introduction of other methods of ending programs,
interrupt @H20 has become slightly obsolete. One other way of ending a program is
interrupt GH21 service GH4C, which allows you to return an error code indicating
success or failure.
Now we come to the monster interrupt, the biggest of them all, interrupt @H21.
Almost all of DOS’ capabilities — handling files, reading keys, printing on the screen —
are wrapped up in the services of this interrupt. This is the interrupt that all the DOS
commands (such as COPY, FORMAT, or DIR) use.
You may have noticed that we can print on the screen and read keys with some of the
BIOS services as well, and that’s true. However, the DOS services are more sophisticated
than the low-level BIOS services; in fact, many of the DOS services call the BIOS services
themselves for low-level control.
Unfortunately, there’s no real plan or layout to the interrupt &H21 services. Roughly,
however, the first ones deal with character input and output (except for service 0, which
does exactly the same thing as interrupt @H20):
> A Tour of BIOS and DOS 377

Interrupt @H21 Service 0 Program Terminate


Interrupt @H21 Service 1 Keyboard Input
Interrupt @H21 Service 2 Character Output on Screen
Interrupt @H21 Service 3 Standard Auxiliary Device Input
Interrupt GH21 Service 4 Standard Auxiliary Device Output
Interrupt @H21 Service 5 Printer Output
Interrupt GH21 Service 6 Console I/O
Interrupt @H21 Service 7 Console Input Without Echo
Interrupt GH21 Service 8 Console Input w/o Echo with *C
Check
Interrupt GH21 Service 9 Character String Print
Interrupt @H21 = Service @HA String Input
Interrupt @H21 = Service @HB_ Check Input Status
Interrupt GH21 = Service @HC Clear Keyboard Buffer and Invoke
Service

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:

Interrupt @H21 = Service @HD Disk Reset


Interrupt @H21 = Service @HE Select 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.

Setting the Default Disk


We can set the default disk with the function we’re going to write, SetDefaultDisk%( ).
We should make this function return 0 if there’s a problem (i.e., the target disk doesn’t
exist), and 1 for success. Here’s how we could use SetDefaultDisk%( ):

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

+ IF LEN(Drive$) <> 1 THEN EXIT FUNCTION

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

> IF LENCDrive$) <> 1 THEN EXIT FUNCTION


InRegs.ax = &HEOO
InRegs.dx = poctucisescb (eae) ~ ASCC"A")
CALL INTERRUPT(&H21, InRegs, QutRegs)

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

IF LEN(DriveS) <> 1 THEN EXIT FUNCTION


InRegs.ax = &HEOO
InRegs.dx = ASCCUCASES(Drives)) = ASCCTAN)
CALL INTERRUPT(&H21, InRegs, OutRegs)
+ IF (CInRegs.dx AND &HFF) <> (CInRegs.ax AND &HFF) THEN EXIT FUNCTION

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

Listing 9-3. Set DefaultDisk% (Drive$) Function.


DECLARE FUNCTION SetDefaultDisk% (Drive$)
FUNCTION SetDefaultDisk% (Drive$)

DIM InRegs AS RegType, OutRegs AS RegType

SetDefaultDisk% = 0

IF LENCDriveS) <> 1 THEN EXIT FUNCTION

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:

Interrupt @H21 Service GHF Open Preexisting File


Interrupt @H21 Service @H10_—— Close File
Interrupt GH21 Service @H11 Search for First Matching File
Interrupt @H21 Service @H12 Search for Next Matching File
Interrupt GH21 Service @H13 ___ Delete Files
Interrupt @H21 Service @H14 Sequential Read
Interrupt @H21 Service @H15 Sequential Write
Interrupt @H21 Service @H16 Create File
Interrupt @H21 Service @H17 ~—Rename File

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.

Then there are some more disk services:

Interrupt @H21 Service @H18 Internal to DOS


Interrupt @H21 Service GH19 Find Default Disk
Interrupt GH21 Service GHIA Set the DTA Location
Interrupt @H21 Service @H1B FAT Information for Default Drive
Interrupt @H21 Service @H1C ‘FAT Information for Specified Drive
Interrupt @H21 Services @HI1D - Internal to DOS
@H20
380 P Advanced BASIC

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.

Getting the Default Disk


We can write a function named GetDefaultDisk$( ) to return a one-character string
indicating the current drive’s letter:

DriveNow$S = GetDefaultDisk$ (¢)

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.

Listing 9-4. Get DefaultDisk$ ( ) Function.


DECLARE FUNCTION GetDefaultDisk$ ¢)
TYPE RegType
ax AS INTEGER
bx AS INTEGER
cx AS INTEGER
dx AS INTEGER
bp AS INTEGER
$i AS INTEGER
di AS INTEGER
> A Tour of BIOS and DOS 381

Listing 9-4. Get DefaultDisk$ ( ) Function.


flags AS INTEGER
END TYPE

DECLARE SUB INTERRUPT (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)


FUNCTION GetDefaultDisk$

DIM InRegs AS RegType, OutRegs AS RegType


InRegs.ax = &H1900
CALL INTERRUPTC&H21, InRegs, OutRegs)
GetDefaultDisk$ = CHRS((OutRegs.ax AND &HFF) + ASCCA"))

END FUNCTION

And here’s a short program that puts GetDefaultDisk$( ) to use:

REM Example of GetDefaultDisk$

DECLARE FUNCTION GetDefaultDisk$ ( )

PRINT “The Default disk is: "; GetDefaultDisk$

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.

Interrupt @H21 Service @H21 Random Read


Interrupt @H21 Service @H22 Random Write
Interrupt GH21 Service @H23 File Size.
Interrupt @H21 Service @H24 Set Random Record Field
Interrupt @H21 Service @H25 Set Interrupt Vector
Interrupt @H21 Service @H26 Create a New Program Segment (PSP)
Interrupt @H21 Service @H27 Random Block Read
Interrupt @H21 Service @H28 Random Block Write
Interrupt @H21 Service @H29 _— Parse Filename

Then, in typical interrupt @H21 fashion, there’s a collection of services without any-
thing in common:
382 b& Advanced BASIC

Interrupt @H21 Service GH2A Get Date


Interrupt @H21 Service GH2B _ Set Date
Interrupt SH21 Service @H2C Get Time
Interrupt @H21 Service @H2D Set Time
Interrupt @H21 Service @H2E Set or Reset Verify Switch
Interrupt &H21 Service @H2F Get Current Disk Transfer Area
Interrupt @H21 Service @H30 Get DOS Version Number
Interrupt &H21 Service @H31 _ Terminate Process and Keep Resident
Interrupt &H21 Service @H32 _ Internal to DOS
Interrupt @H21 Service @H33 —Control-Break Check
Interrupt @H21 Service @H34 _ Internal to DOS
Interrupt @H21 Service @H35 Get Interrupt Vector

The next group of services deal largely with disks again:

Interrupt G@H21 Service @H36 Get Free Disk Space


Interrupt @H21 Service QH37 Internal to DOS
Interrupt @H21 Service @H38 Returns Country Dependent
Information
Interrupt GH21 Service @H39 Create a Subdirectory
Interrupt @H21 Service GH3A Delete a Subdirectory
Interrupt GH21 Service @H3B Change Current Directory

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.

Finding Free Disk Space


Here we'll put together a function that returns the space available (in bytes) on a disk
drive of your choosing. We can call this function DiskFree&( ), and use it this way:

BytesLeft& = DiskFree& (Drive$)

DiskFree$( ) can be exceptionally useful if you have a large output file to


generate and want to check the target disk first.
> A Tour of BIOS and DOS’ 383

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:

DIM InRegs AS Regtype, Outregs AS RegType

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( ):

DIM InRegs AS RegType, Outregs AS RegType

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:

DIM InRegs AS RegType, Outregs AS RegType

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)

— IF OutRegs.ax = -1 THEN EXIT FUNCTION

«
*
384 » Advanced BASIC

Otherwise, we have to multiply ax * bx * cx to get the number of bytes free on the


disk. To avoid overflows, we first load ax into a LONG variable, and then perform the
multiplication:

DIM InRegs AS RegType, QOutregs AS RegType

DiskFree& = -1
IF LENCDrive$S) <> 1 THEN EXIT FUNCTION

InRegs.ax = &H3600
InRegs.dx = ASCCUCASES(Drive$)) ~ ASCC"A") + 1
CALL INTERRUPT(&H21, InRegs, Outregs?

IF OutRegs.ax = -1 THEN EXIT FUNCTION

—+ Temp& = OutRegs.ax
DiskFree& = Temp& * Outregs.bx * Outregs.cx

bx contains the number of available allocation units (clusters), ax the number


of sectors per allocation unit, and cx the number of bytes per sector. The
product of these three is therefore the number of free bytes available on the
drive.

That’s it; we now return the resulting value as the value of DiskFree&. Listing 9-5
shows the whole function.

Listing 9-5. DiskFree& Shows — Amount of Free Disk Space | sie) a4


DECLARE FUNCTION DiskFree& (Drives)
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 (IntNo AS INTEGER, InRegs AS RegType, OutRegs AS RegType)


SS> A Tour of BIOS and DOS 385

Listing 9-5. DiskFree& Shows — Amountof Free Disk Space


FUNCTION DiskFree& (Drives)
DIM InRegs AS RegType, Outregs AS RegType

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

And here’s a short program putting DiskFree& to work:

REM Example of DiskFree&

DECLARE FUNCTION DiskFree& (Drive$)

MyDrivesS = “C"

PRINT “Free space on drive “;MYDrive$; “ ="; DiskFree&(MyDrive$)

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.

The DOS File 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:

Interrupt GH21 Service @H3C Create a File


Interrupt @H21 Service GH3D Open a File
Interrupt GH21 Service @H3E Close a File Handle
386 P» Advanced BASIC

Interrupt @H21 Service @H3F _ Read from File or Device


Interrupt @H21 Service GH40 Write to File or Device
Interrupt @H21 Service @H41 _ Delete a File
Interrupt @H21 Service @H42 Move Read/Write Pointer
Interrupt @H21 Service GH43 Change File’s Attribute
Interrupt @H21 Service @H44 I/O Control
Interrupt @H21 Service @H45 _——Duplicate a File Handle
Interrupt @H21 Service @H46 ‘Force Duplication of a File Handle
Interrupt @H21 Service @H47 —_‘Get Current Directory on Specified
Drive

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):

Interrupt @H21 Service @H48 Allocate Memory


Interrupt @H21 Service @H49 Free Allocated Memory
Interrupt @H21 Service GH4A SETBLOCK (Memory Allocation)
Interrupt @H21 Service GH4B _Load or Execute a program—EXEC
Interrupt @H21 Service @H4C Exit With Return Code
Interrupt GH21 Service GH4D Get Return Code of Subprocess

The following two services, however, can be very useful for BASIC programmers:

Interrupt @H21 Service QH4E Find First Matching File


Interrupt @H21 Service QH4F Find Next Matching File

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.

Finding the First Matching File


Service GH4E is a serious service, and it will introduce us to the intricacies of interfac-
ing with the DOS file services. This service lets you search a directory for a file that
> A Tour of BIOS and DOS 387

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:

Match$ = GetFirstMatchingFileS ("C:\ARCHIVES\PROGRAM.*")

Or, to find if there are any files there at all:

MatchS = GetFirstMatchingFileS ("C:\ARCHIVES\*.*")

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

FUNCTION GetFirstMatchingFileS (FileSpec$)

DIM InRegs AS RegTypeX, OutRegs AS RegTypex

REM Get Disk Transfer Area (DTA) address

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

We can load the address of NameFile$ into ds and dx like this:

FUNCTION GetFirstMatchingFiles CFileSpec$)

DIM InRegs AS RegTypeX, OutRegs AS RegTypex

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2FO00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find first match

NameFile$ = FileSpec$ + CHRS(O)


InRegs.ds = VARSEG(NameFile$) <
InRegs.dx = SADD(NameFileS$)

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

FUNCTION GetFirstMatchingFile$S (FileSpec$)

DIM InRegs AS RegTypeX, OutRegs AS RegTypeXx

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2FO00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find first match


NameFile$ = FileSpec$S + CHR$(Q)
InRegs.ds = VARSEG(NamefFileS) <—
InRegs.dx = SADD(NameFile$) |
InRegs.cx = 0.
InRegs.ax = &H4E00
CALL subarea ce aca InRegs, OutRegs)

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:

OutRegs.flag bit Flag


11 Overflow Flag
10 Direction Flag
9 Enable Interrupts Flag
8 Trap Flag
7 Sign Flag
6 Zero Flag
4 Auxiliary Carry Flag
Z Parity Flag
a= ad) Carry Flag

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

FUNCTION GetFirstMatchingFileS (FileSpec$)

DIM InRegs AS RegTypeX, OutRegs AS RegTypex


REM Get Disk Transfer Area (DTA) address

TnRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find first match

NameFileS = FileSpec$ + CHRS$(0)


InRegs.ds = VARSEG(NamefileSs)
InRegs.dx = SADD(NameFile$)
InRegs.cx = 0
InRegs.ax = &H4&E00
CALL INTERRUPTXC&H21, InRegs, OutRegs)

IF OutRegs.flags AND 1 THEN &


GetFirstMatchingFile$ = ""
EXIT FUNCTION
END IF

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:

FUNCTION GetFirstMatchingFileS (FileSpec$)

DIM InRegs AS RegTypexX, OutRegs AS RegTypex

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
392 P Advanced BASIC

DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find first match

NameFile$ = FileSpec$ + CHR$(0)


InRegs.ds = VARSEG(NameFile$)
InRegs.dx = SADD(CNamefFileS$)
InRegs.cx = 0
InRegs.ax = &H4E00
CALL INTERRUPTX(&H21, InRegs, OutRegs)

IF OutRegs.flags AND 1 THEN


GetFirstMatchingFile$ = "”
EXIT FUNCTION
END IF

DEF SEG = DTASegment% e


MatchOffset% = DTAOQffset% + 29

All that remains is to convert that ASCIIZ string into a BASIC one, and return:

FUNCTION GetFirstMatchingFileS (FileSpec$)

DIM InRegs AS RegTypeX, OutRegs AS RegTypeXx

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OQutRegs.bx

REM Now find first match

NameFileS = FileSpec$ + CHR$(G)


InRegs.ds = VARSEG(NamefFile$)
InRegs.dx = SADD(NameFile$)
InRegs.cx = 0
InRegs.ax = &H4E00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
IF OutRegs.flags AND 1 THEN
GetFirstMatchingFile$ = ""
EXIT FUNCTION
END IF

DEF SEG = DTASegment%


MatchOffset% = DTAOffset% + 29

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

Match$ = Match$ + NewChar$


NEXT i

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$( ).

Listing 9-6. GetFirstMatchingFileS Function.


DECLARE FUNCTION GetFirstMatchingFileS (FileSpec$)

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

DECLARE SUB INTERRUPTX (InNo AS INTEGER, InRgs AS RegTypeX, OutRgs AS RegTypeXx)

FUNCTION GetFirstMatchingFile$ (FileSpec$)

DIM InRegs AS RegTypeX, OutRegs AS RegTypex

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find first match

NameFile$ = FileSpec$ + CHRS$(0)


InRegs.ds = VARSEG(NameFile$)
InRegs.dx = SADD(NameFileS)
InRegs.cx = 0
InRegs.ax = &H4E00
CALL INTERRUPTX(&H21, InRegs, OutRegs)

IF OutRegs.flags AND 1 THEN


GetFirstMatchingFile$ = ""
394 P Advanced BASIC

Listing 9-6. GetFirstMatchingFileS Function. 2 of 2


EXIT FUNCTION
END IF

DEF SEG = DTASegment%


MatchOffset% = DTAOffsetx% + 29

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

And here’s how to use GetFirstMatchingFile$( ):

REM Example of GetFirstMatchingFiles

DECLARE FUNCTION GetFirstMatchingFile$ (FileSpec$)

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$( ).

Finding the Next Matching File(s)


Now we're going to use the next DOS service, service SH4E
to continue searching for
matching files, and we can write a function named GetNextMatchingFil
e$( ) to do it. This
function will search for all subsequent matching files after using
GetFirstMatchingFile$( ).
> A Tour of BIOS and DOS’) $395

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

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find next match

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

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2FO00
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find next match

InRegs.ax = &H4FOO
CALL INTERRUPTX(&H21, InRegs, OutRegs)

IF OutRegs.flags AND 1 THEN


GetNextMatchingFile$S = ""
EXIT FUNCTION
END IF

DEF SEG = DTASegment% co


MatchOffset% = DTAOffset% + 29 :
396 P& Advanced BASIC

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.

Listing 9-7. GetNextMatchingFileS ( ) Function.


DECLARE FUNCTION GetNextMatchingFiles ¢ )

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

DECLARE SUB INTERRUPTX CIMNo AS INTEGER, InRgs AS RegTypeX, OutRgs AS RegTypeX)

FUNCTION GetNextMatchingFiles

DIM InRegs AS RegTypeX, OutRegs AS RegTypeXx

REM Get Disk Transfer Area (DTA) address

InRegs.ax = &H2FO0
CALL INTERRUPTX(&H21, InRegs, OutRegs)
DTASegment% = OutRegs.es
DTAOffset% = OutRegs.bx

REM Now find next match

InRegs.ax = &H4F00
CALL INTERRUPTX(&H21, InRegs, OutRegs)

IF OutRegs.flags AND 1 THEN


GetNextMatchingFile$ = ""
EXIT FUNCTION
END IF
> A Tour of BIOS and DOS 397

Listing 9-7. GetNextMatchingFile$ ( ) Function.


DEF SEG = DTASegment%
MatchOffset% = DTAOffset% + 29

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

And here’s how to use both GetFirstMatchingFile$( ) and GetNextMatchingFile$( ):

REM Example of GetNextMatchingFiles

DECLARE FUNCTION GetFirstMatchingFile$ (FileSpec$)


DECLARE FUNCTION GetNextMatchingFilesS ¢)

FileSpec$ = “*.DAT"

MatchFile$ = GetFirstMatchingFileS(FileSpec$)

IF MatchFile$ <> "" THEN


PRINT “First matching file is: ", MatchFile$s
MatchFile$S = GetNextMatchingFile$
DO WHILE MatchFile$ <> "”
PRINT “Next matching file is: “, MatchFile$
MatchFile$S = GetNextMatchingFile$s
LOOP
ELSE
PRINT “No matches."
END IF

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

~ DO WHILE MatchFilesS <> ""


PRINT “Next matching file is: ", MatchFile$s
MatchFile$ = GetNextMatchingFile$s
LOOP ;

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.

The Rest of DOS


The remaining interrupt &H21 services represent a hodgepodge that was added as
time went on and new DOS versions appeared. Here’s what they look like:

Interrupt &@H21 Services GH50H-G&H53 Internal to DOS


Interrupt @H21 Service @H54 Get Verify State
Interrupt @H21 Service G@H55 Internal to DOS
Interrupt @H21 Service G@H56 Rename File
Interrupt @H21 Service @H57 Get or Set a File’s Date &
Time
Interrupt @H21 Service @H58 Internal to DOS
Interrupt G@H21 Service Q@H59 Get Extended Error DOS 3+
Interrupt @H21 Service GH5A Create Unique File DOS 3+
Interrupt @H21 Service &H5B Create a New File DOS 3+
Interrupt @H21 Service @H5C Lock and Unlock Access to a
File DOS 3+
Interrupt @H21 Service @H5E00 Get Machine Name DOS 3+
Interrupt @H21 Service @QH5E02 Set Printer Setup DOS 3+
Interrupt @H21 Service @H5E03 Get Printer Setup DOS 3+
Interrupt @H21 Service QH5F03 Redirect Device DOS 3+
Interrupt @H21 Service QH5F04 Cancel Redirection DOS 3+
Interrupt @H21 Service G@H61 Reserved
Interrupt @H21 Service QH62 Get Program Segment Prefix
DOS 3+
Interrupt @H21 Service &H63-64 Reserved
Interrupt @H21 Service @H65 Get Extended Country
Information
Interrupt @H21 Service &H6601 Get Global Code Page
Interrupt @H21 Service @H6602 Set Global Code Page
Interrupt @H21 Service @H67 Set Handle Count DOS 3.30
> A Tour of BIOS and DOS = 399

Interrupt GH21 Service GH68 Commit File (Write Buffers)


DOS 3.30
Interrupt @H21 Service QH6C Extended Open/Create DOS
4.0

|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.

|NOTE: | There is more information on this interrupt in the Appendix.

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:

Interrupt @H22 Terminate Address


Interrupt @H23 Control Break Exit Address
Interrupt GH24 Critical Error Handler
Interrupt @H25 Absolute Disk Read
Interrupt @H26 Absolute Disk Write.
Interrupt @H27 Terminate and Stay Resident
Interrupts @H28H-GH2E Internal to DOS
Interrupt @H2F Multiplex Interrupt
Interrupt GH30H-GH3F DOS Reserved

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:

Interrupt GH40H-GH5F Reserved


Interrupt GH60H-G@H66 Reserved for User Software
Interrupt @H67 LIM 4.0 Support
400 P Advanced BASIC

Interrupts @H68H-GH7F Not Used


Interrupts @H80H-&H85 Reserved by BASIC
Interrupts QH86H-G@HFO Used by BASIC Interpreter
Interrupts @QHF1H-GHFF Not Used

And that’s it for all the interrupts, 0 through @HFEF

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:

MOV DI,00BO > BF BO 00


MOV COUNTER,OOBO C7 06 C3 01 BO 00
MOV BX,0080 BB 80 00
CMP INDEX,00 80 3F 00
NZ 0670 (472
CMP [SI],0D 80 3C OD
AWE 0666 OS
MOVSB Ast
JMP 065E EB F8
> Welcome to Assembly Language 405

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 MOV Instruction

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:

MoV AX, OFFFFH

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:

MOV DX,AX (Move thedatafrom AX intoDX)

And now DX and AX hold the same value. We could also work one byte at a time:

MoV DL,AL (Move the data from AL into DL)

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:

MOV cx,CMemory Location]

Or, we can move whatever is in DX into the memory location this way:

MOV CMemory Location],DX


406 P Advanced BASIC

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:

MOV AX, EMemory Location 1]


MOV EMemory Location 2], AX

But it cannot go like this:

MOV CMemory Location 2], CMemory Location 11]

Assembly Language Example


It’s always better to see an example. There is an excellent program that comes with all
DOS versions named DEBUG. DEBUG has had the ability to assemble small programs
that you write on the spot, a mini-assembler is built into the program. We'll use this mini-
assembler to convert our instruction MOV AX,5 into machine language, and then run it,
watching the value stored in AX change from 0 to 5. Start the DEBUG program; it gives
you its hyphen prompt:

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

AX=0000 8X=0000 Cx=0000 ox=0000 sp=FFEE BP=0000 s$1I=0000 p1=0000


DS=0EF1 ES=O0EF1 SS=OQEF1 CS=0EF1 1Pp=0100 NV UP EI PL NZ NA PO NC
OEF1:0100 9AECO4020F CALL OFO2:04EC

In addition to the registers shown:


> Welcome to Assembly Language 407

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

Ax=0000 Bx=0000 Cx=0000 Dx=0000 SP=FFEE BP=0000 s$1=0000 p1=0000


DS=QEF1 ES=OEF1 SS=OEF1 CS=O0EF1 1P=0100 NV UP EI PL NZ NA PO NC
QOEF1:0100 9AECO4020F CALL OFO2:04EC

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

When there is in reality no machine language instruction there, as there frequently is


not, the translation is meaningless. That is the case here. Having just started DEBUG,
there is as yet no program to look at. It is just taking leftover bytes in the computer's
memory and trying to make sense out of them. In fact, DEBUG’s supplied translation
means nothing:

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

QE&F1:0100 9AECO4020F> CALL OF02:04EC

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

AX=0000 BX=0000 ¢x=0000 Dx=0000 SP=FFEE 8sP=0000 $1=0000 01=0000


DS=QEF1 ES=OEF1 SS=OEF1 CS=QEF1 IPp=0100 NV UP EI PL NZ NA PO NC
O£F1:0100 9AECO4020F CALL OFO2:04EC
-A100 « Here is the A100 command.
OEF1:0100 DEBUG's response.

The Assemble DEBUG command provides an easy way of testing assembly


language instructions, saving you the time needed to edit a source file and
assemble it. Also, if you need to find the actual machine language bytes
corresponding to some assembly language instructions, just use the Asse-
mble command and then use the memory dump command, D.

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

AX=0000 8x=0000 cCx=0000 odX=0000 SP=FFEE BP=0000 s$1=0000 »p1I=0000


DS=OEF1 ES=OEF1 SS=OEF1 CS=OEF1 IPp=0100 NV UP EI PL NZ NA PO NC
OEF1:0100 9AECO4020F CALL OFO2:04EC
-A100
OEF1:0100 MOV AX,5
QEF1:0103
-R -
AX=0000 8x=0000 Cx=0000 DX=0000 SP=FFEE BP=0000 s1=0000 b1=0000
DS=OEF1 ES=OEF1 SS=OEF1 CS=QEF1 Ip=0100 NV UP EI PL NZ NA PO NC
OEF1:0100 B80500 MOV AX,0005

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 .

AX=0000 BxX=0000 CX=0000 DX=0000 SP=FFEE 8P=0000 $1=0000 b1=0000


DS=OEF1 ES=OEF1 SS=0EF1 CS=0EF1 1P=0100 NV UP EI PL NZ NA PO NC
QEF1:0100 880500 MOV AxX,0005 ;
~T & This will execute our MOV instruction.

AX=0005 BX=0000 CX=0000 DxX=0000 SP=FFEE BP=0000 SI=0000 01=0000


DS=OEF1 ES=0EF1 SS=OEF1 CS=0EF1 1P=0103 NV UP EI PL NZ NA PO NC
Q€F1:0103 O20F ADD cL,CBXI DS: 0000=Cb

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.

Our First Assembly Language Program


DEBUG not only allows you to assemble programs, but to write them out to the disk as
well. Let’s use DEBUG to assemble our first assembly language progam. This program will
simply type out the letter Z and then exit.
We'll start with the A command. As before, we will put our machine language code
starting at location 0100H:

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:

Service # Name Set These What Happens


1 Keyboard Input AH =1 ASCII code of typed
key returned in AL
2 Character Output AH = 2 The character
Dl = ASCII Code corresponding to the
ASCII CODE in DL
is put on the screen
9 String Output AH=9 Prints a string of
DS:DX = Address of bytes from memory
string of characters on the screen (we
to print will use this service
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

C000:AAAA 1 Byte CAAAAH


C000:AAA9 1 Byte CAAA9SH
1 MB C000:AAA8 1 Byte CAAA8H
C000:AAA7 1 Byte CAAA7H
414 » Advanced BASIC

0000:0003 1 Byte 00003H


0000:0002 1 Byte 00002H
0000:0001 1 Byte 00001H
0000:0000 1 Byte 00000H < The bottom of memory

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

Segment 0001 16 Bytes

Segment 0000 16 Bytes


0000:0000 =a,
Figure 10-7
To let you set the segment that you want to choose as your work area, the 80 X 86
provides four segment registers (which we originally saw in Chapter 9). You set these
segment registers typically at the beginning of a program, or let them be set automatically
for you. Keep in mind, however, that they only define a 64K area. If you want something
outside that area, you'll have to take care of setting them as required.
> Welcome to Assembly Language 415

The four segment registers are CS, DS, ES, and SS. They stand for Code Segment, Data
Segment, Extra Segment, and Stack Segment:

Segment Register Means Used With


CS Code Segment Your program’s
instructions
DS Data Segment The data you want to
work on
|e Extra Segment Auxilary data segment
register
SS Stack Segment Set by DOS; holds the
“stack”

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

CS=2000H DS=3000H <— BOOOH

Program Program Screen


ee Henk < More than 64K bytes away > Butfer
Segment Segment

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

ee elise < More than 64K bytes away —> ee

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.

Segment Registers in Use: .COM Files


The first programs we write will be .COM files. The default for .COM files is to set all
four segment registers to the same value, the Code Segment. We will do this expressly so
that we do not have to worry about the segment registers just at the time when we are
being introduced to our first programs. .
Everything that goes on in a .COM file will be limited to one 64K workspace. And
when the program is loaded, DOS sets that segment address for us. That means in
> Welcome to Assembly Language 417

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.

The .CODE Directive

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

Start: MOV AH,2 =


MOV —DL,5SAH
INT 21H
Exit: INT 20H

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.

Positioning Code in the Code Segment


When .EXE files are loaded, they are put at the beginning of the code segment they
have been given, at CS:0000. Their first instruction can start right there. .COM files, on
the other hand, are supplied with a header that is loaded in before they are, and it is the
> Welcome to Assembly Language 419

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

CS:0100 > MOV AX, 5

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 END Directive

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.

Listing 10-1. PRINTZ.ASM Program.


- CODE

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:

Rs\>MASM PRINTZ; <<

And the macro assembler will do this:

R\:>MASM PRINTZ;
Microsoft (R) Macro Assembler Version 5.10
Copyright (C) Microsoft Corp 1981, 1988. Ait rights reserved.

50144 + 31277 Bytes symbol space free

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.

LINK : warning L4021: no stack segment

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:

R:\>EXE2BIN PRINTZ PRINTZ.COM

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.

Adding Data to Assembler Programs


On the other hand, this is really only half the story. As it stands, PRINTZ.ASM is a
working program, but it is very primitive. In almost all .COM files, some data will be
stored.
Every variable used in a program is data. We might want to read a file from the disk
and store it in the data area, treating it as data. Or we might have program messages like
“Hello, world.” in the data area.
Let’s look at an example. It turns out that we can use variables in assembly language —
just as in BASIC — and we'll use the DB, define byte, and DW, define word, directives to
set them up.

The DB and DW Directives

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

MOVE AL, VALUE VALUE DB 5 < DS holds the segment


address of data.

Figure 10-12
Now we are free to read the data in VALUE in the program:

MOV AL, VALUE

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

In this case, we could work with WORD_VALUE like this:

MOV AX, WORD VALUE

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.

Listing 10-2. PRINTZ.ASM Program.


- CODE

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:

Prompt DB “How Long has it been since you called "


DB "your mother?”, 0

The assembler lets you store strings this way, using the quotation marks as shorthand
(otherwise, you’d have to use DB for each letter).

INT 21H Service 9 — Print a String

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)

MOV DX, OFFSET Our Characters

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.

Listing 10-3. PRINTXYZ.ASM — Prints “XYZ.”


~ CODE

ORG 100H zSet up for a .COM file.


Start: JMP Printz sJmP over data area.
Our_Characters DB "XYz$" sWe will print out this string.
PrintZ: MOV AH,9 gRequest INT 21H service 9.
MOV OX, OFFSET Our_Characters sPoint to our string.
INT 21H zAnd print it out here.
Extts INT 20H sEnd the program.

END Start 7Set entry point totabel Start.

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).

Accepting Keyboard Input


The most basic of the interrupt 21H services is service number 1, which reads key-
board input. This is assembly language’s INKEY$ (except that it waits for keyboard
input). This is really the primary input service. When a key is typed, it is echoed on the
screen and its ASCII code is returned in the AL register. Let’s put this service to use.

The Program CAP.COM


The following example program will accept a letter that you type, capitalize it, and
print it on the screen. For the first time, we will get our assembly language program to
accept input from us. Let’s start with the .COM file shell:
428 » Advanced BASIC

- CODE

ORG 100H
Start: JMP CAP
;Data Area
CAP:
gProgram will go here.

Exit: INT 20H


END Start

And add the instructions that will let us accept input:

- CODE

ORG 100H
Start: JMP CAP
;Data Area
CAP: MOV AH,1 j;Request keyboard ides
- INT 27H ffrom INT 21H

Exit: INT 20H

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:

Uppercase Code Lowercase Code


65 <—subtract 32— a 97
B 66 <—subtract 32— b 98
cS 67 <—subtract 32— c 99

Z 90 <—subtract 32— z al22

The number we have to subtract is just equal to ASCII(a) — ASCII(A), which is 97 - 65


= 32, the distance between the two parts of the table. Here’s how we capitalize the ASCII
> Welcome to Assembly Language 429

value in AL, introducing the new instruction, SUB, for subtract:

- 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

Exit: INT 20H

END Start

The SUB and ADD Instructions

The SUB instruction is used this way:

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

Similarly, expressions like “a’+”A” are allowed.

The assembler understands expressions that include operators like +, -, /, and


*. It's often a good idea to use them to make your code clearer.

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).

Listing 10-4. CAP.ASM Program.


- CODE
ORG 100H
Start: JMP Cap
;Data Area —
Cap: MOV AH,1 sRequest keyboard input
INT... 21H 7From INT 21H.
SUB AL,"a"-"A" = —-_-sCapitalize the typed key
MOV DL,AL eSet up for service 2. :
MOV AH,2 j;Request character output<
: INT 218 slype out character.
Exit: INT 20H —

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.

The CMP Instruction

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

SUB AL,"a"—"A" sCapitalize the typed key


MOV DL,AL ;Set up for service 2.
MOV AH,2 sRequest character output
INT 21H zType out character.
Exit: INT 20H

END Start

Here, we’ve compared AL to the ASCII value for a and then immediately followed with
a JB — Jump if Below — instruction:

CMP AL,"a" ;Compare the incoming ASCII code to “a”.


JB Exit zif the letter is not lower case, exit.

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.

CMP AL,"a" « Sets Flags


JB Exit Reads Flags

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.

Listing 10-5. CAP.ASM Program. sie) a2


- CODE
ORG 100H
Start: JMP Cap
Data Area
Cap: MOV AH,1 sRequest keyboard input
INT 21 zFrom INT 21H
CMP AL,"a" ;Compare the incoming ASCII code to "a".
JB Exit vif the letter is not lower case, exit.
~ CMP AL,"z" ;Compare the incoming ASCII code to "z",
> Welcome to Assembly Language 433

Listing 10-5. CAP.ASM Program. 2 of 2


JA Exit sIif the letter is not lower case, exit.
SUB AL,“a"—-"A" Capitalize the typed key
MOV DL,AL 7Set up for service 2.
MOV AH,2 sRequest character output
INT 21H gType out character.
Exit: INT 20H

END Start

More Conditional Jumps


At this point, we’ve seen the two instructions JA and JB. These follow a CMP (for
compare) instruction, and, depending on the result, a jump may be made. There are
actually many conditional jumps. In fact, there are even variations of JA and JB. In
addition to these two, there are JAE Jump if Above or Equal); JBE Jump if Below or
Equal); JNA Jump if Not Above); JNB Jump if Not Below); JNAE Jump if Not Above or
Equal); and JNBE Jump if Not Below or Equal). All of these can be used after a CMP
instruction. Here are a number of conditional jumps and their meanings:

Conditional Jump Means


JA Jump if Above
JB Jump if Below
JAE Jump if Above
JBE Jump if Below
JNA Jump if Not Above
JNB Jump if Not Below
JNAE Jump if Not Above
JNBE Jump if Not Below
JE Jump if Equal
JNE Jump if Not Equal
Jz Jump if result was Zero
JNZ Jump if result was Not Zero
JCXZ Jump if CX = 0 (Used at end of loops)

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.

An Assembler Program to Convert Hex to Decimal


The next step after CAPASM will be to write a program that’s slightly more substantial.
Let’s develop a program that will change four digit hex numbers to decimal numbers. We
start with the .COM file shell:

- CODE

ORG 100H
ENTRY: JMP DEHEXER

;Data will go here.

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

You might notice the use of the directive DUP:

BUFFER DBS
NUM_TYPED DBO
= ASCII_NUM DB 3 DUP (0)
END_NUM DB O
CRLF DB O

This directive saves us time. This expression is equal to ASCIL.NUM DB 0, 0, 0, not


such a big saving for 3 bytes, but it is if you need to reserve space for 32,000. Now we’ve
set up and labeled our buffer as shown in Figure 10-14.
BUFFER DB 5, O, O, 0, 0, 0, O
| [Bee CRLeE
END_NUM
ASCil_ NUM
NUM_ TYPED
BUFFER
Figure 10-14
We can also add a prompt to the code — Type in a 4 digit hex number:$ — to be typed
out with service 9, the string printer. In order to print out our prompt, we have to pass
service 9 an offset address in DX:
436 }& Advanced BASIC

~ 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

This is what the prompt will look like on the screen:

Type in a 4& digi t hex number:

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

MOV BX, OFFSET END NUM


LOOP1: MOV DL, EBX] San
DEC BX

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

The MUL instruction in the 80 x 86 microprocessors is comparatively slow, so


programmers often cobble their own multiplications out of left shifts and addi-
tions. For example, to multiply AX by 5, make a copy of it in BX, shift AX left
two places, and add BX to it. This is about ten times faster than MUL.
> Welcome to Assembly Language 441

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.)

Listing 10-6. DEHEXER.ASM Program — Converts Hex to Decimal.


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 21H
MOV AH,OAH
MOV DX,OFFSET BUFFER
INT 21H

+ 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

The DIV and MUL Instructions

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.

The terminology DX:AX is an unfortunate way of specifying double words,


since addresses are also specifed with a colon; however, when segment regis-
ters are used, you can be sure it’s an address.

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

UNDER_A:SUB DL, bas @ bas

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

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

In this case, the body of LOOP_1 will be executed five times.


In DEHEXER, the previous loop (where we stripped the decimal digits off the value
in
AX) left the number of digits in CX already, so the loop index CX is all set. All we have
to
do is to use LOOP printing out each digit as we pop it off the stack:

MOV AH,2
LOOPS: POP Dx
ADD Dx,"0"
INT 21H
Loop LOOPS

And that’s it. We've read in a hex number, converted it to binary


by multiplying each
digit by a power of 16, peeled decimal digits off by succes
sively dividing it by 10,
> Welcome to Assembly Language 449

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.

Procedures in Assembly Language


In BASIC, we can write subroutines, subprograms, and functions. In assembly lan-
guage, all we can write are procedures. We can make DEHEXER into a single procedure
by adding the PROC and ENDP directives so that they straddle our code like this:

~ 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

Here’s how the whole program looks now:

«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

MOV DX, OF FSET BUFFER


INT 21H
MOV cx, 0
MOV AX,0
MOV BX, OFFSET END_NUM
LOOP1: MOV DxX,0
MOV DL, CBX]
DEC BX
CMP pL,"9"
JBE UNDER_A
suB OL, lege ed io. “OQ” ae 10

UNDER_A:SUB pL, “0”


SHL DX, CL
ADD AX, 0X
ADD CL,4
CNP CL, 16
JB LOOP1
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
ADD Dx,"0"
INT 21H
LOOP LooP3
INT 20H
DEHEXER ENDP -
END ENTRY

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

Listing 10-7. DEHEXER.ASM with a Subroutine.


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
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

Listing 10-7. DEHEXER.ASM with a Subroutine.


ADD Dx,"0"
INT 21H
LOOP LOOPS
RET re
PRINT NUM ENDP

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.

There is no such thing as a formal local variable in assembly language. All


variables in the same module are shared. You can, however, make variables
local by putting them into another file.

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

eneyecibricast es. etioy’ae 7iq | ee gee oe aea |PI we ak


ue’ Cee: |singer
a> : 20 ai & ae:

WAP ere 4 re) ans Nv nr (ns 6 26 Sw. rouse on


hs ar fea isi f etatts. oe soo ane? ort |
goles: GA
> i geriors cin ww:
ae ete se nr©pte SR AER _——
te gee Say ae

set ely Mapas? oda ee Trew: dite .AMno OO 2 seat


7

es oo a?

Wey ube guy foe Ay wi suit we 1 12008 ye vid


"5 ;
= =) ean or foee-7) > i yi hee ‘y
apilign pee
le he Muy ts 49

aaa ay ea Fite 4 Sphere ow orl ee 70


Ca!) }, i <<“ whi = gm ett ce

“sjuha mL sgestp rent Ee iol


BASIC/Assembly
Language Interface
> BASIC/Assembly Language Interface 457

IN THIS CHAPTER, we'll learn about these topics:

e How to link assembly language procedures into BASIC


e How to work with BASIC data in assembly language
e How to read parameters passed from BASIC
e How to return values to BASIC

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.

Listing 11-1. Assembly Language Version of a BASIC Function That Adds


Two Integers.
-MODEL MEDIUM, BASIC
- CODE
- 286

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.

Linking into BASIC


To make use of this chapter, you must have a Microsoft assembler, such as MASM 5.1,
to assemble the code we develop. For example, you might have a file named A.ASM that
458 P Advanced BASIC

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.

49894 + 311895 Bytes symbol space free

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:

— DE\>LINK /@ A.OBJ QB.LIB, A.@LB,, BQLB45.L1B;

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:

D:\>LINK /Q A.OBJ @BX.LIB, A.QLB,, @BXQLB.LIB;

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.

43997 Bytes Available


43722 Bytes Free

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.

And this creates a file named PROGRAM.EXE, which is ready to run.

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

Receiving Parameters from BASIC


Like all languages, BASIC passes parameters to subprograms and functions on the
stack. Each parameter is pushed in the order it appears in the call; for example, if you
used MID$C( ) like this:

‘MyChar$S = MIDS(MyString$, Place%, Num%)

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

Add5 PROC FAR USES DI SI DS ES, VALUE1: WORD <—


*
.

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:

Add5 PROC FAR USES DI SI DS ES, VALUE1: WORD

> 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:

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

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

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

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.

Returning Values to BASIC


Another question is how do BASIC functions return values to the calling program?
This is actually pretty easy. Here’s how they return values, by data type:

Data type Returned


INTEGER In AX
LONG In DX:AX (high word in DX, low in AX)
All others Offset address in AX

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.

BASIC’s Internal Representation of Data


The real difficulty for the assembly language programmer is not so much in
receiving or returning data as it is in decoding the values that have been passed. For
example, if we get passed a SINGLE and read the value @H23110000 from mem-
ory, how do we know what number those four bytes represent? To be able to read
and pass data to and from BASIC, you must know exactly what the internal repre-
sentation of data in BASIC is; that is, how BASIC stores its data.
Here are the data types that are most standard in BASIC:

Type Symbol Bytes Range


INTEGER % 2. -32,768 to 32,767
LONG & 4 -2,147,483,648 to 2,147,483,647
SINGLE ! 4 -3.402823E+38 to -1.40129E-45
DOUBLE # 8 -1.79769313486232D308 to
-2.2250738585072D-308
CURRENCY @ 8 -$922337203685477.5808 to
$922337203685477.5807
STRING $ 32K Strings can range up to 32K characters
(bytes)

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

INTEGERs and LONGs


The INTEGER and LONG formats are simple: INTEGERs are stored as one word and
LONGs as two words. For example, here’s how some INTEGERs are stored:

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.

Two’s Complement Notation


The whole scheme of signed numbers in computers comes from the simple fact that 1
+ (-1) = 0. We realize that if we want to do any calculation with negative numbers in the
PS/2 or PC, the number we choose to be -1, when added to 1, has to give O. Yet this seems
impossible. Let’s say we were working with INTEGERs. Can you think of a 16-bit num-
ber which, when added to 1, will give a result of 0? It seems as though the result must
always be 1 or greater.
In fact, however, there is an answer. Figure 11-2 shows what happens. If we take
SHFFFF, which is 11111111111111111B in binary, and add 1 to it.
> BASIC/Assembly Language Interface 465

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

binary number 1111111111111111B in INTEGER format, which is QHFFFF


(@HFFFFFFFF in LONG format).
The process is the same for any other number whose sign you want to change. Now we
can work with both positive and negative INTEGERs and LONGs on a bit-by-bit level.

SINGLEs and DOUBLEs


Floating point numbers, however, are considerably more complex. For example, a
number like 10.5 is stored as &H41280000 in BASIC, and this number has little
resemblance to the original value. What’s going on?
We have to keep in mind that the computer is a binary machine, which means it uses
base 2, so a number like 10.5 must be stored as 1 X 2’ plus 1 X2' plus 1 X 2", which is
1010.1, where the point is a binary point. It is possible to store any number in binary that
can be stored in decimal (it just takes more places). For instance, 0.75 can be broken
down into 1/2 + 1/4, or 2* + 2°, so .75 = .11B.
The numbers stored as floating point are all normalized, which means they appear
with the binary point near the beginning. For example, 1010.1B is this in exponent form:
1.0101 X 2’. To expand this, just move the binary point three places to the right, giving
1010.1B again. You can see that the first digit in any normalized binary number is always
1. Under BASIC floating point format, the leading 1 is implicit. In other words, all that is
stored of the significand is 0101B. The exponent is still 3, but, to make matters even more
complex, all exponents are stored after being added to some offset or bias.
For a SINGLE, this bias is @H7EF or 127. For DOUBLEs, this number is @H3FE or
1,023. In other words, if our number, 1010.1B, is going to be stored as a SINGLE, the
exponent will be 3 + 127 = 130 = &H82.
Finally, the first bit of any floating point number is the sign bit. Floating point numbers
are not stored in two’s complement notation. If the sign bit is 1, the number is negative; if
it is 0, the number is positive. That’s the only distinction between positive and negative
numbers. The SINGLE format (4 bytes) is shown in Figure 11-5.
Bit# 31 30 23 22 0

S Exp. — Significand.
Figure 11-5
> BASIC/Assembly Language Interface 467

So 10.5 = 1010.1B will look like Figure 11-6.


Sign Bit
L
01000001001010000000000000000000
eres]
[Exponent + Bias = &H82] [Significand = 0101B]
Figure 11-6

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:

&HOO &HOO &HOO &HOO HOO &HOO &H27 &H10

Let’s finish up by covering the last of the standard data types, STRINGs.

Strings and Arrays


Strings and arrays are stored with descriptors, however, we can only work here with
the descriptors used by BC.EXE and QB.EXE. String and array descriptors used by
QBX.EXE are proprietary, and we'll have to use some special BASIC routines to work with
them in other ways. Under BC.EXE or QB.EXE, a string descriptor looks like Figure 11-9.
Bit# 31 15 0

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

Instead of trying to decode array descriptors, the best thing to do is to simply


pass the address of the first element of the array to your assembly language
code.

DIM Array(10, 2) AS CURRENCY

REM Fill Array(n,1) with today's sales:

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

REM Fill Array(n,2) with yesterday's sales:

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

Figure 11-10 shows the array produced.


Col 1 Col 2
< Row1
< Row2
470 } Advanced BASIC

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:

10.00 <¢ Row 1 (that is, Array(1, 1))


53.00 Row 2

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

Now let’s start writing some code and interfacing to BASIC.

A Function with No Parameters


Our first example is a function that takes no parameters and returns an integer. The
example program here is called Return5%( ), and it simply returns 5 whenever called.
All this procedure does is to act like a BASIC function which returns
an
> BASIC/Assembly Language Interface 471

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):

-MODEL MEDIUM, BASIC -

Next we start the code segment with .CODE:

-MODEL MEDIUM, BASIC


- CODE os

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:

-MODEL MEDIUM, BASIC


~ CODE

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):

-MODEL MEDI UM, BASIC


.CODE

PUBL IC Return5
Return5 PROC
~ PUSH BX ;Should save all registers
PUSH cx
PUSH ES
PUSH DS

7Do work here.

RET
Return5 ENDP
END

Since we want to return an integer value of 5, we have to load 5 in AX after restoring


the other registers, and then we return. Listing 11-2 shows the whole function.

Listing 11-2. Return5 Function.


-MODEL MEDI UM, BASIC
-CODE

PUBL Ic Return5
Return5 PROC
PUSH BX sShould save ail registers
PUSH cx
PUSH ES
PUSH DS

7Do work here.

POP DS ;Restore registers


POP ES
POP cx
POP BX

> MOV AX,5 zReturn integer value of 5 ,

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:

DECLARE FUNCTION Return5%Z() =

PRINT “Value from Return5:";Return5%

And that’s all there is to it: you can link Return5%( ) into this BASIC program and run
it.

A Function with One Parameter


Next we can develop a program to read one integer parameter and return another
integer parameter. For example, we discussed the function Add5%( ) in the beginning of
the chapter, and we can review it here. This function simply adds 5 to the incoming
integer and returns the result. We start with the extended PROC definition like this:

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:

Listing 11-3. Add5 Function.


-MODEL MEDIUM, BASIC
- CODE

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:

REM Example of Assembly Language interface -- Add5 adds 5 to an integer

DECLARE FUNCTION Add5Z%(V%)

PRINT “9 + 5 =";Add5%(9%)

A Function with One Long Integer Parameter


Now let’s see how to recieve a LONG integer as a parameter by changing Add5%( )
into Add5Long&( ). Even though we’re reading a LONG value, its offset address is still
passed as a one word value. We load the LONG into the DX:AX register pair like this:
re> BASIC/Assembly Language Interface 475
e TACS FD

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

|NOTE: | In addition to ADC, there is also SBB, subtract with borrow.

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

Listing 11-4. Add5Long Function.


-MODEL MEDIUM, BASIC
~ CODE
jThis picks a LONG off the stack

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

A Function Returning a String


Now we can develop a example that returns the string “QQQ” to BASIC. Let’s call it
GetQQQ$( ). This function takes no parameters, but just returns a string.
QBX keeps string and array data in a special segment separate from the rest of the data,
while the BC and QB compilers keep it in the same segment. For that reason, they have
different types of string descriptors, and we'll handle QBX separately.

GetQQQ$/ ) with QB.EXE and BC.EXE


In this version of GetQQQ$( ), we'll put together our own string descriptor to return
to BASIC. That string descriptor looks like Figure 11-11.
Bit # 31 15 0

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

MOV BX, CBP+6) sReturn value offset


MOV AX, BX sMust also be returned in AX

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

MOV BX, CBP+6] 7Return vatue offset


MOV AX, BX sMust also be returned in AX
~ MOV WORD PTR CBX], 3 sString Length

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:

MOV BYTE PTR EBX], 3

In which case, the byte at location [BX] is filled with 03H, or WORD PTR:

MOV WORD PTR EBXI, 3

In which case, the word at [BX] is filled with 0003H.


The next word in the string descriptor will hold the offset address of the string data.
Let’s store the string data immediately after the string descriptor itself (i.e., we have to
add 4 bytes to the present value of BX to skip over the descriptor). This is a common
place to store string data in BASIC. We can point to the byte right after the string
descriptor, and also place that address in the descriptor itself like this:

PUBLIC Getaa@
Get@aq PROC FAR USES BX

MOV BX, CBP+6] gReturn value offset


MOV AX, BX sMust also be returned in AX
MOV WORD PTR CBX], 3 : 7String length
~ ADD BX, 4
MOV CBX-2], BX _ gPut string data address into descriptor

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:

MOV WORD PTR CBX], 3 7String length


ADD BX, 4
MOV CBX-21, BX 7Put string data address into descriptor
=> MOV Cex], "a"
MOV CBxX+1], "Q"
MOV CBX+2], “Q”

Because “Q” is just a numeric value, 51H, to the assembler, which sees this:
> BASIC/Assembly Language Interface 479

MOV WORD PTR CBX], 3 7String Length


ADD BX, 4
MOV CBX-21, BX 7String data address
~ MOV CBX], 51H
MOV CBX+1], 51H
MOV CBX+2], 51H

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:

GetQ@aQ@ PROC FAR USES BX

MOV BX, CBP+6] gReturn value offset


MOV AX, BX sMust also be returned in AX
MOV WORD PTR CBXIJ, 3 sString Length
ADD BX, 4
MOV CBX-2], BX sString data address
eed MOV BYTE PTR CBX], "Q" eStore string
MOV BYTE PTR CBX+1], "a"
MOV BYTE PTR CBX+2], "a"

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.

Listing 11-5. GetQQQ Function.


-MODEL MEDIUM, BASIC
- CODE

PUBLIC Geta@a
GetaaQq PROC FAR USES BX

MOV BX, CBP+6] gReturn value offset


MOV AX, BX gMust also be returned in AX
MOV WORD PTR CBX], 3 sString length
ADD BX, 4
MOV CBX-2], BX 7String data address
MOV BYTE PTR CBX], "Q” pStore string
MOV BYTE PTR CBX+1], “Q"
MOV BYTE PTR CBX+2], “a"

RET
Getaaga ENDP
END
480 } Advanced BASIC

Here’s how to use GetQQQ3$( ), just like any other BASIC function that returns a
string:

Example of Assembly Language interface ~~ Getaaas returns the string ‘QQQ'.


REM

DECLARE FUNCTION GetQ@aas()

PRINT Getaaas

GetQQQ$/ ) with QBX.EXE


QBX’s string descriptors, however, have to be made by QBX — they’re not in the same
simple form used by BC.EXE or QB.EXE. Instead, we'll use the same internal subprogram
that QBX itself uses to make a string descriptor for a string. This subprogram is called
StringAssign( ), and you call it like this:

CALL StringAssign(SourceStrAddr, SourceStrlLen%, DescAddr, DescLenz)

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:

-MODEL MEDIUM, BASIC


DATA
Ret Str DB "aaqq"
Ret_Desc_1 DW 0
Ret_Desc_2 DW 0

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:

-MODEL MEDIUM, BASIC


-DATA
Ret_Str DB "QaQq"

Ret_Desc_1 DW
Ret_Desc_2 DW
- CODE

PUBLIC Getaaa
Getaag PROC FAR USES BX

~ MOV BX, CBP+6] zReturn value offset


MOV AX, BX gMust also be returned in AX

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:

CALL StringAssign(SourceStrAddr, SourceStrLen%, DescAddr, DescLenz)

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:

-MODEL MEDIUM, BASIC


-DATA
Ret_Str DB “aaa”
Ret_Desc_1 DW 0
Ret_Desc_2 DW 0
~ CODE

PUBLIC GetQQ@Q
Getaag PROC FAR USES BX

MOV BX, CBP+6] pReturn value offset


MOV AX, BX zMust also be returned in AX
PUSH DS zAddress of Q@Q@ string
MOV DX, OFFSET Ret_Str
PUSH DX
MOV DXeu'S zLength of Q@Q@ string
PUSH DX
*
ry
482 P Advanced BASIC

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( ):

-MODEL MEDIUM, BASIC


DATA
Ret_Str DB “aaa”
Ret_Desc_1 DW 0
Ret_Desc_2 DW 0
«CODE
PUBLIC GetaQae
GetQaaa PROC FAR USES BX

MOV BX, CBP+6] zReturn value offset


MOV AX, BX pMust also be returned in AX
PUSH oS gAddress of Q@Q@ string
MOV DX, OFFSET Ret_Str
PUSH DX
MOV DX, 3 glength of @@@ string
PUSH DX
PUSH DS zAddress of descriptor to be filled
MOV DX, OFFSET Ret_Desc_1
PUSH DX
MOV DX, O 30 for variable length string
PUSH DX
EXTRN StringAssign: PROC
~ 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

Listing 11-6. GetQQQ$ (QBX Version) Program.


REM Example of Q@BX interface -- Getaaas returns the string ''aaq''.
DECLARE FUNCTION Getaaas()

PRINT GetQags

«MODEL MEDIUM, BASIC


-DATA
Ret_Str DB "aaa"
Ret_Desc_1 Dw 0
Ret_Resc_ 2 DW 0
- CODE

PUBLIC Getaae
GetQ@aQ@ PROC FAR USES BX

MOV BX, CBP+6] Return value offset


MOV AX, BX sMust also be returned in AX
PUSH BS rAddress of Q@QQ string
MOV DX, OFFSET Ret_Str
PUSH DX
MOV glength of @8@ string
PUSH
PUSH zAddress of descriptor to be filled
MOV DX, OFFSET Ret_Desc_1
PUSH DX
MOV 70 for variable length string
PUSH DX
EXTRN StringAssign: PROC
CALL StringAssign
MOV DX, Ret_Desc_1 gReturn descriptor
MOV CBX], DX
MOV DX, Ret Desc 2
MOV CBX+2], DX

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.

A Function with Two Parameters


Now let’s expand a little. We can read two parameters from BASIC almost as easily as
one. Let’s write a function called Addem%( ), which takes two integers, adds them, and
returns the sum. Here, we want to read two parameters (both integers), and we can do
that using PROC like this:
484 }& Advanced BASIC

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.

Listing 11-7. ADDEM.ASM — Adds Two Integers.


«MODEL MEDIUM, BASIC
- CODE

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

And here is an example of Addem%( ) at work:

REM Example of Assembly Language interface -- Addem() adds two integers

DECLARE FUNCTION AddemZ(V1%,V2%)

PRINT “9 + 5 ="7Addem%(5,9)
> BASIC/Assembly Language Interface 485

A Function Using the Data Segment


We've seen briefly how to use a data segment in the QBX string example. Let’s take a
closer look at that now. We can modify the procedure Addem%( ), which we’ve already
written, to store data in the data segment. This data segment will be placed in the
common data segment of the .EXE file, along with all the BASIC variables, when the file is
linked.
To store data in memory, we need to reach the data segment, which we do with the
DATA directive. Nothing could be easier. To store the two integers we are to add in the
memory words VAL] and VAL2, we use .DATA this way:

-MODEL MEDIUM, BASIC

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:

-MODEL MEDIUM, BASIC

PUBLIC Addem
-~DATA
VAL1 DW O
VAL2 DW O

- CODE
Addem PROC FAR USES DI SI DS ES, VALUE1: WORD, VALUE2:WORD

;Pick addr of VALUE from stack (BASIC passes ref.)


MOV BX, VALUE1
MOV CX, CBX]
J MOV VAL1, CX
MOV BX, VALUE2
MOV CX, CBX]
MOV VAL2, CX

zNow data is in the data group

MOV AX, VAL1


ADD AX, VAL2

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.

Listing 11-8. ADDEM.ASM Using the Data Segment.


- MODEL MEDIUM, BASIC

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

sNow data is in the data group

MOV AX, VAL‘


ADD AX, VAL2

RET
Addem ENDP
END

A Subprogram with Parameters


Now let’s develop an assembly language version of a subprogram, not a function, that
takes one parameter. Let’s write a subprogram named PrintChar( ) that takes a character's
ASCII code and prints it out.
From assembly language’s point of view, a BASIC subprogram is just like a BASIC
function, except that we don’t have to load AX (and possibly DX) with a return value.
Here, we just read the ASCII code passed to us and use interrupt &H21 service 2 to print
it out:

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, EBX] gload into DL
MOV AH, 2
INT 21H
> BASIC/Assembly Language Interface 487

Listing 11-9 shows the whole thing.

Listing 11-9. PRINTCHAR.ASM.


»~MODEL MEDIUM, BASIC
- CODE

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

You can use PrintChar( ) this way:

REM Example of Assembly Language interface -- Add5 adds 5 to an integer

DECLARE SUB PrintChar(V%)

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):

»MODEL MEDIUM, BASIC


- CODE

PUBLIC PrintSum
PROC FAR USES DI SI DS ES, VALUE1:WORD, VALUE2: WORD
PrintSum

;Pick addr of VALUE from stack (BASIC passes ref.)


MOV BX, VALUE1
MOV DX, CBX]
MOV BX, VALUE2
ADD DX, CBX]
ADD DX, "0"
MOV AH, 2
INT 21H

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

A Subprogram with a String Parameter


Now let’s get a little fancy. We can develop a subprogram that reads a string passed
from BASIC (as opposed to GetQQQ$( ), which was a function that returned a string to
BASIC). For example, let’s develop a subprogram named PrintString( ) that prints a string
which has been passed as an argument, like this:

REM Example of Assembly Language interface -- Prints a string

DECLARE SUB PrintString(Strg$)

CALL PrintString("This is a test.") e

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.

PrintString( ) with BC.EXE and QB.EXE


We'll start off by examining the process under BC.EXE and QB.EXE. In this case, the
calling program passes us the address of the string descriptor when it makes the call
CALL PrintString(“This is a test.”). To decode that descriptor, we load the length of the
string (the first word in the string descriptor) into CX, and the address of the string data
(the second word in the descriptor) into BX like this:
> BASIC/Assembly Language Interface 489

PrintString PROC FAR USES DI SI DS ES, The StringWORD


:

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:

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] 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.

Listing 11-10. PRINTSTRING. ASM


-MODEL MEDIUM, BASIC
- CODE

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

PrintString( ) with QBX.EXE


It’s a little more complicated with QBX. Instead of being able to read the string’s length
and address from the string descriptor, we have to use two internal functions internal to
QBX to find that information. These functions are called StringLength(Descriptor) and
StringAddress(Descriptor), and the argument you pass is the string descriptor that you
want to decode.
We'll start the code segment by using StringLength( ) to find the length of the string
we've been passed. Keep in mind that what was passed to us was not the string’s descrip-
tor but the address of the string’s descriptor. That’s fine, however, because we’ll want to
pass that address to StringLength( ) and StringAddress( ) anyway. We start off by calling
StringLength( ) with the address of the string descriptor we just got from the calling
program:

-MODEL MEDIUM, BASIC


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 zload string length into CX for LOOP

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):

«MODEL MEDIUM, BASIC

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

> PUSH The String


EXTRN StringAddress: PROC
CALL StringAddress

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:

«MODEL MEDIUM, BASIC

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:

-MODEL MEDIUM, BASIC

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

MOV -CX, AX jload string length into CX for LOOP

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.

Listing 11-11. PRINTSTRING.ASM (QBX.EXE Version).


-MODEL MEDIUM, BASIC

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

PUSH The String


EXTRN StringAddress: PROC
CALL StringAddress
PUSH DS ;Save DS
MOV DS, DX 70S € String's segment
MOV BX, AX 7 BX String's offset

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.

A Subprogram with an Array Parameter


As our last example, we're going to see how to pass arrays to a subprogram. As
mentioned earlier, array descriptors are complex to work with. It is far better to pass the
actual address of the first element of the array.
This process also differs with QBX.EXE, but this time, QB.EXE and QBX.EXE operate
the same way. Arrays can be stored anywhere in memory with both of them, so we'll need
to pass both the segment and offset address of the first array element to our assembly
language routine. In BC.EXE, arrays are just stored in the data segment, so we'll only need
to pass a single word address (the data segment address is already in DS).
Let’s write a subprogram to print out the first element of an array, called PrintFirstEle-
ment( ), and see how this works.

PrintFirstElement with BC.EXE

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)):

REM Example of PrintFirstElement


REM Arrays have complex descriptors ~- have to pass addr of first element
REM Note use of BYVAL in SUB declaration.

DECLARE SUB PrintFirstElement (BYVAL Addr AS INTEGER) e


DIM AC1 TO 6) AS INTEGER

AC1)
AC2)
AC3)
AC4)
AC5)
AC6) nunhun
wt =
AuUFWN

PRINT “AC1) = "7


(VARPTR(A(1))) -
CALL PrintFirstElement
494 » Advanced BASIC

Now we have to write the assembly language procedure PrintFirstElement( ). We will


receive the address of the first element of the array. All we have to do is to load that
element into DL and print it (assuming that it is in the range 0 to 9):

PrintFirstElement PROC FAR USES DI SI DS ES, The_Array_Addr:WORD

> MOV BX, The _Array_Addr


2 MOV DX, CBXIJ
ADD DX, “0”
MOV AH,2
INT 21H

And that’s it. Listing 11-12 shows the whole procedure (BC.EXE version).

Listing 11-12. PrintFirst Element (BC.EXE Version).


-MODEL MEDIUM, BASIC
CODE

PUBLIC PrintFirstElement
PrintFirstElement PROC FAR USES DI SI DS ES, The_Array_Addr: WORD

MOV BX, The Array _Addr


MOV DX, [CBX]
ADD DX, “O"
MOV AH,2
INT 21H

RET
PrintFirstElement ENDP
END

PrintFirstElement with QB.EXE and QBX.EXE

Arrays can be stored anywhere in memory with QBX.EXE and QB.EXE, so


we have to
pass the element’s segment address and its offset to PrintFirstElement( ):
> BASIC/Assembly Language Interface 495

REM Example of PrintFirstElement with @Bx


REM Arrays have complex descriptors -- have to pass addr of first element
REM Note use of BYVAL in SUB declaration.

DECLARE SUB PrintFirstElement (BYVAL Addr1 AS INTEGER, BYVAL Addr2 AS INTEGER)


DIM AC1 TO 6) AS INTEGER

AC1)
AC2)
AC3)
AC4)
AC5)
AC6) ouunun
wn
Aur

PRINT “AC1) = ";


CALL PrintFirstElement(VARSEG(A(1)), VARPTR(A(1))) e

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.

Listing 11-13. PrintFirstElement (QB.EXE and QBX.EXE Version).


-MODEL MEDIUM, BASIC
- CODE

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

"ie one, aed a ae pal


oy
es

; =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> .

5 sab wiltnte bourne


ant aoe

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

Interrupt 8 Time of Day


This is another hardware interrupt. This interrupt is called to update the internal
time of day (stored in the BIOS data area) 18.2 times a second. If the date needs to
be changed, this interrupt will handle that too.
500 P& Advanced BASIC

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

Mode Display | Number


(in AL) Lines of Colors Adapters Maximum Pages
0 40x25 B @ W text CGA, EGA, VGA 8
1 40x25 Color text CGA, EGA, VGA 8
2 80x25 B & W text CGA, EGA, VGA 4 (CGA) 8 (EGA, VGA)
3) 80x25 Color text CGA, EGA, VGA 4 (CGA) 8 (EGA, VGA)
4 320x200 4 CGA, EGA, VGA 1
5 320x200 B&W CGA, EGA, VGA 1
6 640x200 2 (on or off) CGA, EGA, VGA 1
7 80x25 Monochrome MDA, EGA, VGA 1 (MDA) 8 (EGA, VGA)
8 160x200 16 Pir 1
9 320x200 16 POE I
AH 640x200 1 PGir 1
BH Reserved for future use.
CH Reserved for future use.
DH 320x200 16 EGA, VGA 8
EH 640x200 16 EGA, VGA 4
> BIOS and DOS Reference 501

Mode Display Number


(in AL) Lines of Colors Adapters Maximum Pages
FH 640x350 monochrome EGA, VGA 2
10H 640x350 16 EGA, VGA #
11H 640x480 2 VGA il
12H 640x480 16 VGA l
13H 320x200 256 VGA iL
INT 10H Service 1 Set Cursor Type
Input
AM=1
CH = Cursor Start Line
CL = Cursor End Line

Output
New Cursor

INT 10H Service 2 Set Cursor Position


Input
DH,DL = Row, Column
BH = Page Number
AH=2

Output
Cursor position changed

|NOTE: | DH,DL = 0,0 = Upper Left

INT 10H Service 3 Find Cursor Position


Input
BH=Page Number
AH=3

Output
DH,DL=Row, Column of Cursor.
CH,CL=Cursor Mode currently Set.
502 & Advanced BASIC

INT 10H Service 4 Read Light Pen Position


Input
AH=4

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

INT 10H Service 5 Set Active Display Page


Input
AL=0-7 (Screen modes 0,1)
0-3 (Screen modes 2,3)
AH=95

Output
Active Page Changed

|NOTE: | Different pages available in alphanumeric modes only (graphics adapters).

INT 10H Service 6 Scroll Active Page Up


Input
AL=#Lines blanked at bottom (O—Blank whole area).
CH,CL=Upper Left Row,Column of area to scroll.
DH,DL=Lower Right Row,Column of area to scroll.
BH=Attribute used on blank line.
AH=6

INT 10H Service 7 Scroll Active Page Down


Input
AL=#Lines blanked at bottom (O—Blank whole area).
CH,CL=Upper Left Row,Column of area to scroll.
DH,DL=Lower Right Row,Column of area to scroll.
BH=Attribute used on blank line.
AH=7
> BIOS and DOS Reference 503

INT 10H Service 8 Read Attribute and Character at Cursor Position


Input
BH = Page Number
AH=8

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

INT 10H Service A Write Character ONLY at Cursor Position


Input
BH=Page Number
CX=Count of characters to write
AL=IBM ASCII code
AH=0AH

Output
Character written on screen at Cursor Position

INT 10H Service B Set Color Palette


Input
BH=Palette Color ID
BL BH=0—9BL=Background Color
BH=1—>BL=Palette Number; (0=Green/Red/Yellow), (1=Cyan/Magenta/White)
AH=11
504 »& Advanced BASIC

INT 10H Service C Write Dot


Input
DX=Row Number(0-199) [0,0] is upper left.
CX=Column Number(0-319,639)
AL=Color Value (0-3)
AH=12

|NOTE: If bit 7 of AL is 1, the color value is XORed with the current value of the dot.

INT 10H Service D Read Dot


Input
DX=Row Number(0-199)
CX=Column Number(0-319,639)
AH=13

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.

INT 10H Service E Teletype Write to Active Page


Input
AL=IBM ASCII code
BL=Foreground Color (Graphics mode).
AH=14
INT 10H Service FH Return Video State ©
Input
AH=15

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

INT 10H Service 10H Set Palette Registers


Default Palette Colors (0-15) on EGA

Color Value Color rgbRGB


Black 000000
Blue 000001
Green 000010
Cyan 000011
Red 000100
Magenta 000101
Brown 010100
White 000111
Dark gray 111000
OOMONA
©
UAWNE Light blue 111001
Light green 111010
Light cyan 111011
Light red 111100
Light magenta 117101
Yellow 111110
Intense white La

INT 10H Service 10H Function 0 Set Individual Palette Register


Input
AH = 10H
AL =0
BL = Palette register to set (0-15)
BH = Value to set (0-63)

INT 10H Service 10H Function 1 — Set Overscan (Border) Register


Input
AH = 10H
BH = Value to set (0-63)
ers
INT 10H Service 10H Function 2 — Set All Palette Regist
Input
AH = 10H
AL =2
506 P Advanced BASIC

ES:BX = Address of a 17-byte table holding color selections (0 - 63)


Bytes 0 - 15 hold color selections for palette registers 0 - 15
Byte 16 holds the new overscan (border) color
INT 10H Service 10H Function 7 — Read Individual Palette Register
Input
An = 10H
AL=7
BL = Register to read (color value)

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

BH = 15 Selects the 15th block of 16 DAC registers


BH = 16 Selects the 16th block of 16 DAC registers
INT 10H Service 11H — Character Generator

INT 10H Service 12H — Alternate Select


Input
AH = 12H
BL = 30H
AL = 0 —> 200 screen scan lines
=] 350 screen scan lines
we 400 screen scan lines

INT 11H Equipment Determination


Output
Bits of AX
15,14 = Number of Printers.
13 Not used
12 Game Adapter attached
11,10,9 Number of RS232 cards installed
8 Unused.
508 Pb Advanced BASIC

7,6 Number of Diskette Drives.


(00-1;01-32;10-43;11-4 If Bit 0 = 1)
5,4 Video Mode (00 Unused; 01=40x25 Color Card;
10=80x25 Color Card, 11=80x25 Monochrome)
3,2 Motherboard RAM:
(00=16K,01=32K,10=48K,
11=64K)
1 Not used
O = 1 if there are diskette drives attached

INT 12H Determine Memory Size


Output
AX=Number of Contiguous 1K Memory Blocks
INT 13H Service 0 Reset Disk
Input
AH=0

Output
No carry > AH=0, Success
Carry > AH=Error Code (see Service 1)

|NOTE: | Hard disk systems: DL=80H->reset diskette(s); DL=81H-—>reset hard disk.

INT 13H Service 1 Read Status of Last Operation


Input
AH=1

Output (Disk Error Codes)


AL=00 No Error
Al=O1 Bad Command passed to controller
AL=02 Address Mark not found
AlL=03 Diskette is Write Protected
AL=04 Sector not found
AL=05 Reset failed
AL=07 Drive parameters wrong
AL=09 DMA across segment end
ater > BIOS
aeeeadie-caetapueenteeteinienetrepeetertieesteematneesom and DOS
npremmienmesia
ok Reference si 509
memcrmmnmmions im
en

AL=0BH Bad track flag seen


AL=10H © Bad error check seen
AL=11H Data is error corrected
AL=20H Controller failure
AL=40H — Seek operation has failed
AL=80H No response from disk
AL=OBBH_ Undefined error
AL=OFFH Sense operation failed

|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.

INT 13H Service 2 Read Sectors into Memory


Input
AH=2

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.

INT 13H Service 3 Write Sectors to Disk


Input
AH=3

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.

INT 13H Service 4 Verify Sectors


Input
AH=4
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 (bits 0-5)
AL=Number of Sectors (Floppies 1-8; Hard Disks 1-80H; Hard Disks Read/Write
Long 1-79H).

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

INT 13H Service 8 Return Drive Parameters


This service works only on hard disks and PS/2s.

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.

INT 13H Services OAH and OBH Reserved

INT 13H Service OCH Seek


This service works ONLY on hard disks.

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.

INT 13H Service ODH Alternate Disk Reset

INT 13H Services OEH and OFH Reserved

INT 13H Service 10H Test Drive Ready

INT 13H Service 11H Recalibrate Hard Drive


This service works ONLY on hard disks.

Input
AH=11H (Read)

DL=Drive number (80H-87H allowed)

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

INT 13H Diagnostic Services


These services work ONLY on hard disks.

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.

INT 13H Service 19H Park Heads PS/2 Only


Input (PS/2)
DL = Drive Number

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.

INT 14H, AH=0 Initialize RS232 Port


Input
AH=0
Bits of AL:
0,1 Word Length 01-37 Bits, 11-8 Bits
2 Stop Bits 0-1, 1-2 stop bits.
3,4 Parity. 00O—None, 01—Odd, 11—Even
5,6,7. Baud Rate. OOO 110
OO1—> 150
010— 300
0ll—> 600
100— 1,200
101-2,400
110-4,800
111-—9,600

INT 14H, AH=1 Send Character through Serial Port


Input
AH=1
AL=Character to send
514 Pb Advanced BASIC

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)

INT 14H, AH=3 Return Serial Port’s Status


Input
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

INT 15H Cassette I/O


Input
AH=0 — Turn Cassette Motor On.
AH=1 — Turn Cassette Motor Off.
AH=2 — Read one or more 256-byte blocks. Store data at ES:BX. CX=Count of
Bytes to read.
AH=3 — Write one or more 256-byte blocks from ES:BX. Count of bytes to
write in CX.

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.

INT 16H, Service 0 Read Key from Keyboard


Input
AH = 0

Output
AH=Scan Code AL=ASCII code

INT 16H, Service 1 Check if Key Ready to be Read


Input
AH = 1

Output
Zero Flag=1 — Buffer Empty
Zero Flag=0 — AH=Scan Code
AL=ASCII Code
516 P& Advanced BASIC

INT 16H, Service 2 Find Keyboard Status


Input
Aliis= 2

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.

INT 17H Service 1 Initialize Printer Port


Input
AH=1
DxX=Printer Number (0,1,2)

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.

INT 17H Service 2 Read Printer Status into AH


Input
AH=2
DX=Printer Number (0,1,2)
> BIOS and DOS Reference 517

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.

|NOTE: | Timer count increments by 65,536 in one hour.

INT 1AH Service 1 Set Time of Day


Input
AH=1
CX=High Word of Timer Count
DX=Low Word of Timer Count

|NOTE: | Timer count increments by 65,536 in one hour.

INT 1BH Keyboard Break Address

INT 1CH Timer Tick Interrupt

INT 1DH Video Parameter Tables

INT 1EH Diskette Parameters

INT 1FH Graphics Character Definitions


518 Pb Advanced BASIC

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

|NOTE: | Checks for “C or “Break.

INT 21H Service 2 Character Output on Screen


Input
AH=2
DL=Character’s ASCII code.

INT 21H Service 3 Standard Auxiliary Device Input


Input
AH=3

Output
Character in AL

INT 21H Service 4 Standard Auxiliary Device Output


Input
AH=4
DL=Character to output
> BIOS and DOS Reference 519

INT 21H Service 5 Printer Output


Input
AH=5
DL=Character to output.
INT 21H Service 6 Console I/O without Echo
Input Output
AH = 6
DL = FFH — Zero flag set if no character was ready. Otherwise, AL holds
character’s ASCII code.
DL<FFH - Type ASCII code in DL on screen.

|NOTE: | Does NOT check for *C or “Break.

INT 21H Service 7 Console Input without Echo


Input
AH = 7

Output
AL = ASCII code of struck key NO Echo on screen.

|NOTE: Does NOT Check for “C or “Break.

INT 21H Service 8 Console Input w/o Echo with “C Check


Input
AH = 8

Output
AL = ASCII code of struck key. Does NOT echo the typed key.

|NOTE: | Checks for “C or “Break.


520 & Advanced BASIC

DOS INT 21H Service 9 String Print


Input
DS:DX point to a string that ends in ‘$’
AH=9
INT 21H Service A String Input
Input
AH = OAH
[DS:DX]=Length of buffer

Output
Buffer at DS:DX filled
Echo the typed keys

|NOTE: | Checks for “C or “Break.

INT 21H Service OBH Check Input Status


Input
AH = OBH

Output
AL = FF — Character ready
AL = 00 — Nothing to read in

|NOTE: | “Break is checked for.


ee

INT 21H Service OCH Clear Keyboard Buffer and Invoke Service
Input
A= 0CH
AL = Keyboard Function #

Output
Standard Output from the selected Service

NOTE: | “Break is checked for.


ae
ee> BIOS and DOS Reference 521

INT 21H Service O0DH Disk Reset


Input
AH=0DH

INT 21H Service OEH Select Disk


Input
AH=0EH
DL=Drive Number (DL=0-A; DL=1-B; and so on)
INT 21H Service OFH Open Preexisting File
Input
DS:DX points to an FCB
AH=0FH

Output
AL=0 -— Success
AL=FF —> Failure

INT 21H Service 10H Close File


Input
DS:DX points to an FCB.
AH=10H

Output
AL=0 — Success
AL=FF — Failure

INT 21H Service 11H Search for First Matching File


Input
DS:DX points to an unopened FCB
AH=11H

Output
AL=FF — Failure
AL=0 — Success. DTA holds FCB for match

|NOTE: | Note DTA is at CS:0080 in .COM files on startup.


522 & Advanced BASIC

INT 21H Service 12H Search for Next Matching File


Input
DS:DX points to an unopened FCB
AH=12H

Output
AL=FF —> Failure
AL=0 — Success. DTA holds FCB for match

|NOTE: | Use this Service after Service 11H.

INT 21H Service 13H Delete Files


Input
DS:DX points to an unopened FCB
A= T3r

Output
AL=FF —> Failure
AL=0 — Success

INT 21H Service 14H Sequential Read


Input
DS:DX points to an opened FCB.
Ali= 14h
Current Block and Record Set in FCB.

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

|NOTE: | Record address increased.


EE
> BIOS and DOS Reference 523

INT 21H Service 15H Sequential Write


Input
DS:DX points to an opened FCB
AH=15H
Current Block & Record Set in FCB

Output
One record read from DTA and written.
AL=0 Success.
1 Disk full.
2 DTA Segment too small for record.

|NOTE: | Record address increased.

INT 21H Service 16H Create File


Input
DS:DX points to an unopened FCB.
AH=16H

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

INT 21H Service 18H Internal to DOS

INT 21H Service 19H Find Current Disk


Input
AH=19H

Output
AL=Current Disk (0=A, 1=B, and so on).

INT 21H Service 1AH Set the DTA Location


Input
DS:DX points to new DTA address
AH=1AH

DTA= Disk Transfer Address, the data area used with FCB services. Default
DTA is 128 bytes long, starting at CS:0080 in the PSP.

INT 21H Service 1BH FAT Information for Default Drive


Input
AH=1BH

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.
—————

INT 21H Service 1CH FAT Information for Specified Drive


Input
AH=1CH
DL=Drive Number (0=Default; 1=A...)
> BIOS and DOS Reference 525

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.

INT 21H Services 1DH - 20H Internal to DOS

INT 21H Service 21H Random Read


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 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

INT 21H Service 23H File Size


Input
DS:DX points to an unopened FCB.
AH=23H

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.

INT 21H Service 25H Set Interrupt Vector


Input
AH=25H
AL = Interrupt Number
DS:DX = New Address

|NOTE: | This service can help you intercept an interrupt vector.

INT 21H Service 26H Create a New Program Segment (PSP)

INT 21H Service 27H Random Block Read


Input
DS:DX points to an opened FCB.
Set FCB’s Random Record field
at DS:DX+33 and DS:DX+35

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.

INT 21H Service 28H Random Block Write


Input
DS:DX points to an opened FCB
Set FCB’s Random Record field
at DS:DX+33 and DS:DX+35

CX=number of records to write.


AH=28H

Output
AL=00 success
=01 disk is full
=02 not enough space in DTA segment

Random Record Fields set to access


next record.

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

INT 21H Service 29H Parse Filename


Input
DS:SI = Command line to parse.
ES:DI = Address to put FCB at.
528 & Advanced BASIC

AL = Bit 0=1 —Leading separators are scanned off command line.


Bit 1=1 — Drive ID in final FCB will be changed ONLY if a drive was
specified.
Bit 2=1 — Filename in FCB changed ONLY if command line includes
filename.
Bit 3=1 — Filename extension in FCB will be changed ONLY if
command line contains a filename extension.
AH= 29H

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.

INT 21H Service 2AH Get Date


Input
AH=2AH

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

INT 21H Service 2CH Get Time


Input
AH=2CH

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

INT 21H Service 2EH Set or Reset Verify Switch


Input
AH=2EH
DL=0
AL=1 — Turn Verify On
=0 — Turn Verify Off
INT 21H Service 2FH Get Current DTA
Input
AH=2FH

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

INT 21H Service 30H Get DOS Version Number


Input
AH=30H

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.

INT 21H Service 31H Terminate Process and Keep Resident


Input
AH=31H
AL=Binary Exit Code
DX=Size of memory request in paragraphs

|NOTE: | Exit code can be read by a parent program with Service 4DH. It can also be
tested by ERRORLEVEL commands in batch files.

INT 21H Service 32H Internal to DOS

INT 21H Service 33H Control-Break Check


Input
AH=33H
AL=0 — Check state of “Break checking
=1 — Set the state of “Break checking (DL=0—> Turn it Off:DL=1—> Turn it
On)

Output
DL=0 > Off
DL=1 > On
INT 21H Service 34H Internal to DOS
> BIOS and DOS Reference 531

INT 21H Service 35H Get Interrupt Vector


Input
AH=35H
AL=Interrupt Number

Output
ES:BX = Interrupt’s Vector

INT 21H Service 36H Get Free Disk Space


Input
AH=36H

DL=Drive Number (0=Default; 1=A...)

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.

INT 21H Service 37H Internal to DOS

INT 21H Service 38H Returns Country Dependent Information


Input
AH=38H
DS:DX = address of 32-byte block
AL=0

The 32-byte block looks like this:

2 Bytes DATE/TIME Format.


1 Byte of currency symbol (ASCII)
1 Byte set to 0.
1 Byte thousands separator (ASCII)
532 DP Advanced BASIC

1 Byte set to 0.
1 Byte decimal separator (ASCII)
1 Byte set to 0.
24 Bytes used internally.

The DATE/TIME format has these values:

0 = USA (H:M:S M/D/Y)


1 = EUROPE (H:M:S D/M/Y)
2 = JAPAN (H:M:S D:M:Y)

Output
Filled in 32-byte block (see below)

|NOTE: In DOS 3+ you can set, as well as read, these values.

INT 21H Service 39H Create a Subdirectory


Input
AH=39H
DS:DX point to ASCIIZ string with directory name

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

Carry AH has error value


AH=3 Path Not Found
AH=5 Access denied or
Subdirectory not empty.
INT 21H Service 3BH Change Current Directory
Input
AH=3BH
DS:DX point to ASCIIZ string with directory name.

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

Access Codes: AL=0 File Opened for Reading.


AL=1 File Opened for Writing.
AL=2 File Opened for Reading and Writing.
Access Code DOS 3+: isssraaa
534 P& Advanced BASIC

i= 1 — file is not to be inherited by child processes


i= 0 > file handle will be inherited
sss = 000 — Compatibility Mode
sss = 001 — Deny All
sss = 010 — Deny Write
sss = 011 — Deny Read
sss = 100 — Deny None
r = reserved
aaa = 000 — Read Access
aaa = 001 — Write Access
aaa = 010 — Read/Write Access

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

INT 21H Service 40H Write to File or Device


Input
DS:DX = Data Buffer Address

CX=Number of bytes to write


BX=File Handle
AH=40H

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.

INT 21H Service 41H Delete a File


Input
DS:DX = ASCIIZ filename
AH=41H

Output
No Carry— Success
Carry AL=2 File Not Found
AL=5 Access Denied

|NOTE: | No wildcards allowed in filename.

INT 21H Service 42H Move Read/Write Pointer


Input
BX=File Handle
CX:DX=Desired offset.
AL=Method Value—
AH=42H
536 P& Advanced BASIC

Method Values (AL):


AL=0 — Read/Write Pointer moved to CX:DX from the start of the file
AL=1 — Pointer incremented CX:DX bytes
AL=2 — Pointer moved to end-of-file plus offset (CX:DX)

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

INT 21H Service 45H Duplicate a File Handle


Input
BX=File Handle to duplicate
AH=45H

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

CX=Second File Handle


AH=46H

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.

|NOTE: Drive letter is NOT included in returned ASCIIZ string.

INT 21H Service 48H Allocate Memory


Input
AH=48H

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

Parameter Block for AL = 0:

Segment Address of environment to pass (Word)


Address of command to put at PSP+80H (DWord)
Address of default FCB to put at PSP+5CH (DWord)
Address of 2nd default FCB to put at PSP+6CH (DWord)
> BIOS and DOS Reference 539

Parameter Block for AL = 3:

Segment Address to load file at (Word)


Relocation Factor for image (Word)

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.

|NOTE: This service can end a program.

INT 21H Service 4DH Get Return Code of Subprocess


Input
AH=4DH

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.

INT 21H Service 4FH Find Next Matching File


Input
Use Service 4EH BEFORE 4FH.
AH=4FH

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.

INT 21H Services 50H-53H Internal to DOS

INT 21H Service 54H Get Verify State


Input
AH=54H

Output
AL=0— verify is OFF
1— verify is ON
INT 21H Service 55H Internal to DOS

INT 21H Service 56H Rename File


Input
DS:DX=ASCIIZ filestring to be renamed.
ES:Dl= ASCIIZ filestring that holds the new name.
AH= 56H

Output
No Carry success.
Carry AL=3 Path Not Found.
AL=5 Access Denied.
AL=17 Not Same Device.

|NOTE: | File CANNOT be renamed to another drive.

INT 21H Service 57H Get or Set a File’s Date & Time
Input
BX=File handle.
AL=0—Get Date & Time

AL=1—Set Time to CX.


Set Date to DX.
542 b& Advanced BASIC

Output
No Carry:
CX returns Time
DX returns Date

File’s date and time set

Carry—AL=1 Invalid Function Number


6 Invalid Handle
The time and date of a file are stored like this:

Time = 2048 X hours + 32 X minutes + Seconds/2


Date = 512 X (Year - 1980) + 32 X Month + Day

INT 21H Service 58H Internal to DOS

INT 21H Service 59H Get Extended Error DOS 3+


Input
AH = 59H
BX = 0

Output
AX = extended error
BH = error class
BL = suggested action
CH = locus

|NOTE: | This error handling service is very lengthy, and involves


the many DOS 3+
extended errors.
RRR
S GREET oee er
RST A

INT 21H Service 5AH Create Unique File DOS 3+


Input
AH = 5AH
DS:DX = Address of an ASCIIZ path (ending with
“\”)
CX = File’s attribute
> BIOS and DOS Reference 543

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

INT 21H Service 5E00H Get Machine Name DOS 3+


Input
AX = 5E00H
DS:DX = Buffer for computer name

Output
DS:DX = ASCIIZ computer name
CH = 0 > Name not defined
CL = NETBIOS number
AX = Error if carry set
544 P& Advanced BASIC

INT 21H Service 5E02 Set Printer Setup DOS 3+


Input
AX = 5E02H
BX = Redirection list index
CX = Length of Setup String
DS:DI = Pointer to printer setup buffer

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

INT 21H Service 5F04H Cancel Redirection DOS 3+


Input
AX = 5F04H
DS:SI = ASCIIZ device name or path

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.

INT 21H Service 67H Set Handle Count DOS 3.30


Input
AX = 67H
BX = Number of allowed open handles (up to 255)

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.

INT 22H Terminate Address

INT 23H Control Break Exit Address


546 > Advanced BASIC

INT 24H Critical Error Handler


AH filled this way:

0 Diskette is write protected


1 Unknown Unit
2 The requested drive is not ready
3 Unknown command
4 Cyclic redundancy check error in the data
5 Bad request structure length
6 Seek error
7 Media type unknown
8 Sector not found
9 The printer is out of paper
A Write fault
B Read fault
C General failure

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

AH=02 Address Mark missing.


AH=00 Error unknown.

|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

INT 26H Absolute Disk Write


Input
AL=Drive Number
CX=Number of Sectors to write
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
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.

INT 27H Terminate and Stay Resident


Input
DS:DX = point directly after end of code which is to stay resident.
548 P Advanced BASIC

INTs 28H-2EH Internal to DOS

INT 2FH Multiplex Interrupt

INT 30H-3FH DOS Reserved


INT 40H-5FH Reserved

INT 60H-67H Reserved for User Software


INTs 68H-7FH Not Used

INTs 80H-85H Reserved by BASIC

INTs 86H-FOH Used by BASIC Interpreter


INTs F1H-FFH Not Used
Index
80 x 86 JBE Jump if Below or Equal) instruction, screen mode, 369-371
flags, 14, 407 433
registers, 10-15
BIOS End of Interrupt (interrupt OBH-OFH),
JCXZ (Jump if CX = 0) instruction, 433 368, 500
segment, 414 JE Qump if Equal) instruction, 433 BIOS interrupts, 10-15, 49, 367-376
viewing, 406-407 JMP instruction, 424 reference, 499-517
JNA (Jump if Not Above) instruction, 433 BLOAD routine, 211-212, 238
A JNAE (Jump if Not Above or Equal) Bootstrap (interrupt 19H), 375, 517
A.OB] file, 459 instruction, 433 Breakpoint (interrupt 3), 367, 499
A.QLB Quicklibrary, 458 JNB Jump if Not Below) instruction, 433 breakpoints, 347-348, 351, 358, 361
Absolute Disk Read (interrupt 25H), 399, JNBE Jump if Not Below or Equal) BSAVE statement, 209-212, 238
546-547 instruction, 433 Button Status (interrupt 33H service 3), 94,
Absolute Disk Write (interrupt 26H), 399, JNE Qump if Not Equal) instruction, 433 216
547 JNZ (ump if Result was Not Zero)
ADC (add with carry) instruction, 475 instruction, 433 c
ADD instruction, 429-430, 432, 475 jump instruction, +24 CALL instruction, 451
Add Watch... option, 356 JZ Gump if Result was Zero) instruction, Cancel Redirection DOS 3+ (interrupt 21H
Add5 Function listing, 474 433 service 5F0O4H), 398, 545
Add5%( ) function, 473-474 keyboard input, 427-431 CAP.ASM file, 430
Add5Long Function listing, 476 LOOP instruction, 447-449 CAP.ASM Program listing, 430, 432-433
AddSLong&( ) function, 474-476 memory segmentation, 413-417 CAP.COM program, 427-431
Addem%( ) function, 483-486 MOV instruction, 405-406 carriage return, 434
ADDEM.ASM listing, 484 MOVS instruction, 488 Cassette I/O (interrupt 15H), 375, 515
ADDEM.ASM Using the Data Segment MUL instruction, 432, 440, 442-447 CGA monitor, 250, 252
listing, 486 POP instruction, 444, 446 Change Current Directory (interrupt 21H
addresses, offset and segment, 210 PrintString( )procedure, 458-459 service 3BH), 382, 533
Allocate Memory (interrupt 21H service PROC directive, 460-462, 473 Change File Attributes (interrupt 21H service
48H), 386, 537 procedures, 449-451, 453 43H), 386, 536
Alternate Disk Reset (interrupt 13H service PUSH instruction, 443 Character Generator (interrupt 10H service
ODH), 374, 512 receiving parameters from BASIC, 460-462 11), 369, 507
Alternate Select (interrupt 10H service 12H), RET (return) instruction, 450-451 Character Output on Screen (interrupt 21H
369, 507 returning values to BASIC, 462-463 service 2), 14-15, 377, 410-412, 430,
animation, 199, 229-238 SBB (subtract with borrow), 475 446, 486, 488, 518
Array Example listing, 299-300 SHL (shift left) instruction, 440 Character String Print (interrupt 21H service
arrays, 299-302, 468-470 SHR (shift right) instruction, 440 9), 49, 377, 412, 425-427, 445
descriptors, 468-470 source files, 417 Character Written On-screen (interrupt 10H
dynamic, 231 storing character strings in memory, 425- service 9), 51, 137
passing to subprogram, 493-495 427 characters
storing in column major order, +70 string handling, 488-492 not echoing on-screen, 4
window data, 42 SUB instruction, 429-430, 432 printing ASCII code, 486-487
arrow keys, reading, 7-9 words, 464 separating out in strings, 6
ASCII codes, 10 Assembly Language Version of BASIC ChartScreen( ) statement, 242
converting letters into digits, 22 Function That Adds Two Integers Check if Key Ready to be Read (interrupt
ASCIIZ string, 388 listing, 457 16H service 1), 375, 515
ASM file, 417, 420, 449 Attribute() array, 42 Check Input Status (interrupt 21H service
assembler directives, 417-420 Attributes() array, 129 OBH), 377, 520
.CODE, 417-418, 471, 481 averaging numbers, 3 CHRTB.BI file, 243
DATA, 422, 480, 485 Averaging Program listing, 3 CHRTBEFR.QLB file, 241
.MODEL, 471 CIRCLE statement, 220-221
DB (define byte), 421-425 B circular buffers, 308-309
DW (define word), 421-425, 480 bar charts, 246-248 Clear Keyboard Buffer and Invoke Service
END, 419-420 base pointer, 462 (interrupt 21H service OCH), 377,
ENDP, 449-450 BASIC 520
labels, 418 assembly language interface, 457-495 clock, drawing, 227-229
OFFSET, 426 base pointer, 462 Close File (interrupt 21H service 10H), 379,
ORG, 419 calling convention, 460 521
positioning code in CS (code segment), internal representation of data, 463-470 Close File Handle (interrupt 21H service
418-419 linking into, 457-459 3EH), 385, 534
PROC, 449-450 memory model, 471 clusters, 304-305
assembly language, 403-453 offset address, 460 CMP (compare) instruction, 431-433
ADC (add with carry) instruction, 475 receiving values from assembly language, CMPS instruction, 488
ADD instruction, 429-430, 432, 475 462-463 CODE assembler directive, 417-418, 471,
adding data, 421-425 sending parameters to assembly language, 481
assembler directives, 417-420 460-462 CodeView, 343, 354-364
BASIC interface, 457-495 storing data, 463-470 compiling program with BC.EXE, 355
CALL instruction, 451 two's complement notation, 464-466 evaluating expressions, 360
CMP (compare) instruction, 431-433 BASIC Interpreter (interrupt 86H-FOH), 400 independently scrolling windows, 358
CMPS instruction, 488 BC.EXE compiler, 458-459, 476-479, 488- screen, 356
comments, 427 489, 493-494 starting in sequential mode, 356
conditional jumps, 431-433 /Zi switch, 355 tracepoints, 363
debugging with DEBUG.COM, 343 compiling program for CodeView, 355 watchpoints, 363
DIV instruction, 442-447 descriptors, 468 color invert characters, 112
indirect addressing, 438-439 UIASM.OB] file, 80-81 COLOR statement, 207
JA Jump if Above) instruction, 432-433 BI files, 185, 241 COLOR/PRINT statement, 49
JAE (Jump if Above or Equal) instruction, bias, 466 column major order, 470
433 binary trees, 309-317 COM files, 416-422, 424-425
JB Jump if Below) instruction, 432-433 BIOS (Basic Input/Output System), 367-376 comments, assembly language, 427
549g
550 »& Advanced BASIC

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 directly from S & S.


¢ Diskette replacement when applicable.
¢ Purchase of future product upgrades at special prices.
° Subscriptions to Hint Books and newsletters where applicable.

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

& SCHUSTER, INC. ("S&S") to the terms and


The software in this package is provided to You on the condition that You agree with SIMON
the terms of this agreement if you open the
conditions set forth below. Read this End User Agreement carefully. You will be bound by your
along with
sealed diskette. If You do not agree to the terms contained in this End User License Agreement, return the entire product, price
receipt, to Brady, Simon & Schuster, Inc., 15 Columbus Circle, 14th floor, New York, NY 10023, Attn: Refunds, and your purchase
will be refunded. ; eee
documentation in this
S&S grants, and You hereby accept, a personal, nonexclusive license to use this software program and associated
package, or any part of it ("Licensed Product"), subject to the following terms and conditions:
1. License .
separate license,
The license granted to You hereunder authorizes You to use the Licensed Product on any single computer system. A
use the Licensed
pursuant to a separate End User License Agreement, is required for any other computer system on which You intend to
Product.
2. Term rag :
This End User License Agreement is effective from the date of purchase by You of the Licensed Product and shall remain in force until
terminated. You may terminate this End User License at any time by destroying the Licensed Product together with all copies in any form
made by You or received by You. Your right to use or copy the Licensed Product will terminate if You fail to comply with any of the terms
or conditions of this End User License Agreement. Upon such termination, You shall destroy the copies of the Licensed Product in your
possession.
3. Restriction against Transfer
This End User License Agreement, and the Licensed Product, may not be assigned, sublicensed, or otherwise transferred by You to another
party unless the other party agrees to accept the terms and conditions of the End User License Agreement. If You transfer the Licensed
Product, You must at the same time either transfer all copies, whether in printed or machine-readable form, to the same party or destroy
any copies not transferred.
4. Restrictions against Copying or Modifying the Licensed Product
The Licensed Product is copyrighted and except for certain limited uses as noted on the copyright page, may not be further copied without
the prior written approval of S&S. You may make one copy for backup purposes provided You reproduce and include the complete
copyright notice on the backup copy. Any unauthorized coyping is in violation of this Agreement and may also constitute a violation of
the United States Copyright Law for which You could be liable in a civil or criminal lawsuit. You may not use, transfer, copy, or otherwise
reproduce the Licensed Product, or any part of it, except as expressly permitted in this End User License Agreement.
5. Protection and Security
You shall take all reasonable steps to safeguard the Licensed Product and to ensure that no unauthorized person shall have access to it and
that no unauthorized copy of any part of it in any form shall be made.
6. Limited Warranty
If You are the original consumer purchaser of a diskette and it is found to be defective in materials or workmanship (which shall not include
problems relating to the nature or operation of the Licensed Product) under normal use, S&S will replace it free of charge (or, atS&S's option,
refund your purchase price) within 30 days following the date of purchase. Following the 30-day period, and up to one year ofter purchase,
S&S will replace any such defective diskette upon payment of a $5 charge (or, at S&S's option, refund your purchase price), provided that
the Limited Warranty Registration Card has been filed within 30 days following the date of purchase. Any request for replacement of a
defective diskette must be accompanied by the original defective diskette and proof
of date of purchase and purchase price. S&S shall have
no obligation to replace a diskette (or refund your purchase price based on claims of defects in the nature or operation of the Licensed
Product.
The software program is provided "as is" without warranty of any kind, either expressed or implied, including but not limited to the
implied warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and performance of the program
is with You. Should the program prove defective, You (and not S&S) assume the entire cost of necessary servicing, repair or correction.
Some states do not allow the exclusion of implied waraanties, so the above exclusion may not apply to You. This warrant gives you
specific legal rights, and You may also have other rights which vary from state to satate.
S&S does not warrant that the functions contained in the program will meet your requirements or that the operation of the program
will be uninterrupted or error free. Neither S&S nor anyone else who has been involved in the creation of production of this product
shall be liable for any direct, indirect, incidental, special, or consequential damages, whether arising out of the use or inability to use
the product, or any breach of a warranty, and S&S shall have no responsibility except to replace the diskette pursuant to this limited
warranty (or, at its option, provide a refund of the purchase price).
No sales personnel or other representative of any party involved in the distribution of the Licensed Product is authorized by S&S to make
any warranties with respect to the diskette or the Licensed Product beyond those contained in this Agreement. Oral statements do not
constitute warranties, shall not be relied upon by You, and are not part of this Agreement. The entire agreement between S&S and You
is embodied in this Agreement.
7. General
If any provision ofthis End User License Agreement is determined to be invalid under any applicable statute of rule of law, itshall be deemed
omitted and the remaining provisions shall continue in full force and effect. This End User License Agreement is to be governed by the
construed in accordance with the laws of the State of New York.
Simon & Schuster, Inc. and the authors make no warranties, express or implied in connection with the software, and expressly exclude
all warranties of fitness for a particular purpose. Simon & Schuster, Inc. and the authors shall have no liability for consequental, incidental,
or exemplary damages.
Advanced BASIC
REPLACEMENT ORDER FORM
Please use this form when ordering a 3.5-inch disk or a replacement for a defective diskette.

A. If ordering within thirty days of purchase


If a diskette is reported defective within thirty days of purchase, a replacement diskette will be provided free of charge.
The back of this card must be totally filled out and accompanied by the defective diskette and a copy of the dated sales
receipt. In addition, please complete and return the Limited Warranty Registration Card.

B. If ordering after thirty Days of purchase but within one year


If a diskette is reported defective after thirty days, but within one year of purchase and the Warranty Registration Card
has been properly filed, a replacement diskette will be provided to you for anominal fee of $5.00 (send check or money
order only). The back of this card must be totally filled out and accompanied by the defective diskette, a copy of the
dated sales receipt, and a $5.00 check or money order made payable to Simon & Schuster, Inc.

C. If ordering a 3.5 inch replacement disk


If you wish to order a 3.5-inch disk for this product, please complete the back of this card and mail it with your original
5.25-inch diskettes along with a nominal fee of $5.00 to cover shipping and handling (send check or money order only).
In addition, please complete and return the Limited Warranty Registration Card.

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.

Please fill in the information requested:


NAME PHONE NUMBER (_ )
ADDRESS
CITY : STATE ZIP
COMPUTER BRAND & MODEL DOS VERSION MEMORY K
Where did you purchase this product?
DEALER NAME? PHONE NUMBER (_)
ADDRESS
Oo J Se i ee || i 4
PURCHASE DATE PURCHASE PRICE
How did you learn about this product? (Check as many as applicable.)
STORE DISPLAY SALESPERSON MAGAZINE ARTICLE ADVERTISEMENT
OTHER (Please explain)
How long have you owned or used this computer?
LESS THAN 30 DAYS LESS THAN 6 MONTHS 6 MONTHS TO A YEAR OVER 1 YEAR
What is your primary use for the computer?

BUSINESS PERSONAL EDUCATION OTHER (Please explain)

Where is your computer located?

HOME OFFICE SCHOOL OTHER (Please explain)

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.

ie | am ordering a replacement diskette within 30 days. | have enclosed my original diskette


and a copy of the dated sales receipt. ISBN 0-13-658758-5

[| | 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

NAME PHONE NUMBER (_)


ADDRESS
CITY STATE ZIP

Please mail this request to: MICROSERVICES, 200 Old Tappan Road, Old Tappan, NJ 07675.
For more information, call (201) 767-5054.
a

College Marketing Group


50 Cross Street
Winchester, MA 01890

ATTN: CHERYL READ


CUT TO OPEN
THE PETER NORTON PROGRAMMING LIBRARY

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...

Enclosed disk contains all programs described in the book, plus a


bonus file encryption program, a special keyboard contoller and
handler, a function key interface, a window resizing utility and
more!

Works with Microsoft QuickBASIC® and BASIC PDS®.

The New Peter Norton Microcomputer


Libraries from Brady
The books in Peter Norton’s Hardware Library guide you to an
insider’s grasp of your computer and the way it works. This series
includes such best-selling classics as Inside the IBM PC, Inside the
Apple Macintosh and The Hard Disk Companion. Peter Norton’s
Programming Library offers books and book/disk utilities for
readers at all levels of expertise—beginning to intermediate to
advanced. They focus on creating programs that really work and
offer the hottest tips and techniques in the industry! For a direct,
accessible, no-nonsense approach to performance computing,
look to Brady’s Peter Norton Libraries.

Brady Publishing ™ Simon & Schuster ™ New York

ISBN 0-13-658758-5
|| 90000>

9°78 0136'587583 |

You might also like