Petey PowerBASIC
Macros

What is a macro?  Actually, it is nothing more than a simple text-substitution.  TEXT-A is created to represent TEXT-B 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 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 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 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 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 utilized 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 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 color MACRO PrL(Y,X)= LOCATE Y,X: PRINT 'yx= y,x 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 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.

PowerBASIC Menu