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

Delphi and Lazarus: Working with text files via memos
Quotes Machine as an example

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.

This page is "browser friendly". Make your browser window as wide as you want it. The text will flow nicely for you. It is easier to read in a narrow window. With most browsers, pressing plus, minus or zero while the control key (ctrl) is held down will change the texts size. (Enlarge, reduce, restore to default, respectively.) (This is more fully explained, and there's another tip, at my Power Browsing page.)


You may download the Delphi sourcecode for this in a zip file, and the .exe is there.Even Lazarus readers would be advised to fetch the zip file, for the sample properly formatted text file of quotations which is included.

Written using Delphi 4. No "special Delphi 4 tricks" in it, should work with other Delphis, but "guaranteed" to work with Delphi 4! Tutorial also tested using Lazarus, September 2011, using Lazarus version 0.9.30

This tutorial will show you some techniques for dealing with text in a text file. The application we will develop will fetch quotes from a collection of quotes. It will be called "the Quote Machine". For the tutorial, what you see here will stand alone, but it was written with the intention of "plugging it in" to a larger program, and guidance on doing that is given at the end. Writing things this way... as stand alone small applications first... will help you build large things well. Not only will it enforce important disciplines, but testing of elements of a big project in isolation proceeds more quickly than testing them as a part of a whole. Of course, just because they worked in isolation doesn't mean that you are done when you "plug them in" to the larger application... especially if you don't do it well... but you can get rid of some of the problems while the module is isolated!

I was delighted to find that, apart from two mysteries, potentially serious, but probably not, everything "just worked" in Lazarus. I had very little editing to do to make this tutorial useful to both Delphi and Lazarus oriented readers. The mysteries are presented further down the page.

Not only does this tutorial show you the things that are specific to the task of presenting quotes from collection of quotes in a text file, but it also will give you advice on how to go about programming in general, and give you some Delphi/ Lazarus specific general tips.

Forgive me a little digression? Writing this was a bit of a "learning experience" for me, too.

I anticipated that the original Delphi application and tutorial would take me part of a morning, but if I were to write up a tutorial alongside doing the programming, then perhaps the working hours of a day might be consumed. Wrong. And the really interesting thing was where the time went.

The heart of the program took about a half hour. It was getting to writing that code which took the time.

First I did a good job of describing the format of the data file the quotes come from. This is always a good idea, and paid dividends all through the project's progress... but it is very tempting to skimp on this stage. Had I done so, writing the heart of the program would have taken much longer. It would have taken longer because I would have had to backtrack, undo bits that were connected with false starts, etc. Part of the "etc" is that when you are backtracking, re-writing, you get confused... and then you need to fix the things which arise by mistake when you are fixing the things you thought you were "fixing".

Having done the good description of what form the datafile was going to take, I wrote the code to move the datafile to a memo inside my application. That's really easy. But it wasn't always so. (If you haven't used memos for "scratch" copies of text you want to access, congratulations on your restraint... I think I know why you've eschewed them, as I did for so long... but go ahead, be tempted, fall... they are so easy to work with.)

Once the raw datafile was in the memo, I then started writing the code to transform it from the "human-friendly", "sloppy" condition allowed by the datafile spec into the "computer-friendly", "strict" format I'd decided was best suited to my needs.

That took hours. Took me completely by surprise. It lead to the bulk of the text below. Most of the "massage the memo" stuff is pretty mundane... but it is necessary, and it took me a while to work through creating it.

However, there's a lesson even in that. Because I proceeded in a methodical, step- by- step manner, I was never in any difficulty. It wasn't "hard" (i.e. there weren't times when I was likely to make a mistake which could lie hidden deep in the code, and "pop out" later. It was a long task, but not a difficult one.

When you are working through that part of the tutorial, skim if you wish. The "secrets" and "good bits" are mostly elsewhere.... as long as you remember the "secret" that says: Work methodically. Develop your code in small bits, and test each bit as you put it in place.

Another secret lies in the art of planning your route from "Nothing" to "Finished Product" so that you CAN do it bit by bit, testing as you go along. Sadly, this is an art, which you should try to master by trying, and trying and trying again. How to become a master of that art is not something that I can just tell you.

A general guideline that will help you achieve bit- by- bit development is to create early the window that your application will present to users, with most of its buttons, edit boxes, labels, etc already present. They may not do much yet, but get them on the form. Gradually build up the event handlers the application needs. Sometimes you will create bits of code to "fake" simple "answers" from things which will later be given more complicated code which will produce fancier answers. You will see that approach taken in the story below.

That's the guideline. Actually doing what it suggests takes a bit of practice!

Going back to the application this tutorial revolves around: Especially in the early stages of the work of the tutorial, keep in mind that in this case, the hardest bit is at the beginning, then there's a quite hard bit in the middle, but the final part is easy- peasy!



As I said above: In the course of this tutorial, we will create a simple little application, which might seem trivial. However, the way it is written makes it easy to "insert" it in a bigger application. In the bigger application, what it does might be useful. I have a quotes server in my weather monitoring software, for instance.

The user of the simple demo application we are making in the tutorial will see....

A form with a button and two labels. When users click the button, they will be rewarded with a quote in one label, and the source of the quote in the other.

The quotes (and their sources) will come from a simple text datafile. They will appear, in response to button clicks, in a random sequence.

If you just want to know in five paragraphs "How do I do it?", you've come to the wrong place. If you want to become more skilled in the craft of creating applications, you are in the right place. Here's an example...

I'm now going to digress, and tell you a bit about the creation of what you are reading, and, "underneath" that, the creation of the application it is about.

I wrote a Quote Machine years ago. It was working, was what I wanted, etc. And then I lost the source code. When I sat down an hour ago to re-write it, I finished about 20 lines of this web page telling you how you could write a Quote Machine, and then went off on a tangent. A very necessary and helpful tangent. It is all about planning where you are going, what you are going to do. Sadly, there's a Catch-22 in operation here. You won't be very good at planning programming journeys until you have made many of them. And you won't make them very well until you have learned about the planning of them....

So what planning did I do in the 55 minutes after creating the first 20 lines of this tutorial? I wrote the following. I wrote it in the file, called DD92Quotes.txt, which will be the source of the quotes that the Quote Machine will present.

File format....      Notes version 19 May 11

1)
Header lines... whatever you want, as many as you want, as long as none STARTS <!--q-->

2)
As many QUOTE RECORDS as you want.
File should end after last quote record ends.

Quote Record:

Each quote record consists of....

1: A line starting <!--q-->

(1a:) An optional line feed

2: A single line with the quote. (It may be a continuation
of the <!--q--> line, or a new line, decided by whether
you chose to include the optional line feed.) The term
"line" here is used in the sense that the computer means
it. The "line" may be displayed across several SCREEN
"lines", due to wordwrap.

3: A line starting <!--s-->

(3a:) An optional line feed

4: A single line with the source of the quote.

THE LAST QUOTE RECORD MUST HAVE both a "q" section AND
an "s" section. Other quote records do not need elements
3 and 4. (The application will check that this condition has
been met, and give you an error message if it isn't.)

Within the quote records, any spaces at the start of
a line will be stripped out.

The file may contain blank lines. They will be ignored,
apart from being stripped out when the file is loaded into
a quote serving application, to accelerate processing.
The <!--q--> and <!--s--> tags are also altered for the
same reason. They become qq and qs at the start of lines.
As a consequence, no quote or source may start "qq" or "qs".
(The application checks that this is so, and will throw an
error message at you if you slip up.)

The file may have lines beginning with a semicolon (;). They
will be ignored, apart from being stripped out when the
file of quotes is loaded into the Quote Machine. (They are
provided to allow file authors to include comments within
the file.)

The <!--q--> and <!--s--> tags must be at the start of lines.
They may be on their own lines, or start the line they flag.
Again, the extra line feeds are stripped out when the quotes
file is loaded, so they won't "cost" processing time, if you
want to insert them for clarity.

(See also ";Last quote record MUST ..." at end of file. (Note
from this line that semicolon MAY appear within a line
without meaning anything special. It is only at the START of
a line that it means "This line is merely a comment.")

Apart from the few restrictions mentioned above, a line may
hold any printable character. Thus HTML tags MAY be included
in the file. The Quote Machine won't do anything with them,
but when the same code is used to "feed" quotes to a larger
program, THAT application MAY be sending quotes to web pages, in
which case the HTML will, eventually, be processed.


;This is last line of header. Rest of file is quote records...
<!--q-->
"It is not power that corrupts but fear. Fear of losing power corrupts those who wield it and fear of the scourge of power corrupts those who are subject to it."
<!--s-->
Aung San Suu Kyi
<!--q-->
"In small proportions we just beauties see, and in short measures life may perfect be."
<!--s-->
Ben Jonson

Now... that's a grand plan. Can I do it? Can I stick to it? We'll see. I'll try not to "cheat" too much, editing the above as my mistakes in planning emerge, during my attempt to write the application. Notice that early in the file, I've included a "version" indicator. I could use the common practice of calling things "version 1.0.0", "version 1.0.2", "version 1.1.0", "version 2.0.0", etc.. .but I just keep losing track of where I got to in my numbering. So instead, I simply use the date of revision to distinguish between versions. If I change the file marked "version 18 May 2011" AGAIN on the 18th, I just call the new version "version 18 May 2011b".

Not only do I have to make a good plan... or have many hassles, as I go back and re-do things to accommodate changes to the plan as the project proceeds.... but, also, it is essential to have good records, good documentation of the plan. Whole books have been written on the subject of documentation. You have to write several sorts: Some is for users, some is for programmers working on the application's code. Sometimes that documentation goes within the source code, sometimes in .hlp files, sometimes (as in the example above) in header material or comments within data files. You can't have too much documentation... unless you let it become disorganized. It is also best not to document a given fact in more places than you can help. Take the fact given in the datafile header above: "A line which starts with a semicolon is ignored". You probably will need to put that in more than one place... but when you do, you create a risk. Suppose you change your mind, and decide that a dollar sign should be used to flag comment lines. If you don't revise all of the comments in your documentation, you will have some places which say to use a dollar sign, and others which say use a semicolon. Not good.

Moving on. Notice all the bits in the documentation which are like "the application strips out comments from the data when the data is loaded"? Another example: "The application will check that this condition has been met..."?

As part of the process of creating code for the application... which we are just about to embark upon... I will put a "scratch" copy of the documentation within the source code. As features become written into the code, I will "cross them off" the "do list". Thus, my code will actually do what I planned it to do, not what it happened by random chance to end up doing, when I became distracted by the minutiae of implementing things.


Writing the code

Here we go....

Start up a new application. Name the form DD92f1. Save the project as DD92, creating a folder for it and all the files associated with it, including the quotes datafile. Save unit with the form's code as DD92u1. Those names come from "Delphi Demo". Use them even if you are working with Lazarus. (Changing all the references in the following is just too tedious.)

If you haven't downloaded the Delphi sourcecode I have provided, which includes a file of suitably formatted quotations, create a quotations file with a simple text editor (e.g. TextPad, which you can try for free). Just use copy/paste, using the text below. Add a few quotes (or quote substitutes) of your own, if you are feeling less than boring today.

"Quotes file substitute"? here you go....

<!--q-->
First quote
<!--s-->
Source of first quote

;Here's a comment line.
;Notice that variations on the layout of the
;  quotes have been included in the test data
<!--q-->2nd quote
<!--s-->Source of 2nd quote

<!--q-->
Third quote

;notice the spaces at the beginning of the
;next <!--s--> line... and that this line
;tests that <!--s--> CAN appear in mid line.

   <!--s-->Source of third quote

Learn to generate simple test data, and life becomes easier. Notice I put in a few "extras" in my dumb quotes, to test "features" of the Quote Machine.

Put two buttons on the form, name them buDoIt and buQuit. Caption them Do It and Quit.

The OnClick handler for Quit is...

procedure TDD92f1.buQuitClick(Sender: TObject);
begin
application.terminate;
end;

(We'll give buDoIt an OnClick handler later)

Put two labels on the form... make them quite wide. Name them laQuote and laSourceOfQuote. That second name is a little long, but we won't need to type it often. Leave the captions the same as the label names. We'll fix it so that they don't show that when the application starts, but in the design screen it is helpful if things show us their names.

Details: Not "important", but for this, I would set the "Autosize" properties of both labels to "false", and the "Alignment" (not "Align") properties to "taCenter", and then drag out the size of each label to span most of the width of the application's window.

So far, so simple? But do do the simple things first. Get "the shape" of your application roughed in.

In that spirit, do the following next, but don't expect it to do much... yet!

Make the OnClick handler for buDoIt be....

procedure TDD92f1.buDoItClick(Sender: TObject);
begin
GetQuoteAndSource;
laQuote.caption:=sQuote;
laSourceOfQuote.caption:=sSourceOfQuote;
end;

Remember... that's not supposed to work yet.

In case you were wondering? That's Bad Programming. I'm using global variables in a way that can lead to troubles. But we'll eat those vegetables another time.

I have at least been good in resisting naming "sSourceOfQuote" the easier to type "sSource". Try for consistency. It pays off in the long run. In this case, we are being consistent in that we used the same root ("SourceOfQuote") in both the label name and the variable name.

Just so we can make progress... and get rid of some typos, etc, "cheat"... intelligently... and add the line "sQuote,sSourceOfQuote,sQuotesLibFilename:string;" to the application in the context indicated by...

  private
    { Private declarations }
    sQuote,sSourceOfQuote,sQuotesLibFilename:string;
  public
    { Public declarations }
  end;

(Everything except the "sQuote.." line is already in your code... you just have to find it. Note also: I included sQuotesLibFilename, which we don't need yet, but will want before long. Might as well make it available now, while we are declaring the other variables.)

We have declared those two variables as global variables, available for our use throughout the application. Globals should be avoided, but that doesn't mean you never use them.

If you are, as I expect, looking at the application's code, press F12 to switch to viewing the application's form. Double click on it, to switch back to the code view. That will now be showing the newly created...

procedure TDD92f1.FormCreate(Sender: TObject);
begin

end;

Do not save at this exact point. (If you do, Delphi will strip out the newly created OnFormCreate handler skeleton. (Lazarus doesn't seem to exhibit this useful "tidying" behavior.))

Between the begin and the end, add....

sQuote:='Quote kludge';
sSourceOfQuote:='Source kludge';

Now save your work, and try running the application... not that it will be very exciting or polished yet!

Oh. I got caught. When I tried exactly what I told you above, in Delphi I got "Undeclared identifier: 'GetQuoteAndSource', didn't I? (In Lazarus, the message was "Identifier not found".)

Put a // in front of the first line of the buDoItClick procedure, in front of GetQuoteAndSource;, to "rem that out".

Try running the application again. It "works"!!... Sort of. You even get something happening when you click "Do It"!

It may seem that what we have is pretty pointless, but it isn't. It is a good foundation. Because we started with planning, decided ahead of time where we were going, all that we've done so far is a start towards our goal. And we have something working very early on. Watch how easily it builds into something better.

First, let's tidy up a small inelegance. When you run the application in its present state, the two labels have dreadful captions... dreadful as something to confront the user with.

Easily fixed. Go to your already existing OnCreate handler, change it so that it becomes what follows. (You only have to add two lines)....

procedure TDD92f1.FormCreate(Sender: TObject);
begin
laQuote.caption:='Welcome to the Quote Machine, DD92';
laSourceOfQuote.caption:='Delphi Tutorial from Sheepdog Software';
sQuote:='Quote kludge';
sSourceOfQuote:='Source kludge';
end;

Whew. Started. Onward...

That's a good start. Now we will introduce an initial GetQuoteAndSource procedure, which we will then build up.

Just before the final "end." in the code, add....

procedure TDD92f1.GetQuoteAndSource;
begin
sQuote:='Quote kludge';
sSourceOfQuote:='Source kludge';
end;

... and add the one new line in the following to the place indicated by the rest of what's here....

  private
    { Private declarations }
    sQuote,sSourceOfQuote:string;
    procedure GetQuoteAndSource;
  public
    { Public declarations }
  end;

Take the "//" off of the GetQuoteAndSource line in the OnClick handler for buDoIt, and try running the application again. It should run, and clicking Do It should do something... not much, and you don't see it "do" anything the second time you click it, but it is "working"... or you have typos to deal with!

Now we need to "clean up" some of the "scaffolding" we used earlier. Think about why first, and then from...

procedure TDD92f1.FormCreate(Sender: TObject);
begin
laQuote.caption:='Welcome to the Quote Machine, DD92';
laSourceOfQuote.caption:='Delphi Tutorial from Sheepdog Software';
sQuote:='Quote kludge';
sSourceOfQuote:='Source kludge';
end;

... remove the....

sQuote:='Quote kludge';
sSourceOfQuote:='Source kludge';

Those lines do no "harm", but they are no longer necessary... and unnecessary code is a Very Bad Thing. Will bite you in the butt every time... but never when you are expecting it, and in a position to deal with the affront easily.

Make sure the application still "runs"... even if "weakly".


Slight detour

Would I have done the following if I was just writing this for my own needs? No. But just before we embark on something substantial, I want you to do the following, just to reinforce what you should already understand....

Change GetQuoteAndSource to....

procedure TDD92f1.GetQuoteAndSource;
var bTmp:byte;
begin
bTmp:=random(3);//Will fill bTmp with 0, 1 or 2
   sQuote:='Quote One';
   sSourceOfQuote:='Source of Q1';
if bTmp=1 then begin
   sQuote:='Quote Two';
   sSourceOfQuote:='Source of Q2';
   end;
if bTmp=2 then begin
   sQuote:='Quote 3';
   sSourceOfQuote:='Source of Q3';
   end;
end;

Now, if you click Do It several times... it will probably take more than 3... you should see the different quotes arising, picked at random. Why does it take more than three clicks? Imagine you had a gaming die, and you "threw" it exactly six times. You wouldn't expect to get one "1", one "2", one "3", one "4", one "5" and one "6", would you? Which face of the die is left up is determined randomly, and which quote you get is determined randomly. Hence more that 3 clicks to see all three possible quotes.

A couple of little things are illustrated here. Note that even if I am wrong about "random(3)" producing 0, 1 or 2, (which I'm not... but there's an easy mistake lurking there, which I sometimes make) sQuote and sSourceOfQuote will always be filled with something in the course of passing through this part of the code.

Note also that while humans count "1,2,3....", many programmers count "0,1,2,...". I would actually usually keep what is inside the program, and what the user sees more in step for this... but cases often arise where it is worth introducing the discrepancy. Inside the computer, when in doubt, count from zero.

Speaking of "cases", there is a Delphi / Pascal word "case", which would be very appropriate to the code just presented. I did the job with if... thens just to save you having to think about one more thing... but if you wondered "wouldn't 'case bTmp of...' be better?", then take a gold star, Keilie Gibson! ($5 off the price of the Sheepdog Software application of your choice if you can explain the reference to Miss Gibson.)


The heart of the Quote Machine

Sigh. I've put it off as long as I can. Have a cup of coffee... the real work is about to begin.

Add a memo to the application's form. Make it quite wide, and generously high.

Set the following non-default choices for the memo's properties....

Add to the form's OnCreate handler "FillMemo", and make it...

procedure TDD92f1.FillMemo;
begin
sQuotesLibFilename:='DD92quotes.txt';
if fileexists(sQuotesLibFilename) then
Memo1.lines.LoadFromFile(sQuotesLibFilename)//no ; here
else begin
   showmessage('A text file named '+sQuotesLibFilename+' must '+
   'exist in the same folder as the application''s .exe file. '+
   'It must consist of at least two lines conforming to '+
   'the following pattern (quote and source).....'+chr($d)+chr($d)+
   '   <!--q-->E tu Brute?'+chr($d)+
   '   <!--s-->Caesar, J'+chr($d)+chr($d)+
   'Provide such a file.'+chr($d)+
   'Program will shut down after you press OK.');
   application.terminate;
   end;//else
end;

Remember that you not only need to insert the above, but also a declaration ("procedure FillMemo;", up near the top), like the one you did for the GetQuoteAndSource procedure. Note that you need the "TDD92f1" part at the start of the code, and that you must not include it in the declaration up at the top of the code.

The data file we discussed earlier must be in the same folder as your application. (The file should be called DD92Quotes.txt, but for the moment the exact contents don't yet matter.) Run the application and you should see the text from the data file appear in the memo. (Setting "read only" to true prevents the user from altering the contents of the memo from the keyboard. Code in the application can still change what's in the memo.)

(In "the real world", you might want to fill the sQuotesLibFilename in another part of the program, perhaps picking up the name of the file from an ini file... no big deal.)


Right.. now we get down to some (more?) "boring" stuff...

Okay. We've got the file into the computer. But back when we planned things, we said all sorts of brave things about what was allowed in the file, to make it user friendly. But now that it is in the computer, we are going to massage it to make it computer friendly.

We're now starting into a LONG section of BORING stuff... but boring is good in programming. It is boring because we are just chipping away, doing one little bit at a time, moving from where we are to where we want to be by SIMPLE steps, not clever steps. It takes more simple steps... but you don't go astray, and spend many hours trying to get something complicated working. You just spend a few hours taking the many simple steps... but staying on course along the way. You do have to develop the knack of "looking up" from time to time, and making sure that you always continue heading in the right direction. With the "simple steps" approach, you should never be "lost", should never have code which is doing things you can't understand... but you do have to keep the destination in mind, so that you are always heading towards it, not just taking well understood steps wandering all around the landscape!

Digression: When I got to this point, I knew there were some "chores" ahead... but I didn't anticipate two hours of heavy struggle just to start getting the next part of the application written! Don't be put off by that information, but take it as a cautionary tale... sometimes things just take longer than you think they will. I'll say more about this in a moment.

We will start by going through what is in the memo, the text we loaded from the data file, and in this pass we will....

<ul> <li>Strip out spaces at the start of any lines <li>Strip out blank lines <li>Strip out "comment" lines,. those beginning with a semicolon </ul>

Make a new button on the form, and make it's OnClick handler the code which follows. Don't worry too much about the comments in that code just now....

procedure TDD92f1.Button1Click(Sender: TObject);
var sTmp:string;
    c1,c2:integer;//Must be able to hold -1
begin
(*Beware "fencepost" problems. First line is line [0], and the
  memo1.lines.count will equal FIVE when there are FIVE lines,
  but the last line will be line[4]!

  Beware also: If, with a c1 that means you are working
  with the then-last line of the memo, you use...

  memo1.lines[c1]:=sTmp;
  or
  memo1.lines.add(sTmp);

  it may SEEM that an blank EXTRA line is added to the memo,
  because you can now use the down arrow to move onto the next
  line, whereas previously you could not... but the count of
  lines in the memo has not gone up. A puzzle, and one that can
  drive you crazy when debugging "last line" questions. Sigh.

  (These notes made while working with Delphi 4.)
*)

//Remove leading and trailing spaces and control characters...
c1:=0;
c2:=memo1.lines.count;
repeat
  sTmp:=memo1.lines[c1];
  if sTmp<>'' then begin
       if (sTmp[1]=' ') then begin
          sTmp:=trim(sTmp);//strips off leading and trailing spaces and control characters.
          //N.B.: We are only looking to strip LEADING spaces... lines with just TRAILING
          //   spaces... or starting with a control character... will not be modified.
          memo1.lines[c1]:=sTmp;
       end;//if sTmp[1]=' '...
     end;//if sTmp[1]=<>''...
  inc(c1);
until c1=c2;
(*Yes... the above WILL remove leading spaces from last line,
and the "until" is satisfied when c1 is "too big". Inc(c1)
happens AFTER attempt to work with current line[c1].
If there are 3 lines in the file, count will be 3, but the
last line will be line[2]*)

//Now....
//Remove blank lines, and lines starting with semicolons (;)...
//A line starting with a space and THEN a semicolon will also
//be removed, because spaces were removed before we looked
//for blank or semicolon lines.

c1:=0;
c2:=memo1.lines.count;
repeat
  sTmp:=memo1.lines[c1];
  if sTmp<>'' then if sTmp[1]=';' then sTmp:='';
  if sTmp='' then begin
     memo1.lines.delete(c1);
     dec(c1);
     dec(c2);
     end;
  inc(c1);
until c1>=c2;// '>' arises when you delete the last line.
end; //end of Button1Click

That will strip out all blank lines. Simple when you know how. I made three false starts before getting the above right for you. For instance, my first thought was to start by stripping out all the blank lines. Fine. Then I went on to strip out leading spaces. As a line could consist JUST of spaces, that would "reinsert" some blank lines, wouldn't it? Hence, in the now edited plan for this level of creating the application you can only see "remove leading spaces, then remove blank lines." (At first, I had those two steps the other way around. Etc.

Although I had to reverse the sequence of the steps, because I had always "been boring" and done them with two separate passes through the datafile, instead of "being clever" and trying to do to things with one pass, it was quite easy just to copy/ paste the code for "remove spaces" from after the "remove blank lines", put it first. Be boring when coding. Too much cleverness just leads to headaches. Be boring especially in the early stages. If this code turns out to take too long to execute, well at least you get a right answer, if slowly. Once you are getting a right, if slow, answer, you can reconsider the relevant part of the program, and maybe come up with a faster alternative. Anyway, I'm not always uber-timid: I did use only one pass to remove blank lines and those starting with a semicolon.

Note a little detail: See the "//end of Button1Click" at the end. This is a rem, Lazarus will ignore it. It only "sees" the "end;" at the beginning of the line. However, every time you create a procedure that is more than about 8 lines long, end it with a little rem like the one I've put at the end of Button1Click. It is very helpful, when you are scanning through code, if the programmer left you guidance as to WHAT a particular "end" ends.

Confession: I was not entirely successful when I tried to create the complete "overview", the description of "everything" which was way up at the top of this essay, and way up at the start of my "writing" of this application. Originally, I said that lines in the database which start with a semicolon are comment lines, which will be ignored while generating quotes. This is still true. What I should have said is "lines starting with a semicolon, or lines starting with a space or spaces and then a semicolon." As we are stripping out leading spaces before we look to see if the line "starts" with a semicolon, we have to modify the exact details of what lines are considered comments, don't we? It might seem like a "trivial" detail, but such things can trip you up if you don't pay attention to them.

Another thing to look out for in all such work: Maybe everything does what it should most of the time, but does the code work right if "it" (e.g. removing leading spaces) has to be done to the first or the last line? That's where "Are we counting from zero or from one?" can be a Big Pain. (Speaking of which, note that in the above, the lines of the memo are counted from zero, but the first character in sTmp is sTmp[1]. Sigh.

As I said, the 60 lines of Button1Click took me about two hours. Why? I was having a lot of trouble with getting the process to go exactly to the end of the memo. If you do one line too few, then the last line of data doesn't get processed. If it is blank, or starts with a space or a semicolon, the necessary adjusting doesn't happen. If you tell the application to do one line too many, usually Delphi just hangs up, and you have to use "Run|Program Reset" to "un-freeze" things... but that's better than what happens in some languages. In some, if you try to process a line beyond the last one in the memo, you can bring the whole computer down, lose unsaved work, and have to wait through a full re-boot. And even worse are the systems which give you no immediate sign that something is wrong... but will later fail when you aren't thinking about "am I doing EXACTLY the right number of lines?".

By the way.... you might think that all the comment lines in the new code are just for your benefit. No. I would put those in something like this even if no one else was ever going to see it. Especially if something has been a bit tricky, write documentation. Out it in the program, where it will always be next to the material it relates to.

So, after 2 hours for some "trivial" chores, is all well? Actually... no. There are still mysteries, still things that seem not quite right, in the Delphi version, even. (And those mysteries carry forward to the Lazarus version.) Sometimes after you have processed the memo, you can use the arrow keys to move to the end of the last line... but no further. And other times you can move down to the line below the last line with text, but not get beyond the first position on that line. I'm not entirely happy that all is well, even after my two hours. I may regret not spending three, and perhaps resolving that last mystery, but I'm going to hope for the best, and move on.


Further processing of the file of quotes

So far we have removed spaces at the beginnings of lines. In some cases... but not all... we have removed spaces from the ends of lines. We have removed blank lines, and lines that started with semicolons, or had semicolons following only spaces. (by the way... in this context I should be saying "spaces or control characters" throughout.)

For much of the rest of this tutorial, I will be telling you to extend the Button1Click procedure. The removing of spaces, etc, which we've done so far is a good start, but there's lots more to be done to convert the text in the memo from user friendly to computer friendly, and that's what the coming code is all about.

We still have the header lines. but it is easy to see where the header lines end... they end with the first Quote Record. The program can recognize that, because it starts <!--q-->. So it should be easy to discard the header lines.

Before we do that, however, it is time to visit our scratch copy of the description of everything that is going to happen in this application, and cross off the things that we've provided for already. I'm not presenting the description in its various stages in this essay, but be assured that I am actually doing what I have just described, and you ought to, too.

Just below the code you've done so far for Button1Click, add the following to remove the header lines, and to be sure that there is at least one quote in the file. (I might have said "Add the following just before the "end;//end of Button1Click". Is that more clear?)

//Now remove header lines...
c1:=0;
c2:=memo1.lines.count;
boTmp:=false;
repeat
  sTmp:=memo1.lines[c1];
  if copy(sTmp,1,8)='<!--q-->' then boTmp:=true;
  if boTmp=false then begin
     memo1.lines.delete(c1);
     dec(c1);
     dec(c2);
     end;
  inc(c1);
until (boTmp=true) or (c1=c2);

if c1=c2 then begin
  showmessage('Your datafile does not appear to have even '+
    'one line with <!--q--> at the start of a line. '+
    'The application will shut down. Revise your '+
    'datafile and try again.');
  application.terminate;
  //N.B. "application.terminate" used thus does not
  //always succeed, but it mostly will, and, I hope,
  //you won't try to run the app with an invalid datafile
  //anyway. But you should always TRY, as best you can,
  //to provide for the many, many possible user errors.
  //Thinking of ALL the mistakes users may make is the
  //hard part. Idiot-proofing things would be so much
  //easier if idiots were not so clever.
  end;

(To make that work, you also need to create a variable called boTmp, of type "boolean". Some would say it "should" be local to this procedure. I tend to make such things global variables, being very careful not to use "tmp" variables except to hold a value for just a very brief, clearly constrained, period. While you are at it, create a global bTmp. The two declarations go near the top of the program, as part of....

  private
    { Private declarations }
    sQuote,sSourceOfQuote:string;
    boTmp:boolean;
    bTmp:byte;
    procedure GetQuoteAndSource;
    procedure FillMemo;

At this point, Lazarus gave me a shock. It doesn't seem to allow you to have both a global bTmp and a local bTmp, which I wanted to have. (The local bTmp was in the GetQuoteAndSource procedure.) In this case, we are able just to delete the local declaration in GetQuoteAndSource. I suspect I've misunderstood something, because having both shouldn't be disallowed. And there are times when having both is helpful. But for now, just "fix it" by the answer I've given.

The "or c1-c2" test at the end of the repeat loop is necessary to get you out of the loop if the datafile has no line starting "<!--q-->". While this is not likely to happen, Murphy was a programmer... and if it is possible for something to go wrong, it is wise to provide for a way to handle it.

I'm sorry to have to include the little caveat about "application.terminate" not always "working". I'm afraid I don't know why it doesn't always work, or what conditions will "trick" it into not working. I use it frequently, and it usually "works"... but I know there are times when it doesn't. Sigh.

Be that as it may. We now have a lot of the extra "stuff" weeded out of our file of quotes.

Next, we will "slim down" the tags, changing each <!--q--> to qq and each <!--s--> to qs. At the start of a line, they will mean "start of a quote" and "start of a source", respectively. I hope that no one will want to use this application with a language which can have either token at the start of a line, Just in case, and in case of typos, a test and cure is incorporated in the following. (Any qq or qs which starts a line will be changed to q-q or q-s. Note that qq's or qs's within a line would not cause our code a problem, and so are left unchanged. Unlikely to arise? Yes. But remember: If it CAN go wrong, it WILL go wrong, unless you provide for it. Add the following to the bottom of the buButton1Click OnClick handler. It may look like a lot of code, but if you read through it, you will find no rocket science.

//Convert "human friendly" tags to shorter tags, and
//  process any "qq"s or "qs"s which are supposed to start
//  a quote or the description of a quote's source...
c1:=0;
c2:=memo1.lines.count;
repeat
  bTmp:=255;//Biggest number you can put in a Delphi byte- type variable
  sTmp:=memo1.lines[c1];
  if copy(sTmp,1,8)='<!--q-->' then bTmp:=0;
  if copy(sTmp,1,8)='<!--s-->' then bTmp:=1;
  if copy(sTmp,1,2)='qq' then bTmp:=2;
  if copy(sTmp,1,2)='qs' then bTmp:=3;
  //Note: AFTER this process, there will be many lines
  //starting qq or qs. But IN this process, we convert
  //lines which STARTED starting with qq or qs to lines
  //starting q-q and q-s, respectively.

  if bTmp=0 then begin
    sTmp:='qq'+copy(sTmp,9,length(sTmp)-8);
    memo1.lines[c1]:=sTmp;
    end;
  if bTmp=1 then begin
    sTmp:='qs'+copy(sTmp,9,length(sTmp)-8);
    memo1.lines[c1]:=sTmp;
    end;
  if bTmp=2 then begin
    sTmp:='q-q'+copy(sTmp,3,length(sTmp)-2);
    memo1.lines[c1]:=sTmp;
    end;
  if bTmp=3 then begin
    sTmp:='q-s'+copy(sTmp,3,length(sTmp)-2);
    memo1.lines[c1]:=sTmp;
    end;

  inc(c1);
until c1=c2;

It may seem that you are working very hard... but it could be worse! Each time I added the "next bit", I tested it, to see that it actually worked. It is important to try to imagine (and test for) all conceivable scenarios. When you are writing your own code, you'll need to do that testing, too. But if you do, the location of the flaw should be easy to pin down. It should relate to something you've just changed. The right place to make changes will not always be in the code you just wrote, though. (The new code may have revealed a flaw in your design.)

At the start of this essay, it might have seemed that we aren't doing anything very complicated of challenging. And we aren't. But the details do have to be taken care of, none- the- less. By building the application up, a bit a time, and testing each bit as we add it, we will eventually create something robust and reliable, without spending too long doing it.


More massaging of the data in the memo....

Now we come to a quite painful task. It isn't "clever".. just tiresome. You might be tempted to say it isn't worth the trouble,,, but it is. A little "pain" here will save us having other pains later, when we come to actually USE the quotes! Remember what we wanted? Just a little Quote Machine to throw up quotes when we click a button. Who would have thought it would be so hard??!

At the moment, our quotes file might look like....

qq
She made me do it.
qs
Adam, Genesis
qq
I am not a crook
qsRichard Nixon
qqNever work with children or animals
qsWC Fields

What should you notice in the above? This: we have quote records with the tags ("qq" and "qs") on their own lines... but we also have quote records with the tags pre-pended to the quote and/or source. To a human, the difference is trivial. Computers are not as adaptable, and work better if things follow one pattern.

Our next code will further massage the file. After that, the tags are always pre-pended to the line they mark, thus

qqShe made me do it.
qsAdam, Genesis
qqI am not a crook
qsRichard Nixon
qqNever work with children or animals
qsWC Fields

Happily, the work we've done so far limits the possibilities of what is in the memo at this stage.

The following looks a bit complex, perhaps, but it isn't doing more than what I have just described. Again, it is to be added to the bottom of our growing Button1Click.

//Put all tags ("qq"/""qs") at the start of the data they
//   mark, Don't leave them on their own lines...
c1:=0;
c2:=memo1.lines.count;
repeat
  sTmp:=memo1.lines[c1];
  if sTmp='qq' then begin
     sTmp:='qq'+memo1.lines[c1+1];
     memo1.lines[c1]:=sTmp;
     // and now take the extra line out...
     memo1.lines.delete(c1+1);
     dec(c1);
     dec(c2);
     end;

  if sTmp='qs' then begin
     sTmp:='qs'+memo1.lines[c1+1];
     memo1.lines[c1]:=sTmp;
     // and now take the extra line out...
     memo1.lines.delete(c1+1);
     dec(c1);
     dec(c2);
     end;

  inc(c1);
until c1=c2-1;
//minus 1 in case file ends with "qq" or "qs".... which it shouldn't... but
//  it could. That particular error will be caught in a moment, when we check
//  that the last line of the file is a "qs+source description" line.
//HERE, we are only conflating qq and qs lines which are on their own,
//  conflating them with what comes next

We're now nearly there... After you check that the above works, add the next bit...

//=========
//Right... NOW we can check that the file ends with a line starting qs, and
//  having a source- of- quote description after it. Remember: Not EVERY
//  quote needs to have a source description, but the last one in the
//  file DOES need one, to save us some hassle that I described elsewhere.
//  (That hassle associated with USING the file of quotes)

//BLUSH: I had to add a little kludge here. There SHOULDN'T be a blank
//  line at the end of the memo by now... but it turns out, somehow, that
//  there can be one. IF there is, at this point, it cannot... I hope...
//  be a problem. Other things we've done so far should still be "happy"...
//  So it is "okay" (not ideal, but "okay") to just take off what should
//  not be here... and THEN make the check we were going to make...

c1:=memo1.lines.count;
repeat
sTmp:=memo1.lines[c1];
if sTmp='' then begin
  memo1.lines.delete(c1);
  dec(c1);
  sTmp:=memo1.lines[c1];
  end;
until sTmp<>'';
//Could we have a case where we entered that loop when there
//  was NOTHING but blank lines in the memo? Perhaps. And if
//  that arose, the program would crash. I'm going to take
//  THAT chance. Got to live dangerously once in a while.

//The sort of highly specific tests in the next line are
//  not always possible... but that doesn't mean that they
//  are NEVER possible. But when you use them, always ask
//  yourself... "Can I rely on things being like this?"
//  "Am I covering all the possibilities?"
//At this point in this program I know a lot, and I
//  only want to check one "detail"... so this works...
//  ... if I haven't overlooked something!

if(length(sTmp)<3) or (copy(sTmp,1,2)<>'qs') then begin
   showmessage('The last line of the data file must be '+
   'describing the source of a quote. Not EVERY quote '+
   'needs its source described, but the last quote DOES '+
   'need this. Revise your datafile. The application '+
   'will shut down when you click "OK".');
   application.terminate;
   end;

Done?? You'd think so, wouldn't you? There is just one more "little" thing to check... well, only one more that occurs to me so far. We'll see if I've thought of everything when we try to use the massaged memo! Can you think of what it might be?




....




....come on... YOU do some work for a change...




....what have we not yet tested??....




....




....last chance to think of it for yourself...




....




....something to do with what the data in the memo should consist of...



....We've made sure that the last line is "qsSomething", but....




Hope you thought of this....

We need to check that the whole memo now just lines starting with qq or qs, with something following, which should be a quote or quote- source, e.g....

qqShe made me do it.
qsAdam, Genesis
qqI am not a crook
qsRichard Nixon
qqNever work with children or animals
qsWC Fields

... AND we need to check that "qs" lines are always followed by "qq" lines. There should never be two "qs" lines in a row.

I'm not sure I've said the following explicitly elsewhere. Here's a detail to reward the attentive reader: As designed here, all quotes delivered by the Quote Machine must consist of only one "line"... if it is a long line, your computer may use word wrap to display it across several lines on the screen, but, to the computer, it must be only one "line".

So, here's the final section of code needed for buButton1Click's OnClick handler. After this, we will be moving on to new and I hope interesting things....

//Next, check that memo now consists only of qq/qs lines, with
//  no cases of two qs lines in a row. We don't need to check
//  that the last line is a qs line, but we do need to check
//  that the penultimate line wasn't also a qs line, and the
//  easiest way to do that is to make a special case out of
//  it....

//During these tests, we will also check that no line is
//  JUST "qq" or "qs", which would also be wrong.

bTmp:=255;//For now, assume that it is true that there
   //are no problems in the structure of the memo.

//check penultimate line...
sTmp:=memo1.lines[memo1.lines.count-2];
if length(sTmp)<3 then bTmp:=0;
if copy(sTmp,1,2)<>'qq' then bTmp:=1;

//check last line...
sTmp:=memo1.lines[memo1.lines.count-1];
if length(sTmp)<3 then bTmp:=0;
if copy(sTmp,1,2)<>'qs' then bTmp:=3;

//Now check all lines are at least 3 characters, and that
//  only qq lines follow qs lines. (In the body of the memo
//  two qq lines in a row are ok, and we've already checked
//  that the last quote record has both a qq and a qs part.

//OOPs... late in the day, I realized that one(?) more test
//  is needed: 1st line must be a qq line. Happily, because
//  of the structure of the program, adding the extra test
//  was not very difficult.


c1:=0;
c2:=memo1.lines.count-2;
repeat
  sTmp:=memo1.lines[c1];
    if length(sTmp)<3 then bTmp:=0;
    if copy(sTmp,1,2)='qs' then
       if copy(memo1.lines[c1+1],1,2)='qs' then bTmp:=2;
  inc(c1);
until (c1=c2) or (bTmp<>255);

if bTmp=0 then begin
   showmessage('There is at least one line in the data '+
   'file which, after massaging, is too short. '+
   'Revise your datafile. The application '+
   'will shut down when you click "OK".');
   application.terminate;
   end;

if bTmp=1 then begin
   showmessage('The penultimate line in the data '+
   'file, after massaging, must be a <!--q--> line, and '+
   'it isn''t. Revise your datafile. The application '+
   'will shut down when you click "OK".');
   application.terminate;
   end;

if btmp=2 then begin
   showmessage('You cannot, as you have now, '+
   'have two  <!--s--> lines in a row. '+
   'Revise your datafile. The application '+
   'will shut down when you click "OK".');
   application.terminate;
   end;

if bTmp=3 then begin
   showmessage('The last line in the data '+
   'file, after massaging, must be a <!--s--> line, and '+
   'it isn''t. Revise your datafile. The application '+
   'will shut down when you click "OK".');
   application.terminate;
   end;

if bTmp=4 then begin
   showmessage('The first line in the data '+
   'file, after massaging, including the removal '+
   'of the header lines, must be a <!--q--> line, and '+
   'it isn''t. Revise your datafile. The application '+
   'will shut down when you click "OK".');
   application.terminate;
   end;

As you can see from one of the comments in that, late in the day, I realized that one(?) more test was needed: the first line must be a qq line. Happily, because of the structure of the program, adding the extra test was not very difficult.

Oh heck... I'd written "But... hurrah!... we are now ready for the "interesting" part of the creation of the Quote Machine" when I realized... because I checked the "scratch" copy of the program requirements before moving on... that we've STILL left one check out. Sigh.

We haven't yet checked that EVERY line, after the preliminary massaging, starts qq or qs!

We will add a another pass through the datafile at the point where we have combined the tag with the line, i.e. when we have....

qqShe made me do it.
qsAdam, Genesis
qqI am not a crook
qsRichard Nixon
qqNever work with children or animals
qsWC Fields

... rather than...

qq
She made me do it.
qs
Adam, Genesis
qq
I am not a crook
qs
Richard Nixon
qq
Never work with children or animals
qs
WC Fields

The code we need is quite simple. It can be derived from the code we used to see that there are never two "qs"s in a row.

Just before....

//=========
//Right... NOW we can check that the file ends with a line starting qs, and...

... insert the following....

//=========
//Check each line except the last starts with a qq or
//   a qs. (We check the last line later, anyway, and there
//   is a quirk (bug) in the program still which means that
//   the last line at this point is sometimes still a blank
//   line, even though that "shouldn't" happen. Sigh.
bTmp:=255;
c1:=0;
c2:=memo1.lines.count-1;
repeat
  sTmp:=copy(memo1.lines[c1],1,2);
  if (sTmp<>'qs') and (sTmp<>'qq') then bTmp:=0;
  inc(c1);
until (c1=c2) or (bTmp<>255);

if bTmp=0 then begin
   showmessage('There is at least one line in the data '+
   'file which, after massaging, doesn''t begin with '+
   'either "qq" or "qs". '+
   'Revise your datafile. The application '+
   'will shut down when you click "OK".');
   application.terminate;
   end;

That was supposed to be "the last thing", but in fact, the "final code" I MEANT to write when I wrote that was something else. But the good news is that we did need the stuff we've just written.

So what did I MEAN to provide code to handle? We need to check, before we convert <!--q-->s and <!--s-->s to qq's and qs's, respectively, that there are not any lines starting "qq" or "qs" already. Sigh. But at least again, we've already got nearly the code we need. To make the check described, add the following to the program.

Nota bene: This code does not just go at the bottom of all the Button1Click code created to date, which is where you should have been putting all the previous bits, as they were developed. This code goes just before the....

//Convert "human friendly" tags to shorter tags, and
//  process any "qq"s or "qs"s which are supposed....

So!... in the right place, insert...

//Make sure that no line starts with "qq" or "qs"
//   at this point. (We haven't yet converted the
//   human friendly tags to the shorter tags.
bTmp:=255;
c1:=0;
c2:=memo1.lines.count-1;
repeat
  sTmp:=copy(memo1.lines[c1],1,2);
  if sTmp='qq' then bTmp:=0;
  if sTmp='qs' then bTmp:=1;
  inc(c1);
until (c1=c2) or (bTmp<>255);

if bTmp=0 then begin
   showmessage('There is at least one line in the data '+
   'file which, before massaging, starts with "qq". '+
   'This is not allowed. Revise your datafile. The '+
   'application will shut down when you click "OK".');
   application.terminate;
   end;

if bTmp=1 then begin
   showmessage('There is at least one line in the data '+
   'file which, before massaging, starts with "qs". '+
   'This is not allowed. Revise your datafile. The '+
   'application will shut down when you click "OK".');
   application.terminate;
   end;

Right! At last! I really, really hope that we are done with all the preparations we need to make. Finally, we can proceed to....


Creating the basic application

We have a form with a "Do It" button, two labels (to hold quote and source), and a memo to hold the "library" of quotes the Quote Machine will choose from. (You can hide the memo later, if you want to. Probably makes sense. But it is helpful to be able to see the contents during debugging.) We also have the code which converts a human-friendly text file, e.g....

Datafile for DS92...
A few introductory remarks....

<!--q-->
She made me do it.
<!--s-->
Adam, Genesis
<!--q-->I am not a crook
<!--q-->
Never work with children or animals
<!--s-->
WC Fields

....into a less human friendly, more rigorously specified, and more telegraphic file, which the computer will draw quotes from. Along the way, we have run various checks to see that the data is in a valid format, and to advise the user of what flaws are found, if any. The "internal" form of the datafile above would be....

qqShe made me do it.
qsAdam, Genesis
qqI am not a crook
qqNever work with children or animals
qsWC Fields

At the moment, the massaging and vetting occurs when the temporary button "Button1" is clicked. The massaging and vetting should occur just after the call of FillMemo in the OnCreate handler for the application's main form.

Oh no!

We have to take a little detour!

I spent hours creating the first version of this tutorial, the Delphi-only version. I then spent further hours adapting it to apply to Lazarus as well, testing that what I said you should do would actually work.

I got this far in a very good mood, because Lazarus was living up to the promises I'd heard about it being a "free Delphi". Things were Just Working! Hurrah!

And then something weird struck. I'm not convinced it is a big problem... but I don't at the moment have a very good answer.

Before the "Oh no" heading above, I was saying that MassageAndVett should just happen automatically, as the application starts up. And it will, in Delphi, and almost will in Lazarus, if we just move the Button1Click code into MassageAndVett, and call that within the FormCreate handler. Which is what I will show you how to do in a moment.

Sadly, at the moment, when I run the resulting program under Lazarus, I get....

Project DD92.exe raised exception class 'EStringListError' with message:
Lost index exceeds bounds (24)

Break/ Continue?

[ ] Ignore this exception type

Let me first say that I hate little tick boxes like the one here. If I tell Lazarus to ignore this exception type, what will I miss, and can I turn "paying attention" back on. I did not tick the box.

I did, however, click on "Continue". That gave rise to....

List index exceeds bounds (24).

Press OK to ignore and risk data corruption.

I did click on OK, and no untoward events transpired... the times I tried it. But the warnings make me nervous.

Weird, though: The same code works, without warnings, if I use it in one spot... but triggers the warnings if I use it inside the FormCreate handler. It may be to do with the form not yet being fully created? A mystery to be resolved. Sigh.

If you go ahead and release the software in the "dangerous" form, the user who invokes it directly from the .exe only sees the second warning/ question.

I have a feeling that all of this is not really a problem, that there is a way around it.... but I must admit that I can't (yet) give you the details.

Back to creating the final application...

Make the FormCreate procedure say....

procedure TDD92f1.FormCreate(Sender: TObject);
begin
laQuote.caption:='Welcome to the Quote Machine, DD92';
laSourceOfQuote.caption:='Delphi Tutorial from Sheepdog Software';
FillMemo;
MassageAndVett;
end;

Create a procedure called MassageAndVett, with just the following...

procedure TDD92f1.MassageAndVett;
begin
//comment to keep the proc from being deleted
end;

Add the necessary....

procedure MassageAndVett;

... line up at the top of the code, in the "private" section of the declarations, just after the "procedure FillMemo;" which is there already.

Try running the application. While you won't see anything new or different, yet, it should compile and run okay.

This might be a good time to save your work!

Very carefully, copy ALL of the code in the Button1 OnClick handler into the MassageAndVett procedure. Be sure to include the...

var sTmp:string;
    c1,c2:integer;//Must be able to hold -1

...from before the "begin", as well as the rest.

Once that is (properly) done, you will have an application which is "nearly done". As soon as the application is launched, the data will be fetched from the datafile, and massaged into a machine- friendly form. Along the way, it will be checked to be sure that various requirements have been met. As I explained a moment ago, Lazarus will "moan"... but just tell it to Do It Anyway.

Once this is done, our need for the "Button1" button is ended. Delete everything in the body of the Button1 OnClick handler, i.e. delete the "var c1,c2:integer;//Must be able to hold -1" and everything between the procedure's "begin" and "end". Delete any comments associated with the Button1 OnClick handler. Then delete the button. Make sure your application still runs, that you haven't deleted things you shouldn't have. If it won't run at the moment, go back to the saved version you had. If it will run, then re-save the code.

In Delphi, the skeleton of the Button1 OnClick handler should be erased for you by the system, when you save. Some other hidden stuff will be cleaned up at the same time, and the declaration of the procedure up at the top of the code will be removed, too.

Lazarus doesn't seem to do this work for you. My advice, at this point? Just leave the empty shell of Button1Click in place. I'm new to Lazarus. Maybe there's a way to "turn on" the housekeeping Delphi does? Maybe there are "safe" procedures for removing the shell of no- longer- needed components' event handlers? (A button is one type of component.) But I do know that messing with installed components can lead to trouble, so I won't be deleting the shell of the Button1Click handler until I know the right way to do that.

And now, I must admit a(nother) mistake on my part. Right at the beginning of developing the MassageAndVett code, I should have put that in its own procedure, just like the one it ended up in. During de-bugging, I could still have used the temporary Button1 to invoke the procedure. And that would have saved you the risky copy/ paste, etc, which we've just been talking about. Sorry. Oops. I left things as they are so you could see how things actually evolved, rather than describing an air-brushed ideal world.


"Easy" is hard, "Hard" is easy....

Despite all my years of programming, and despite having written this program once before, I would never have guessed a quarter of the work it took to get to this point. Well done you, those who are still reading.

I've got some really good news for you...

Because of the care taken in planning the datafile's structure, and then the care taken with its "reduction" to a more human- friendly form, the next (final) part of creating the quote machine is almost trivial!!

If you think back to work we did at the beginning, you may recall that the OnClick handler for buDoIt is....

procedure TDD92f1.buDoItClick(Sender: TObject);
begin
GetQuoteAndSource;
laQuote.caption:=sQuote;
laSourceOfQuote.caption:=sSourceOfQuote;
end;

From that, I hope you see that there's a procedure called GetQuoteAndSource, and that you can infer that it fills the global variables sQuote and sSourceOfQuote. (Yes, using global variables to pass data between modules of the program is a Bad Idea... but it is a happy shortcut much of the time. But you will be a limited programmer if you don't... another time... learn the alternatives.

So... let's look at GetQuoteAndSource. At the moment it is....

procedure TDD92f1.GetQuoteAndSource;
var bTmp:byte;
begin
bTmp:=random(3);//Will fill bTmp with 0, 1 or 2
   sQuote:='Quote One';
   sSourceOfQuote:='Source of Q1';
if bTmp=1 then begin
   sQuote:='Quote Two';
   sSourceOfQuote:='Source of Q2';
   end;
if bTmp=2 then begin
   sQuote:='Quote 3';
   sSourceOfQuote:='Source of Q3';
   end;
end;

The final GetQuoteAndSource

Memos are so wonderful. They make things so easy.

If our memo held just....

qqShe made me do it.
qsAdam, Genesis
qqI am not a crook
qqNever work with children or animals
qsWC Fields

... then....

memo1.lines[0] would be 'qqShe made me do it.'

memo1.lines[4] would be 'qsWC Fields'

... and ...

memo1.lines.count would return 5

I've told you before... the numbering can be confusing. Study the example above. The first line is "line zero". The count of lines is done as any "normal" person would do it. There are five lines, so memo1.lines.count returns 5. However, to LOOK AT the LAST line, you say "Show me memo1,lines[memo1.lines.count-1]... don't miss the "minus 1" bit in that!

Here's something else you may need reminding of...

If I ask for random(3), I will get 0, 1 or 2.. but not 3.

Combine the above, and you should see that if you say...

sTmp:=memo1.lines[random(memo1.lines.count)];

.. then you will get a random line from the file.

The last quote record will ALWAYS have a quote and a source-of-quote line, remember.

If you say....

sTmp:=memo1.lines[random(memo1.lines.count)];

... you will get a random line from the file, but NOT the last line of the file. You might get the penultimate line of the file, but that's okay. Watch...

c1:=random(memo1.lines.count);
repeat
  sQuote:=memo1.lines[c1];
  inc(c1);
until copy(sQuote,1,2)='qq';

That will jump to a random point in the file. If the place we've jumped to happens to be a 'qq' line, we're done with this part of generating what we need. If we happen to land on a 'qs' line, the repeat/ until sends us back, and we move forward one line, to a line which WILL be a 'qq' line, because of the rules we made about how the datafile is structures.

Often with "repeat/ until..." structures looking for something in a file, we have to be careful not to go past the end of the file. Can't happen here. You should be able to see why not, and if you don't, study the material above until you do see why not.

Having filled sQuote, we now need to see if the datafile has a source for the quote. It won't in every case. When the datafile has no source for the quote, we will return with sSourceOfQuote set equal to '' (nothing).

sSourceOfQuote:='qs';
sTmp:=memo1.lines[c1];
if sTmp[2]='s' then sSourceOfQuote:=sTmp;

All that is left now is to strip off the 'qq' or 'qs' at the start of the two values. You can see that being done at the bottom of what follows, which replaces the previous GetQuoteAndSource

procedure TDD92f1.GetQuoteAndSource;
var c1:integer;
begin
//Pick a line as a possible quote... and
//  go to the next line if it isn't one.
c1:=random(memo1.lines.count);
repeat
  sQuote:=memo1.lines[c1];
  inc(c1);
until copy(sQuote,1,2)='qq';

//Fill sSourceOfQuote... with 'qs' if there
//  is no source given in the datafile.
sSourceOfQuote:='qs';
sTmp:=memo1.lines[c1];
if sTmp[2]='s' then sSourceOfQuote:=sTmp;

//Strip off the 'qq' and 'qs' at the starts
//  of the quote and source....
sQuote:=copy(sQuote,3,length(sQuote)-2);
sSourceOfQuote:=copy(sSourceOfQuote,3,length(sSourceOfQuote)-2);

end;//GetQuoteAndSource

That's it! The Quote Machine works! Sometimes the quotes (or sources) are too long to fit in the labels we have provided, but don't let that worry you.

The basic elements of the Quote Machine can be "transplanted" to a bigger program. If that application is generating text for web pages, then just having a few well placed <br>'s in the text will cause it to split into several lines when the web page is displayed. Even without the <br>'s, the browser will wrap long lines, usually. (It won't do so in every context... but it will in most!)


Transplanting the essentials of the Quote Machine to a bigger program.

You might want to create a stand alone module for this... but that's a discussion for another day. To transplant the essentials "by hand", you need to do the following. Don't try to run it until I tell you to, by the way.

I'm afraid that while I've spent hours on adapting the above to explain any Delphi- Lazarus differences, I have not (yet) revised the rest of this to explain such differences. You're on your own now, Lazarus readers! But everything else has gone so well that I trust that the rest is done in Lazarus (almost) as it is done in Delphi....

1) Make sure that the application you are adding the Quote Machine to is not already using variable or procedure names that you are using in the Quote Machine, apart from silly things like bTmp.... and even in that case be careful. Make sure the application doesn't expect a value in something like bTmp to remain unchanged for extended periods, not if the value is wanted out of bTmp at the end of the extended period.

2) Copy the declarations from the "private" section to the same section in the application you are adding a Quote Machine to... i.e. these declarations...

    procedure GetQuoteAndSource;
    procedure FillMemo;
    procedure MassageAndVett;

.. plus if any of the following are not yet provided for, put global declarations in for them...

    sQuote,sSourceOfQuote:string;
    boTmp:boolean;
    bTmp:byte;

Copy the main body of the three procedures to the application gaining the Quote Machine, e.g. the following. **N.B.** you will have to change the first thing after the word "procedure", the "TDD92f1". You'll have to change it to whatever is right in the application you are adding the Quote Machine to. As I was putting a Quote Machine into DS025, with a main form called DS025f1, the first line below had to be changed to....

procedure TDS025f1.GetQuoteAndSource;

... here's one of the three things you have to move....

procedure TDD92f1.GetQuoteAndSource;
var c1:integer;
begin
//Pick a line as a possible quote... and
//  go to the next line if it isn't one.
c1:=random(memo1.lines.count);
repeat
  sQuote:=memo1.lines[c1];
  inc(c1);
until copy(sQuote,1,2)='qq';

//Fill sSourceOfQuote... with 'qs' if there
//  is no source given in the datafile.
sSourceOfQuote:='qs';
sTmp:=memo1.lines[c1];
if sTmp[2]='s' then sSourceOfQuote:=sTmp;

//Strip off the 'qq' and 'qs' at the starts
//  of the quote and source....
sQuote:=copy(sQuote,3,length(sQuote)-2);
sSourceOfQuote:=copy(sSourceOfQuote,3,length(sSourceOfQuote)-2);

end;//GetQuoteAndSource

3) You need to put a memo on the application's form. Set it's properties as we did during DD92...

4) You need to add two lines to the application's OnCreate handler for the application's main form. (Recall that in Lazarus, this gave rise to the mysterious "List index exceeds bounds" issue.) They are....

FillMemo;
MassageAndVett;

We left the memo visible while working on DD92. In a "real world" example, you probably wouldn't want to have the memo visible... so, also in the FormCreate procedure, add "memo1.hide;" You will probably also want to add "randomize;" to the FormCreate procedure.

That's what it takes to put the Quote Machine inside another program. Of course we haven't talked yet about using the quote machine within the other program.

I wrote an application which created a page of HTML. To get that page to include a quote, I used the following. The "meHTML.lines.add" is how I was adding things to the HTML page that the application was putting together. To prevent having the same quote appear twice in a row, add a string variable, sPrevQuote, to the program. Initialize it as empty during the main form's OnCreate. Having the code shown adds a requirement to the quotes data file: There must be at least two quotes in the file.

//Here begins code from Quote Machine, DD92....
repeat
GetQuoteAndSource;//fills sQuote, sSourceOfQuote
until sQuote<>sPrevQuote;
sPrevQuote:=sQuote;

meHTML.lines.add('<br><br><hr><center>A quote chosen from a bank '+
   'assembled by the administrator of this page. It '+
   'changes often...</center>');//as often as graph refreshes
meHTML.lines.add('<br>'+sQuote);
meHTML.lines.add('<br>'+sSourceOfQuote+'<br><br><hr>');
//end of code from Quote Machine, DD92....

Here's a neat little detail: The Quote Machine just returns whatever text is in your quotes datafile. That data might include something like...

<!--q-->
Fee<br>Fie<br>Fo<br>Fum

.... "one line" as far as the internals of the Quote Machine are concerned... but if the program using what the Quote Machine returns passes that to a page of HTML, the "line" can appear across several lines, it can be centered, be in italic, etc, etc!



In conclusion....

So? That helped, I hope? Take aways: TMemo components make working with text files from data files stored on disks, etc, a breeze. (memo1.lines.load('file.txt');) PLAN, PLAN, PLAN... then start writing code. Develop your applications a step at a time, testing each bit as you go.

Oh! And have fun with the quotes. Build your own library!





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


Click here if you're feeling kind! (Promotes my site via "Top100Borland")

If you visit 1&1's site from here, it helps me. They host my website, and I wouldn't put this link up for them if I wasn't happy with their service. They offer things for the beginner and the corporation.www.1and1.com icon

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
How to contact the editor of this page, Tom Boyd


Valid HTML 4.01 Transitional Page 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 .....