HOME - - - - - Delphi Tutorials TOC - - - - - - - - - - - - Other material for programmers
Delicious.Com  Bookmark this on Delicious     StumbleUpon.Com Recommend to StumbleUpon

Delphi: TScrollbar, GetTickCount, TabOrder, TabStop, Interlocking enables

This has good information, and a search button at the bottom of the page

Please don't dismiss it because it isn't full of graphics, scripts, cookies, etc!

Click here if you want to know more about the source and format of these pages.
The most generally useful apart of this tutorial will be the material on the TScrollBar component. Don't assume that scrollbars are only the ones we know and love on the edges of windows. They can also be place in the main part of the window, where they are often called slider controls. I would guess that almost everyone as used one to control the volume of audio output from the computer, for instance.

While the job the program does will be of little interest to most readers, I think some techniques in the program have general applicability.

If you are an intermediate Delphi programmer, I'm afraid you're going to have to wade through some material expressed in detail for beginners. Sorry about that!

Another caveat for you: This was written back in the days when the "port" command worked for directly setting the values on a parallel port pin. The sourcecode would not compile when I tried it in December 2009 on an XP system, using Delphi 4. But you could get around that. The "port" issue is not central to the useful stuff in this tutorial. See my parallel port page, in particular the stuff about InpOut32, if you came here for the "port" stuff.

Before starting into the "meat" of the program, I will deal with GetTickCount, which is used in the program. It was not written up in the Delphi 1 help files, nor was it in the default help index for Delphi 4 (but you can get information on it, Delphi 4, by putting your insertion point on it, and pressing F1). It does work, at least under Delphi 1 and Delphi 4.

And... digressing from the digression... those of you using Lazarus will find that "GetTickCount" won't be available to you automatically. I believe... but haven't yet tested... that if you add LCLIntF... or does it have to be LclIntf?... to your code's "uses" clause, then GetTickCount will become available. But the GetTickCount in that may be of a different data type than the Windows GetTickCount, which will (merely?) change when you get the overflow problem. If you want to know more, deal with these things more robustly, study the content in http://forum.lazarus.freepascal.org/index.php?topic=6099.0, even if the "read a book" comments are, to me, a little... umm... "unnecessary"? (Although I DO agree that some "book study" will help any novice make more progress sooner, as opposed to the 100% "learn by doing" that many of us use. It is just that you don't find the right book easily. In an attempt to answer the really new programmer's needs, I created my Delphi COURSE, which should also help Lazarus newbies.

Back to the next higher level... "What is GetTickCount?"....

If you say liTmp:=GetTickCount, you will get a number. (liTmp should have been declared as being of type longint (long integer).) The number changes rapidly, but at a fairly constant speed in all computers. I've used this to cause a short delay with
   liTmp:=GetTickCount;
   repeat
   until GetTickCount>liTmp;
 

Ordinarily, a loop like this would spell disaster... Windows doesn't respond well if a program takes over the machine. Windows has to get a chance to do all of it's multi-tasking bits and pieces. If you do have a loop in your program, it is wise to include
application.processmessages
within that loop. However, in this case a) the loop will not seize control for very long, and b) I had problems when I put application.processmessages inside the loop.

(Since writing the above, I've seen posts which say that using application.processmessages is Not A Very Good Idea. Probably not... in a perfect world. I have to admit, though, that I HAVE used them, carefully and sparingly for many years. Mostly seem to work, the way I use them. Best to avoid situations where you "need" them, but it is a rule that can be "broken"... carefully. The "Windows doesn't respond well if a program takes over the machine" rule certainly trumps the "don't use processmessages" rule.)

One point to bear in mind when using GetTickCount: It will, from time to time, give you grief... I'll explain why in a moment. However... the grief I'm talking about arises only rarely. (Not good enough, I hear you say... and for some programs I'd agree... but not all.)

The problem is one of overflow. If you understand that, you can skip down to the story continues. For those who don't understand, I'm going to repeat myself without the technobabble.

I know a boy who has two brothers. I can indicate how may children there are a number of ways with a keyboard: There's "3", of course... don't dismiss that as too obvious, or you're going to miss the point I'm making. There's also "three", "tres" (if you speak Spanish). I show how many brothers there are with: "X-X-X". So... the idea of three is simple, but how we show it is really fairly independent of the idea.

Inside the computer, we often "write down" numbers, of course. And there are many ways to do that. (That could be the subject of a tutorial in itself, but for now I'll confine myself to one method!) The number we get from GetTickCount is held inside the computer as a FIXED number of ones and zeros. If you know binary, you know that the number of brothers in my example (3, as we usually write it), is "11" in binary. However, if that number were stored in a longint type variable, it would be more accurate to say that it is...
00000000000000000000000000000011
Remember I said that longint numbers occupy a FIXED number of ones and zeros? Computers are not as clever as humans. We know that 0003 is the same as 3, and rarely bother to write out leading zeros.

(A little aside: Don't assume that one system's "Long Int" is the same as another system's. Delphi 1 uses 32 1s and 0s for a longint type number. If you work with the datasheets for the Dallas MicroLan / 1-wire devices, you'll find that one of their integer types is of a different size from Delphi's idea. There's even a nasty niggle at the back of my mind saying that between versions of Delphi the definition of some of the integer types changed.)

(While I'm off of my topic anyway: Don't let my use of the word "fixed" above confuse you. There's a major way of writing numbers called "fixed point notation". What we're talking about here isn't that.)

Hang in there.. we're getting to the point. Before that, consider the odometer in a car... the thing that says how many miles it has traveled. I have a very old car with only 5 wheels. It says I have driven 23,000 miles. Actually, I've traveled 123,000 miles- that's an example of an overflow problem.... there just isn't a wheel for hundreds of thousands of miles. After it read 99,999 it went back to reading 00,000, even though it should have said 100,000.

Overflow problems with longint values are similar. Eventually, the number gets so big that the next number can't be shown properly. At that point you either get a "range check error", if you have {$R+} in your program (more on this in a moment) or your program gets away with the problem, or you see strange behavior, even a crash, perhaps. (Similar things happen with other number representation systems, longint isn't unique in this. (The famous "Millenium Bug", which caused problems in some computer systems, and caused a great deal of hard work to reprogram systems so they would not have the problem, was, purely and simply, an "overflow" problem. Many computers stored the year in dates as two digits, so 81, 82, 83... worked fine for 1981, 1982, 1983... but when we got to 98,99... the system didn't "know" that "00" was 2000, the year AFTER 1999... not 1900, the year 99 years BEFORE "99")

Just to be thorough (ignore this paragraph if you like): Delphi's longint is a signed type.... the 32 bits are not all used for simple "binary" information. The left hand bit, if 0, says "treat the rest as a simple binary number" (e.g. "000...0011" would mean "three") However, if that left hand bit is a 1, the whole number is a negative number.

IN ANY CASE....

If you try to use GetTickCount to implement a small delay in the execution of your code, it will probably "mostly" work... but for a 100% robust solution you have to provide for the day that you make a reading of GetTickCount when the value has become so high... it took about 25 days of uninterrupted program execution, once upon a time... become so high that it is about to "roll over", rather like "98, 99, 00..." in the days of years-as-two-digits.

The problems are not insuperable, and one day I will get around to addressing them. In the meantime, as I don't use the program for 25 uninterrupted days "it will do". Well... would have done. In the end I didn't use a GetTcikCount based version! (Sorry to have dragged you through all of that if the academic interest didn't adequately repay your time!

There are scraps in the program of my second idea: an attempt to provide the small delay I needed without using GetTickCount. I did things that "should" have worked, but they didn't, and I couldn't find my errors. This failed and mostly remmed out code is the only reason for the timer and for boTickTock.

It was a while ago that I wrote the code behind this tutorial. It worked... I'm pretty sure... but I'm not going to dig into this detail today while "polishing" this tutorial.

The {$R+} issue: This is a compiler directive I recommend that you put into your sourcecode. I usually put it just after the automatically generated {$R *.DFM} which appears just after "implementation". N.B.: the {$R+} turns on range checking which is a good idea, with small penalties. The {$R *.DFM} is unconnected to the range checking issue. If you take the {$R *.DFM} out, you program probably won't compile.



The Story Continues... The program was written to drive some electronics plugged into the computer's parallel port. Those electronics could turn on or off 4 banks, each of eight, LEDs. The program has 4 scrollbars. If they are all fully down, all 32 LEDs will be off. If they are all fully up, all 32 LEDs will be fully on.

I don't know if the program actually works! But I know that all the major parts are working correctly, and because of the way the program was written, if it doesn't work, the problems will be in one small part of the program and easily overcome. The inspiration for the program was an article in Everyday with Practical Electronics, a magazine published in the UK for electronics hobbyists. They had a similar program in the article written in some nasty other language, hence my urge to port it to lovely Delphi. The article was on page 738 of the October 2001 issue. If anyone would loan me the hardware to finish testing the software, I'd be grateful! (Click here for EPE's website.)

Another brief(?) aside. This one covers the hardware. You can skip to next part of main story if you like. The "other electronics" is a single chip, the UCN5818AF from Allegro Microsystems. (Available in the UK from the usual RS outlets. I don't know how to get it in the US, sorry.)

Pins 1 and 40 are connected to 5v. Pins 19 and 20 are connected to 0v. Put a 100n cap between 5v and 0v. Connect the following to your printer port:
UCN5... pin number   LPT1 name  Pin on 25-way D
           39          D0             2
           22          Strobe         1
           21          ALF           14

Connect the LPT ground to our external circuit's ground. Any of pins 18-25 on the D-25 should be suitable.

PLEASE NOTE: You CAN damage your computer if you make ill-advised connections to it. Any use you make of anything you find here must be AT YOUR OWN RISK



For information on making things to connect to your parallel port, you may want to visit my page on that topic. From there, you will find links to electronics tutorials. The parallel port page is the most popular of all my pages.



So! The program uses four sliders to turn 32 bits of output from an external chip on or off. Each slider controls 8 of the output bits. When the slider is fully down, the message to those 8 bits is "0"; when it is fully up, the message is "256", which in binary just happens to be 11111111. The numbers between 0 and 256 cover all possible combinations of 1s and 0s in the 8 digit binary equivalent of the number.

I didn't try to be "clever" in my program.

We can ignore most of what's in the FormCreate handler, but just going through that:

laTitle3.caption:=laTitle3.captin+ver+');
takes what I put in the laTitle3 label at design time, which was "Delphi version by TKBoyd (version", adds in whatever's in the program's "ver" constant (I'll come back to that), and finishes the label off with a ")" to balance the one before "version".

In almost all of my programs, I declare a constant called "ver" (For "version") I make backups of work from time to time, I have multiple copies in use, and distributed via my shareware enterprise. Whenever I know that major changes have taken place, or that an older version has been "left behind" (e.g. a backup has been done), I change the version ID. A date usually suffices, but if on a given day I have several versions, I just add "a", "b", "c"... to the date. Many a time this practice has saved me agonizing over which copy of something is the most advanced. It also helps when customers come to me with questions about the copy they are using.

Now we come to the second line of FormCreate:
boHWEnabled:=false;


boHWEnabled is a boolean variable I declared to keep track of whether the user wants any messages passed to the hardware the program was written to drive. ("boOOLEAN hARDwARE enabled") Until I can borrow that hardware, there's no point in me literally unplugging my printer while I do the programming, when I can "unplug" the parts of the program that will (eventually) send messages to LPT1. Note that I set the program up so that it defaults to NOT sending things to LPT1... the user must do something first... a partial protection against accidents.

Connected with this initialization of boHWEnabled is the design-time specified caption on buPortOnOff. When the program starts, this says "Turn Port On". Digressing from FormCreate for a moment: the buPortOnOff handler is as follows:
procedure Tepe01af1.buPortOnOffClick(Sender: TObject);
begin
if boHWEnabled then begin
laPortOnOff.caption:='enable';
buPortOnOff.caption:='Turn Port On';
boHWEnabled:=false;
end (*no ; here*)
else begin
laPortOnOff.caption:='disable';
buPortOnOff.caption:='Turn Port Off';
boHWEnabled:=true;
end;(*else*)
end;


laPortOnOff is just one line of a longer message above buPortOnOff saying "Click button below to (enable/disable) messages to parallel port." (Note how good choice over object names can make working on a project easier.)

The buPortOnOff handling doesn't exactly turn the port on or off. Rather, it sets boHWEnabled true or false. Then, anywhere in the program that something might be sent to the parallel port, there's an "if boHWEnabled..." test first. It may seem complicated, but doing it this way keeps things more clear in the long run.

The two lines in FormCreate relating to the timer are irrelevant, unless you can overcome the problems I had with boTickTock and it's intended role.

What remains breaks into two halves. Parts of the program are concerned with where the sliders are, and the other parts deal with passing that information to the hardware. Cutting through the details, we come to the event handler for ScrollBarChange. Every time the "thingie" on the scrollbar moves, for whatever reason, the ScrollBarChange event is generated. And within that we find
if boHWEnabled then Send4BytesToExternalDevice;


We'll come back to that. First a little comment to forestall confusion: There are four ScrollBarChange event handlers: ScrollBar1Change, ScrollBar2Change, Scroll3BarChange, and ScrollBar4Change. This is pretty inelegant programming, but with intelligent use of cut and paste, it sure gets the job done quickly. Note: The Send4BytesToExternalDevice which is part of each of the scrollbar's Change handlers is the same Send4BytesToExternalDevice, and it sends the values of all four scrollbars each time, even though (typically) only one scrollbar changes at a time. This wasn't done out of laziness, but rather because the hardware the program drives has no provision for accepting new values for sub-sets of the 32 bits of output it provides.

While we're looking at ScrollBarChange let me explain
bSB1Inverted:=255-ScrollBar1.position;
When the scrollbar is vertical on the screen, if the "thingie" is at the top, ScrollBar1.position returns zero. I wanted "up" to equate to "high". The scrollbars were configured so that the highest value they return is 255. Thus, 255-ScrollBar1.position returns 0 if ScrollBar1.position is at its maximum, and 255 when it is at zero. Obviously (?) Send4BytesToExternalDevice uses what is in bSB1Inverted, bSB2Inverted, bSB3Inverted and bSB4Inverted.

Certain properties of the scrollbars deserve special mention. These properties were set as indicated at design time.

Setting Kind to sbVertical oriented the scrollbar the way I wanted it.
The setting in LargeChange (16) determines how much the scrollbar moves if you click on the bar, but not on the "thingie". SmallChange is set to 1 for this program, but you might have circumstances when you wanted a larger change. Even if SmallChange is set to, say, 10 you can get intermediate values by dragging the "thingie".
Max and Min determine the highest and lowest values returned by Position. Setting Position to 255 meant that the "thingie" would start at the bottom of the scrollbar.
The values set up for the TabOrder and TabStop properties of various objects on the form are not much affected by the fact that some of the them are scrollbars. Some users never use the tab key, but many do, and in some programs it is very useful. If you don't use the tab key, you will usually have to use your mouse to get from object to object in the Window. The way EPE01a was written, you can control the 32 bits without using a mouse. The program starts with the first scrollbar selected, because its TabOrder has been set to 0, and its TabStop is set to true. If you want to change the setting of the scrollbar without using the mouse, the "arrow keys" will do... this is normal for any scrollbar, nothing to do with TabOrder/Stop. The other three scrollbars have their TabOrder properties set to 2,3 and 4; all have their TabStop property set to "true". Everything else on the form with a TabStop property has it set to "false". Because of his, if you repeatedly press the tab key, you will move from scrollbar to scrollbar, going back to scrollbar1 after visiting scrollbar4. Shift-tab moves you through the scrollbars, but in reverse order. Just normal Windows behavior.) So now you know about TabOrder and TabStop!

That nearly finishes what you need to know. I'm now going to examine Send4BytesToExternalDevice in some detail, which will cover some ground that beginners may appreciate. Don't worry too much about what we're doing to the hardware. Concentrate on the structures used. Before I start, I will again confess to being lazy... the code is "inefficient" in that there are a lot of almost identical lines. You can have a lot of fun devising more elegant solutions, but this one worked and has few "clevernesses" in it to confuse a beginner.

The lost important thing to notice is that the main program only "knows" about Send4BytesToExternalDevice. If you look near the top of the listing, you find...
type
  Tepe01af1 = class(TForm)
    laTitle: TLabel;
    laTitle2: TLabel;
    laTitle3: TLabel;
    laTitle4: TLabel;
    ScrollBar1: TScrollBar;
    ScrollBar2: TScrollBar;
    ScrollBar3: TScrollBar;
    ScrollBar4: TScrollBar;
    buQuit: TButton;
    laSB1: TLabel;
    laSB2: TLabel;
    laSB3: TLabel;
    laSB4: TLabel;
    buPortOnOff: TButton;
    Label1: TLabel;
    Label2: TLabel;
    laPortOnOff: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Timer1: TTimer;
    Label3: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure buQuitClick(Sender: TObject);
    procedure ScrollBar1Change(Sender: TObject);
    procedure buPortOnOffClick(Sender: TObject);
    procedure ScrollBar2Change(Sender: TObject);
    procedure ScrollBar3Change(Sender: TObject);
    procedure ScrollBar4Change(Sender: TObject);
    procedure Timer1Timer(Sender: TObject);
  private
    { Private declarations }
    bSB1Inverted,bSB2Inverted,bSB3Inverted,bSB4Inverted:byte;
    (*The scrollbars consider UP to be 0, which is non-
    intuitive, so at a low level, the sb.position is moved
    to a variable which works the other way around.*)
    boTickTock,boHWEnabled:boolean;
    liTmp:longint;
    procedure Send4BytesToExternalDevice;
  public
    { Public declarations }
  end;

Everything in that above "private" was generated for me by Delphi. I wrote the stuff in the "private" section. (You don't need to worry about the use of the "public" section for quite a while.) When I wrote...
procedure Send4BytesToExternalDevice;

... I immediately used copy/paste (edit) to put the following into the program, below the word "implementation":
procedure Tepe01af1.Send4BytesToExternalDevice;
begin
end;

... and I refrained from compiling or running the project until I'd put something between the "begin" and "end". (If you run an empty procedure, Delphi will "helpfully" edit it out of your program for you! Putting even something as simple as {x} between the "begin" and "end" will prevent this.)

As you've been going through the program, you've probably noticed the procedures Delay, PRn1, Prn3, and DoByte1,2,3 and 4. But a few lines ago I implied that the program didn't "know" about them! This is because they are buried within Send4BytesToExternalDevice. There are two benefits to this hiding:
a Things done by the hidden parts are less likely to upset other parts of the program. (They can, especially if you do Bad Things with variables, but once you learn to avoid that, you're pretty safe... and...
b) You'll remain clear about the fact that the hidden parts are not needed by anything other that Send4BytesToExternalDevice. This becomes a real benefit when you start using parts of old programs when constructing new ones.

So! How is the "hiding" accomplished?

The following isn't as lengthy as Send4BytesToExternalDevice, but it illustrates what is going on....
procedure Tepe01af1.ADemo;
   procedure appendC;
     begin
     sTmp:=sTmp+'C';
     end;
   procedure appendO;
     begin
     sTmp:=sTmp:'O';
     end;
   procedure appendL;
     begin
     sTmp:=sTmp+'L':
     end;
 begin (*ADemo...*)
   sTmp:='';
   appendC;
   appendO;
   appendO;
   appendL;
   showmessage(sTmp);
 end;
The procedure assumes that there is a declared string variable called sTmp. My use of it here has been perilously close to the Bad Things I mentioned earlier, but forget that, if you realized. What is going on in the above?

First of all, you have to learn to "read" the text... which is laid out in a not- especially- human- friendly order.
procedure Tepe01af1.ADemo;

...marks the beginning of the programmer's indications of what should happen whenever ADemo is invoked anywhere in the program. Following that are some details, which the informed reader skips over at first, jumping down to...
 begin (*ADemo...*)
   sTmp:='';
   appendC;
   appendO;
   appendO;
   appendL;
   showmessage(sTmp);
 end;

(The (*ADemo) rem following the "begin" is optional, but always a good idea if the begin is at all far from the start of the declaration

ADemo builds a word ("cool") in sTmp, and then displays it. Of course we could have simply said "Showmessage('cool');", but I wanted to show you this structure where procedures are defined within procedures.

While ADemo was doing its thing, it was required to use "appendC", "appendO", "appendL". These procedures are not parts of Delphi.They were created, within ADemo, by putting..
   procedure appendC;
     begin
     sTmp:=sTmp+'C';
     end;
(and the other two, similar, procedure declarations) into ADemo where we did. As such, those procedures are available to ADemo, but unknown to the rest of the program. The rules for what you can put between "procedure" and "end" remain exactly what they were before. (These are remarkably simple procedures). You can repeat the process, and have sub- sub- procedures, e.g. some procedure that is defined within appendC, which is hidden from even ADemo. (I'm not sure I've ever felt the need, but you may do it.) You can also put function declarations within something like ADemo. (I've done this many times.) You can declare constants and variables within something like ADemo (and often should, and in one case must)... you just use "const" and "var" as you would in the main program. Here's more complex example, showing some of these things.... (sTmp:=sTmp+chr(65) is like saying sTmp:=sTmp+'A') The example also uses parameter passing. (This is an important concept I don't seem to have written up anywhere. I'm not talking (here) about command line parameters.)
procedure Tepe01af1.Demo2;
const kA=65;
var sTmp:string;
    c1:byte;
  procedure append(b:byte);
    begin
      sTmp:=sTmp+chr(kA+b);
    end;
begin
sTmp:='';
for c1:=0 to 5 do
  append(c1);
showmessage(sTmp);
end;
This program will display ABCDEF.

It illustrates the time when you must use a "var" inside a procedure declaration: the control variable for a "for" loop (c1, here) must be declared thus. It is called a "local" variable because it is "inside" Demo2, and not "known" to the rest of the program. sTmp, obviously (I hope) is also local... you can even have ANOTHER "sTmp" in the "outside world. You could have
sTmp:='fred';
Demo2;
showmessage sTmp;

there, and you'd first get "ABCDEF" and then get "fred".... which, if you think about it, is a little odd. The "outside" sTmp wasn't messed up by the local sTmp while the program was doing Demo2! This isolating of various parts of your program can be a big help in wring programs that do what you meant them to do!

So... I hope the structure of Send4BytesToExternalDevice is more clear? Now to analyze what it is doing.

To "work" the external hardware, we need to send 1s and 0s according to the numbers in bSBInverted. Prn1 is central to Sent4BytesToExternalDevice. If you say "Prn1(0)", you send a 0 (if boHWEnabled is true), and if you say "Prn1(1)", you send a 1.

Before I go on: a "byte" is an eight digit binary number.

The DoByte1 sub-procedure looks at a part of bSB1Inverted, which, you may recall, is a number that is determined by the position of the "thingie" in the first scrollbar. If you call DoByte1(128), you look at the left hand bit (1 or 0) in the byte. DoByte1(64) looks at the next bit, and so on.

During DoByte1 (and 2 and 3 and 4), as each bit is examined, Prn1(0) or Prn(1) is executed, as necessary. The Prn1(0 or 1) establishes a 0 or 1 on a line "feeding" the external hardware. The Prn3(1);Delay(2);Prn(3); says, via a second line, "Hey! Take note of what's on the data line at the moment."

After all 32 bits have been sent, the little
Prn3(2);
Delay(2);
Prn3(3);
at the end of Send4BytesToExternalDevice sets it so that the 1s and 0s you sent remain on the external hardware's outputs until the next time you tell it to change what is showing.

One last little point: As it stands, I think this program will only compile with Delhi version 1, because of the lines port[888]:=bTmp; and port[890]:=bTmp. There are ways around this problem if you want to compile the program with more advanced versions of Delphi. Also, I'm 99% sure it won't work in versions of Windows beyond Windows 98, certainly not in Windows NT. This is due to those operating systems protecting the printer port from the sort of direct access used here. I THINK the program will run okay on Windows 95 and 98 machines, which is why I include it in the zip file, for those of you (grin) not still working with Delphi 1 on a Win 3.1 machine. See my page about using the parallel port for more information on this issue of port[888], etc.

I'm out of time! Hope I didn't leave any loose ends?
Click here to download zip file with sourcecode.

            powered by FreeFind
  Site search Web search
Site Map    What's New    Search This search merely looks for the words you enter. It won't answer "Where can I download InpOut32?"
The search engine is not intelligent. It merely seeks the words you specify. It will not do anything sensible with "What does the 'could not compile' error mean?" It will just return references to pages with "what", "does", "could", "not".... etc.

Also: This search only searches the material on one of my websites.

      Click here to visit another of my sites.

      Click here to visit my third site.




Click here if you're feeling kind! (Promotes my site via "Top100Borland")
Ad from page's editor: Yes.. I do enjoy compiling these things for you. I hope they are helpful. However... this doesn't pay my bills!!! Sheepdog Software (tm) is supposed to help do that, so if you found this stuff useful, (and you run a Windows or MS-DOS PC) please visit my freeware and shareware page, download something, and circulate it for me? Links on your page to this page would also be appreciated!
Click here to visit editor's freeware, shareware page.

Link to Tutorials main page
Here is how you can contact this page's author, Tom Boyd.


Valid HTML 4.01 Transitional Page WILL BE tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org. Mostly passes. There were two "unknown attributes" in Google+ button code. Sigh.


If this page causes a script to run, why? Because of things like Google panels, and the code for the search button. Why do I mention scripts? Be sure you know all you need to about spyware.

....... P a g e . . . E n d s .....