What is a macro? Actually, it is nothing more than a simple
text-substitution. is created to
wherever it occurs in a program listing.
It so happens, however, that not only does such a seemingly trivial maneuver enable
the programmer to streamline his code and render it more readable, it opens the
door to a whole new operating system in which one can create the
BASIC language of one's
dreams — a subset of commands
and functions that integrate seamlessly with other PowerBASIC
Many, if not most, other
high-level languages do not support macros.
Visual Basic and C++ are prominent examples, and shame
on them for that. You can do better; in fact, your interest in
PowerBASIC suggests that you already have. Whatever guru Bob
Zale has not elected to provide you in the way of official keywords and functions
can be augmented with a little ingenuity on your own part. The sky is the
limit regarding the complexity of a macro system; the depths to which you might
wish to plunge are limited only by your imagination.
Some of the examples I would share admittedly are more useful than others. Simple code-shorteners render program listings more readable; whereas other macros provide more complicated benefits. The following suggestions are designed for the Console Compiler, but are not necessarily limited to it.
ODD vs. EVEN INTEGERS
Let's begin with something simple. Pascal features a keyword for identifying
whether an integer is odd (or not):
Odd(n). We can do the same:
MACRO Odd(N)= (N/2<>N\2)
Now your code can read:
IF Odd(myint) THEN MyInt *= 2
In this example, if a certain integer is odd, it is doubled. The intended function of the improved line of code is more readily apparent than this equivalent:
IF MyInt/2 <> MyInt\2 THEN MyInt *= 2
Yes, the same result could have been implemented thusly:
IF (MyInt MOD 2) THEN MyInt *= 2
You get the idea, however. The macro makes the code read more like English, and that always is a good thing. We might as well include in our library the function's corollary:
MACRO Even(N)= (N/2=N\2) -or- MACRO Even(N)= (N MOD 2=0)
Every instance of N in the definition is just a
place-holder for whatever integer actually is supplied when calling the macro;
it could just as easily read MOM or
Fahrenheit451. My own protocol is to tend to use
N as the standard numeric substitute and
A to represent string values, as a somewhat
self-documenting mechanism. The parameter characters are
case-specific; so the following macro definition would fail:
MACRO Odd(n)= (N/2<>N\2)
Note the use of Boolean math in the formula. This marvelous feature, relatively undocumented and utterly unavailable in many programming languages, is my favorite programming toy. More importantly, it is ideal for use in certain macros.
Note also that the previous macro definitions are enclosed in parentheses. Doing so seems unnecessary in these particular cases; but the practice usually cannot hurt, and sometimes the logic of a program statement makes it necessary to isolate a macro formula from the surrounding code.
The MODULUS FUNCTION
MOD(x) is a highly useful and favorite function that
returns the remainder of an integer division. So
(X MOD 4)
yields one of: 1,2,3,0. Frequently, however, the programmer would
prefer an output in the range of 1,2,3,4. This is easily accomplished
with an all-purpose macro:
MACRO ModX(N,M)= (((N+M-1) MOD M)+1)
Now, ModX(35,7) becomes 7 instead of zero. If a certain program
were to utilize, say,
Modulus-5 a lot, then a dedicated function might be
MACRO Mod5(N)= ((N+4) MOD 5 +1)
These adjustments accommodate a negative N:
MACRO ModZ(N,M)= (((N MOD M)+M) MOD M))
MACRO ModZ(N,M)= (((N MOD M)+M-1) MOD M +1)
So the all-purpose ModZ() might as well be used all the time, I suppose. All definitions have been enclosed in parentheses just in case.
SHELL TO "DOS"
Perhaps you prefer to utilize an API call for command-level activities; but the
built-in SHELL command works just fine for a single-statement
MACRO DOScmd= SHELL ENVIRON$("comspec")+" /c "+
Now, the next time you need to shell to the operating system (yes, I'll always
call it DOS), you don't have to remember the convoluted syntax or hunt for a reminder
of it elsewhere in your code; all you need to do is type the macro name followed
command-level directive in quotes:
DOScmd "dir /b myfolder > dirlist.txt"
Perhaps you already know most of this stuff. In that case, please bear with me for a time; for not everyone is as knowledgeable as you. Let's consider a few more minor examples before moving on to more exotic constructs.
Are you as weary as I of having to write out LTRIM$(STR$(num))
whenever you wish to print a number without its leading space
which probably is most of the time? A short macro reduces the tedium:
MACRO TrimN(N)= LTRIM$(STR$(N))
'delete the leading space from a number
Now, simply PRINT TrimN(num)
These also can reduce the tedium of programming:
MACRO Line1(N)= STRING$(N,CHR$(196))
'single horizontal line of length N
MACRO Line2(N)= STRING$(N,CHR$(205))
'double horizontal line of length N
MACRO Ucode(N)= UCODE$(CHR$(N))
'get an extended font character
MACRO GetKey= A$=WAITKEY$: ESC=-(A$=$ESC)
'input a keyboard character to A$
MACRO GetUP= A$=UCASE$(WAITKEY$): ESC=-(A$=$ESC)
'convert keyboard input to uppercase
I use GetKey a lot, as will be evidenced in subsequent
articles. Since the <Esc> condition always is checked in my programs,
that flag is set automatically in GetKey and
GetUp. My choices of values for ESC are
(0,+1) for compatibility with other code.
BACKSPACE A LINE OF TEXT
When using the Console Compiler, special considerations are in order regarding user input. Having no mouse or GUI textbox to work with, the programmer must handle console screen activity in other ways. The necessary commands can be lengthy, and repetitive usage can become quite tedious. In that regard, macros save the day as usual.
Suppose that you wish to intercept the BackSpace character (ascii #8) and udpate the screen accordingly. In the olden days, one could echo cursor movements directly:
PRINT CHR$(29); $SPC; CHR$(29);
PowerBASIC will not let you do that, however; so something else is in order:
MACRO BackSpace(A)= LOCATE,CURSORX-1: PRINT $SPC;: LOCATE,CURSORX-1: A=LEFT$(A,LEN(A)-1)
The cursor is moved leftward one column and a space is printed, thereby deleting the
last-entered character from the screen. Then the cursor is moved left again,
and the unwanted character is removed from the accumulated input string. To the user,
the procedure looks like an ordinary Backspace function. Here is a primitive example
of its usage, which also incorporates the Getkey macro:
(get A$; flag ESC var)
IF ESC THEN EXIT
IF A$=$CR THEN EXIT
IF A$=$BS THEN BackSpace(UserInput$): ITERATE
UserInput$ += A$
Of course, one could create an ordinary Function to do the same thing, and that
is a programmer's prerogative. In a quest to streamline code, however, if
something can be accomplished with a
single-line macro rather than a
multiple-line function, then I consider that the more attractive option.
If desired, of course, any multi-statement macro can be broken up to look more
like a standard function or procedure for readability, even if doing so is
MACRO BackSpace(A) LOCATE,CURSORX-1 PRINT $SPC; LOCATE,CURSORX-1 A=LEFT$(A,LEN(A)-1) END MACRO
Here are two offerings that test the validity of an inputted character:
MACRO OKchar(A)= (ASC(A)>=32 AND ASC(A)<=125)
'is standard typewriter character
MACRO ISdigit(A)= (A>="0" AND A<="9")
'is a digit
These guys resemble the old GW-BASIC function definitions of yesteryear, and they are
in-line just as any other function:
IF ISdigit(A$) THEN MyNum$ += A$
'accepts only numeric characters 0-9
IF NOT OKchar(A$) THEN ITERATE
'accepts only printable keyboard characters
Note that the logical NOT function can be used
here, because the macro itself takes on a value of
-1 or 0.
Later on we will build an all-purpose text-input routine and put that macro to good use. For now, here are some other ideas for screen detection and manipulation:
MACRO PriorChar= CHR$(SCREEN(CURSORY,CURSORX-1))
'character left of cursor
MACRO ScrnCHR= CHR$(SCREEN(CURSORY,CURSORX))
'ID of character at caret
MACRO ScrnASC= SCREEN(CURSORY,CURSORX)
'asc value of character at caret
MACRO CharHere= SCREEN(CURSORY,CURSORX)<>32
'character exists at caret
MACRO NoCharHere= SCREEN(CURSORY,CURSORX)=32
'caret position is blank
MACRO EraseLine(N)= PRINT SPACE$(N);: LOCATE,CURSORX-N
'clear space right of cursor
The last entry, EraseLine(), is especially useful for clearing away any existing screen text prior to inputting some more; but it is handy in other contexts as well.
MANAGING PROGRAM FLOW WITH <ESC>
All of my applications are designed such that most processes can be aborted by
pressing the <Esc> key. Anytime a user finds herself unsure
of something, on the wrong screen or menu, or simply has wearied of the present
activity, pressing <Esc> lets the user do just
escape from the current process back to a logical return-point.
When the escape flag is encountered, a number of different things might need to
occur. Depending upon the actual situation, one might wish to quit the program,
exit a menu or subroutine, or perhaps
re-run some loop or other. To that
end, a global Boolean variable ESC is set equal to one (or any other desired
value) whenever an escape condition is established, and the pertinent routines are
designed to alter their behavior accordingly.
Because this facility is used extensively, I utilize a table of macros which
actually are just simple
code-shorteners, but which make program listings
much more readable. Here are most of them:
MACRO EscNOW= ESC=Yes: EXIT SUB
'(global constant Yes& = 1)
MACRO EscEXIT= IF ESC THEN EXIT
MACRO EscSUB= IF ESC THEN EXIT SUB
MACRO EscRET= IF ESC THEN RETURN
MACRO EscITER= IF ESC THEN ITERATE
MACRO EscQUIT= IF ESC GOTO QuitOption
Any of these equivalents can be tacked onto the end of a program line, which also helps to visually document its function.
IMPROVING 'ON ERROR GOTO'
PowerBASIC's built-in ON ERROR GOTO feature is a good
debugging tool; but its usefulness is somewhat limited in that the designated
error-trapping routine is expected to reside in the same subroutine or
function as the calling code. Wouldn't it be nice if there were a clean and
easy way to use ON ERROR GOTO more generically?
In fact, there is! Just install Ted's Tricky Trapper Macro, which employs
a simple batch-file leapfrog technique:
MACRO OnError ON ERROR GOTO Er_Trap GOTO Skip_ET Er_Trap: LOCAL E&: E=ERR: CALL ErrorTrap(FUNCNAME$,E) Skip_ET: END MACRO
Place the following error-processing module with your other utilities:
SUB ErrorTrap(OffendingModuleName$,ER AS LONG) ...your own tricky error-handling stuff goes here... END SUB
Finally, at the tippy-top of every selected subroutine, simply name the macro:
SUB AnyRoutine(parm1,parm2) OnError ...other code... END SUB
When control passes to AnyRoutine(), the call to ErrorTrap() is bypassed automatically, to be accessed later only if an error condition accrues. Variable E is needed because the system variable ERR cannot be passed as a parameter.
ECHOING TEXT TO THE SCREEN
When printing text, especially with frequent color changes, the coding can become quite tedious, especially if long lines of text are involved. A small group of macros can effect a substantially more compact program listing, by combining the LOCATE, COLOR, and PRINT functions into a single command:
MACRO PrC(C)= COLOR C: PRINT
'add "item to print" 'c=fgd color
MACRO PrL(Y,X)= LOCATE Y,X: PRINT
MACRO PrLC(Y,X,C)= LOCATE Y,X: COLOR C: PRINT
'yx= y,x: c=color
Here is a short comparison:
LOCATE Y+1,X+2: COLOR %CYA: PRINT "Welcome to Ted's MACRO WORLD!" LOCATE Y+3,X+4: COLOR %BLU: PRINT "Any"; COLOR %RAD: PRINT " resemblance to intelligent coding "; LOCATE Y+6,X+4: PRINT " is purely coincidental."; vs. PrLC(y+1,x+2,%cya) "Welcome to Ted's MACRO WORLD!" PrL(y+3,x+4,%blu) "Any";: PrC(%rad) " resemblance to intelligent coding"; PrL(y+6,x+4) " is purely coincidental.";
The shortcuts are especially handy when long lines of text are to be printed:
LOCATE Y+1,M+15: COLOR %GRN PRINT "Now is the time for all good men to come to the aid of their party." vs. PrLC(y+1,m+15,%grn) "Now is the time for all good men to come to the aid of their party."
Note the use of %RAD for the color red. When set up as an equate, the more logical spelling conflicts with a PowerBASIC keyword.
PLAYING WAVE FILES
I incorporate several WAV files in my programs, some of which don't match the
ones offered by Windows. These small sound files are combined with the
executable program as
resources — a procedure which is made so easy in
PBCC-6. Irrespective of that, another brand-new
command specifically caters to sound files:
PLAY WAVE "filename.wav" [,descriptors]
One possible descriptor is SYNCH, which has a value of +1 and tells the program to wait for the music to stop before continuing execution. For us, however, that's too much text to be coding on a regular basis; so a few macros are in order:
MACRO Beeep= PLAY WAVE "beep.wav", SYNCH MACRO PlayDing= PLAY WAVE "ding.wav", SYNCH MACRO PlayDone= PLAY WAVE "done.wav", SYNCH MACRO PlayNotify= PLAY WAVE "notify.wav",SYNCH
Assuming that those four WAV files are in the same directory as the executable program, that's all there is to it. Now, just place one of the new commands anywhere in your code:
PlayDone: EXIT SUB
I use another sound-related macro within
it warns of an unacceptable keypress:
MACRO Complain= Beeep: ITERATE
One can easily include those sound files as resources, in which case a minor adjustment is necessary. Example:
#RESOURCE WAVE, Beep2, "beep.wav"
Now the macro must reference the Resource ID, not the file name itself:
MACRO Beeep= PLAY WAVE "beep2", SYNCH
(Just for the record: despite what some confused bloggers suggest because they can't figure out why their stuff doesn't work, letter case is immaterial to resource specification.)
That's it for PB-6 sounds. If you have not yet upgraded, you still can
play WAV files by directly addressing the requisite API call. Despite much
online pulling of hair and gnashing of teeth over how to implement this feature,
it is in fact child's
play — at least in PowerBASIC:
DECLARE FUNCTION PlaySound LIB "winmm.dll" ALIAS "PlaySoundA"_ (WavName AS ASCIIZ, BYVAL Module AS DWORD, BYVAL Func AS DWORD) AS LONG
MACRO Beeep= PlaySound "beep.wav", 0,1+&h40004 MACRO PlayDing= PlaySound "ding.wav", 0,1+&h40004 MACRO PlayDone= PlaySound "done.wav", 0,1+&h40004 MACRO PlayNotify= PlaySound "notify.wav",0,1+&h40004
Despite PB's support for the lazy-programmer option, it is entirely unnecessary
to bloat your executable program by loading most or all of
declaring the pertinent function is sufficient. Also, the Func
1+&h40004 specifies to play a sound resource
asynchronously, meaning that the sounds run concurrently with other activities.
If you wish for the music to conclude before execution is continued (synchronously),
delete the [1+]. For short WAV files such as my examples,
it really doesn't matter.
Finally, if there are multiple music files to play, and creating a dedicated macro for each one would be tedious or inappropriate, then a generic construct could be in order:
MACRO Sound(A)= PlaySound A+".wav",0,&h40005
Now: Sound("chimes") -or- Sound("Happy_Trails_To_You")
PowerBASIC's printer-control facilities are awesome; anything one might need to
do can be accomplished with the
built-in command set. Many of those
commands, unfortunately, have become quite lengthy —
XPRINT SET STRETCHMODE, for example, and of course
I don't much like that. Additionally, an application might need to
micro-manage printer output down to the pixel. To that end I have set
up an elaborate macro scheme, some of which entails a modicum of memory work on the
part of the programmer in order to take full advantage. At least a couple of
them should be considered useful by any coder, however.
This one-word macro sets up output to the Default Printer on standard
8½×11 paper, with ¼-inch text margins, and assigns world
coordinates designated in inches:
MACRO AttachPrinter= XPRINT ATTACH DEFAULT: XPRINT SCALE (0,0)-(8,10.5)
These functions accommodate the positioning of text, also assuming a text width of eight inches:
MACRO SetPos(a,b)= XPRINT SET POS (a,b) MACRO TextSize(a)= XPRINT TEXT SIZE a TO TxtW,TxtH MACRO AlignCtr= (8-TxtW)/2 MACRO AlignRt= (8-TxtW-.01)
It makes no difference as to what font is currently being used. I find that the [-.01] can help to prevent the rightmost pixel from disappearing from a printed page. To center a line of text on a page one inch below the top margin:
A$="HELLO WORLD!" Textsize(a$) SetPos(AlignCtr,1) XPRINT A$
What a nice, clean listing! That's what macros are all about.
Note the usage of one macro inside another, which is perfectly legal. In fact,
the PowerBASIC compiler is so smart that it makes no difference in what order one's
macros are declared or how deeply they might be nested; all is resolved at
compile-time. About the only inviolate rule is that a macro must be
defined higher up in the code than any reference to it.
Lately I have been using a simple approach to multiple print statements, which requires only that the page coordinates always be referenced by variables X and Y:
MACRO PXY= XPRINT SET POS x,y:: XPRINT
X=2: Y=3 PXY "Pack my box with five dozen liquor jugs." ... ... Y=4: PXY "Time flies like an arrow;" Y=5: PXY "fruit flies like bananas."
Life can become even better — or not, depending upon one's penchant for complication. There is virtually no limit to the level of depravity to which a macro programmer might sink if desired.
Great things also can be done with macros and font management; some are detailed on another page.