PowerBASIC Macros |
What is a macro? Actually, it is nothing more than a simple
text-substitution. is created to represent
TEXT-A
wherever it occurs in a program listing. It so
happens, however, that not only does such a seemingly trivial facility 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 TEXT-B
dreams — a subset of commands
and functions that integrate seamlessly with other PowerBASIC
protocols.
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 (r.i.p.) 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, and 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 complex 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 in order:
MACRO Mod5(N)= ((N+4) MOD 5 +1) |
These adjustments accommodate a negative N:
MACRO ModZ(N,M)= (((N MOD M)+M) MOD M)) 'outputs 1,2,3...0-or- MACRO ModZ(N,M)= (((N MOD M)+M-1) MOD M +1) 'outputs 1,2,3...N |
So the all-purpose ModZ() might as well be used all the time, I suppose.
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 process:
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
by the 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.
TRIMMING SPACES
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 character — 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 line, length NMACRO Line2(N)= STRING$(N,CHR$(205)) 'double line, length NMACRO Ucode(N)= UCODE$(CHR$(N)) 'get extended font charMACRO GetKey= A$=WAITKEY$: ESC=-(A$=$ESC) 'input keyboard char to A$MACRO GetUP= A$=UCASE$(WAITKEY$): ESC=-(A$=$ESC) 'convert kbd 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 update 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:
DO: GetKey '(get A$; flag ESC var)IF ESC THEN EXIT IF A$=$CR THEN EXIT IF A$=$BS THEN BackSpace(UserInput$): ITERATE ..more code.. UserInput$ += A$ LOOP |
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
unnecessary:
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 characterMACRO ISdigit(A)= (A>="0" AND A<="9") 'is a digit |
These guys resemble the old GW-BASIC function definitions of yesteryear, and they are
utilized in-line just as any other function:
IF ISdigit(A$) THEN MyNum$ += A$ 'accepts only numeric characters 0-9IF 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 cursorMACRO ScrnCHR= CHR$(SCREEN(CURSORY,CURSORX)) 'ID of character at caretMACRO ScrnASC= SCREEN(CURSORY,CURSORX) 'asc value of character at caretMACRO CharHere= SCREEN(CURSORY,CURSORX)<>32 'character exists at caretMACRO NoCharHere= SCREEN(CURSORY,CURSORX)=32 'caret position is blankMACRO 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 that —
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 run 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 colorMACRO PrL(Y,X)= LOCATE Y,X: PRINT 'yx= y,xMACRO 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 optional 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 user-input routines;
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 WIN32API.INC
;
declaring the pertinent function is sufficient. Also, the Func
parameter 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") |
PRINTING
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 page 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 |
Now:
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.