HOME - - - - - Delphi Tutorials TOC - - - - - - - - - - - - Other material for programmers

Delphi: A demo project: Testing with performance recording. Also: How to Save on Exit

This tutorial is not as polished as most, but there is good information here. You can download a zip file with the sourcecode.

DD74

Introduction

In this tutorial, we will develop the basis for a program which can help learners master material they need to know. What is special about this "drill and practice" program is that it will be able to remember the user's past performance on each question. That opens the way to a program that concentrates on what the learner has not yet mastered.

Along the way, we explore many generally useful programming concepts. Not least, at the end we cover how to implement the sort of thing that is present in every decent program where the user creates data... Protection agaist the user closing the application without saving the data first!

If you are only interested in the Save-On-Exit programming, you can skip down to that, but be warned... some of what has gone before is involved.

The application was created on an XP machine, using Delphi 4, but I don't think there's anything special to either of those; I mention the environment just to be thorough

Overview

A datafile on a disk will hold both the questions and the user's previous performance on each question. At the start of a session, that file will be moved to a memo in memory. The history data will be written to the memo in the first instance. From time to time, the user can re-save the memo back to the disk.

Working through this tutorial should expand your understanding of working with memos.

Development

Start a new project.

Do an initial save, establishing a folder for all things connected with this demo. Set the project up with a form named DD74f1 and a unit named DD74u1.

Put four buttons on the form. Name them buLoadMemo, buPlay, buSave and buQuit. Set buPlay and buSave's "enabled" properties to false... you shouldn't be able to "Play" or "Save" until the memo has been loaded! The buttons will be enabled once the memo has been loaded. Make the buttons captions &Load Memo, &Play, &Save and &Quit. (The ampersands (&) will mean that if the form/ application has focus, then just pressing L, P, S or Q will be as good as clicking on any of the buttons, if it is enabled.)

Before we can go farther with the application, we need to create the data it will "chew" on. Use Notepad or some other text editor... you can do it within Delphi! (File|New... Text)... to create a little text file with the following. (I'll explain it in a moment.)

q1nnnnn05What is 14 minus 9?
q2nnnnn0aWhere is Paris? a-France, b-Germany, c-India
q3nnnnn48What is 8 times 6?

Save the file in the same folder the application is being built in, and name it "DD74questions.txt".

Now... what do the characters on the lines mean?

The first two characters on each line simply identify the question. They can be whatever you like, but something logical would be wisest.

The next 5 are where our record of the user's success in answering the questions will reside. We are building provision to remember users last five answers. An "n" means the user hasn't answered the question, "+" will signify that the answer was right, "x" will signify the answer was wrong. So, if on a given line, the code were "++xnn", it would mean that the user has answered the question 3 times so far, getting it right ("+", for the symmetry in the symbol) on the most recent two attempts, but wrong ("x") on the first attempt.

The next two characters are the right answer to the question. More in a moment.

And the rest of the line is the question to be presented to the user.

Going back to "the answer": In this crude implementation, which is merely meant to show you some things about working with memos as data, and about saving data on exit, all answers will consist of two characters. We will make one concession to user sanity by setting up the program to accept single character answers, padding them with a zero on the left to allow users to enter "5" to give 05 as their answer, or "a" to give "0a" as their answer. It will seem to users that "5" and "a" are acceptable answers... and they will be accepted... but the computer will treat them as "05" and "0a" so that other questions may have answers like "48". By the way... the program will be "seeing" 05 and 48 as mere characters. It will not "understand", say, that 05 is one more than 04. There's no need for it to be arithmetically literate. The person putting the file DD74questions.txt together will be responsible for all arithmetic, where necessary to a question/ answer pair.

Yes... the scope of questions/ answers possible, and the way they will be presented is crude. Such issues are not the point of this programming tutorial. they can be addressed,,, but won't be, here.

Onward!

Put a memo on the form. The default name (memo1) is adequate. Change the memo's "lines" property so that the memo is empty. Change the memo's WordWrap property to false.

Double-click on buLoadMemo. This should bring up the skeleton of and event handler for you....

procedure TDD74f1.buLoadMemoClick(Sender: TObject);
begin

end;

Between the "begin" and the "end", fill in....

memo1.Lines.LoadFromFile('DD74questions.txt');

That may look a bit scary, but in a moment I think you'll agree that we'll keep secret from our friends just how easy these things are.

Try running the program, and clicking on the "Load" button. You should see the memo fill with what is in the questions/answers/performance file.

Then exit the program, and come back here.

Reading the scary line from left to right...

"memo1." says "with memo1's..."

"Lines." is a property of any memo

"LoadFromFile." is a method for any Lines property. It is built into Delphi.

"('DD74questions.txt')" specifies where to get the data to put in memo1's Lines property.

A minor aside about something that may be obvious....

If you had a string variable, say sTmp, with 'DD74questions.txt' in it, then you could use....

memo1.Lines.LoadFromFile(sTmp);

... end minor aside.

There are things you could do, should do, to trap errors... suppose there is no DD74questions.txt, for instance. But we'll shirk those tedious details.

Filling the memo from a file one of the core techniques demonstrated in this tutorial.

After the "memo.Lines..." line, add...

buPlay.enabled:=true;

... so that the "Play" button is "turned on" once the memo has been filled with data.

We are not turning the "Save" button on here because until the data in the memo has been changed, there's no reason to save what's in the memo.

Let's take care of a detail: Writing the code behind the "Quit" button.

Double-click on the button. That should give rise to....

procedure TDD74f1.buQuitClick(Sender: TObject);
begin

end;

Put....

close;

between the "begin" and "end;" Don't, this time, use application.terminate, which I often use. That's okay for crude shutting down, but in the program we're developing here, there are going to be some refinements which application.terminate will not respect.

=====

Whew. Well, at least we are started.

To come:

"Playing" the "game"... simple enough: the computer will....

Also to come:

Saving the memo back to the external file...

a) Whenever the user clicks the "Save" button. (Immediately after a manual Save, buSave.enabled will be set back to false.)

b) When the user exits the program. There will be some annoying odds and ends to attend to here. The "Save-on-exit" can be skipped if, when the user exits, buSave.enabled is false, which will arise if the user has done a manual save just before invoking a Quit. Also, we will explore giving the user the option to decline a save.

====

So... playing the game....

It pays to think ahead before you start typing code. The list of what we need to do to "play", presented a moment ago, was carefully considered. I tried to be sure that everything needed was in the list, and that I knew how I'd do each thing in the list, before I started typing code.

Long before I tried to write the list presented a moment ago, I'd created another list. It said that in this application, we would....

That first list was couched in general terms. Any parts of the plan I wasn't confident I could do had to be examined, perhaps by a list like the one for "What is meant by "Play"?" before coding could safely be started.

I also wrote down quite carefully what would be in the data before starting. What you want to do informs what you need, and what you need informs what you need to do. Getting competent at identifying needs and solutions is an art that you master through practice and mistakes.

Also note: We will go through the "Play" steps in sequence. This is nice because it is simple. The larger plan doesn't permit the luxury of knowing what will happen when. We may want to save at almost any time, and we may want to quit before we've done the first save. For fairly simple situations like the application in this demo (simple?!), we can get away will a little sloppiness of this sort in our planning. State diagrams.... a story for another time... are a tool for managing more complex situations where you won't get away with being sloppy.

===

Having planned the playing, having defined in some detail what we mean by "playing", we can now proceed to write the code....

Create the following new global variables by adding to the "private" block. I'll explain what they are for as I use them. Global variables are generally speaking a Bad Thing. Try to avoid needing them.

  private
    { Private declarations }
    wMaxQNumber:word;
    bFirstQLine:byte;
    bQID,bHistory,bAns:byte;

And, just after... "memo1.Lines.LoadFromFile('DD74questions.txt');"

... in...

"procedure TDD74f1.buLoadMemoClick(Sender: TObject);"

... add...

wMaxQNumber:=3;
bFirstQLine:=1;
bQID:=2;
bHistory:=5;
bAns:=2;

The numbers in these variables will not change during the program's execution. They will be used the way constants are used. You could, in fact, use a 5 anywhere in the program that I am going to use bHistory. But your code would be....

a) Harder to read b) Harder to modify.

bHistory will tell the program how many characters in each line are used to store the user's performance on the question. If I choose to give it "a better memory", change it so it remembers the user's past, say, 8, answers, I only need to change the one line...

bHistory:=5;

... and, if I've done things properly, everything else will take care of itself.

Do we WANT to change how many previous answers the program remembers? Maybe not... but it costs nothing to build in the opportunity.

The other two variables, wMaxQNumber and bFirstQLine are used as follows...

wMaxQNumber tells the program how many questions are in the questions datafile. You might think that the number of lines would tell the program this, but it is often wise to include header lines in a datafile. We're not going to do this for our simple demonstration program, but a more realistic questions datafile might look like....

Questions for DD74
version 20 Dec 2008
(c) TK Boyd)
2,5,2
q1nnnnn05What is 14-9?
q2nnnnn0aWhere is Paris? a-France, b-Germany, c-India
q3nnnnn48What is 8 times 4?

In this case, wMaxQNumber would still be 3. (We're counting as humans do, "1,2,3...", not as computers do, "0,1,2...". We're doing this for a reason, not just to be human friendly.)

However, in this case, bFirstQLine would be 5. (Again, the first line of data (the header line "Questions...") is called "1".)

What do you think the 4th header line might be for? That's right: To tell the program values for bQID,bHistory,bAns. You can build flexibility into your programs, with a little work.

However, today we are creating just a simple demonstration program. We'll hardcode the values into the five variable just discussed, and leave them alone. In a "real" question giving program, we would almost certainly at least want to be able to change the number of questions in the questions datafile!!

===

Speaking of constants: Near the top of the program, just before "type TDD74...", insert...

const ver='1.0.0';

Things to note:

a) "ver" for "version". You can use either, or something else. b) I prefer dates to designate version, but for this essay "1.0.0" makes sense. c) The equals sign is correct, strangely enough. (Not ":=")

Once you have defined "ver", then, in "procedure TDD74f1.FormCreate...", insert....

DD74f1.caption:=DD74f1.caption+', version:'+ver;

... and when the application runs, you'll see something useful in the title bar of the main window.

While we're dealing with little odds and ends, put {+R$} on the first line after {$R *.DFM}. (The one you're adding turns on range checking; the one that was there tells Delphi where to find resources.)

===

Back to work...

Now that we have the necessary "constants" filled, we can begin working on fetching a question, presenting it, etc.

Double click on buPlay, and modify the buPlayClick handler as follows....

procedure TDD74f1.buPlayClick(Sender: TObject);
var sRecord,sQID,sHist,sAns,sQ:string;
    wQues:word;
begin
wQues:=random(wMaxQNumber)+bFirstQLine-1; (*random(4) returns 0,1,2 or 3... but not 4..
    The "+bFirstQLine" deals with any header lines, and the "-1" compensates
    for the fact that the first line of the memo is numbered "0"*)
sRecord:=memo1.Lines[wQues];
showmessage(sRecord);
end;

This isn't the complete code for "Play", but run it anyway. Click "Load Memo" once, then click "Play" several times. You should see that the code we have picks a line from the file of questions (now residing in the memo), and displays it, so we can be sure about what is being put into sRecord.

Always build up your code a bit at a time. Avoid having to type in many lines before you run a test on what you have so far. Even if it means you have to arrive at your destination with numerous little excursions along the way. For instance, no sooner than we have used that "showmessage(sRecord)", but we are going to take it out again.

Do some further work on buPlayClick, making it what follows. Note that ParseRecord is within buPlayClick. It fills sQID, sHist, sAns and sQ (all local variables) with things from the line from the memo. Splitting the parsing ("breaking up" into parts) of the record out into it's own procedure makes the code more readable, and it makes it easier to alter the program if a different data coding scheme is adopted.

Before you add the new stuff in the following code, add an edit box to the form called eAns, a label called laQuestion, and a label called laFeedback.

procedure TDD74f1.buPlayClick(Sender: TObject);
var sRecord, sQID,sHist,sAns,sQ:string;
    wQues:word;

   procedure ParseRecord(sRecord:string;var sQID,sHist,sAns,sQ:string);
   begin
     sQID:=copy(sRecord,1,bQID);
     sHist:='temporaryHist';
     sAns:='tmpAns';
     sQ:='tmpQ';
   end;

begin
wQues:=random(wMaxQNumber)+bFirstQLine-1; (*random(4) returns 0,1,2 or 3... but not 4..
    The "+bFirstQLine" deals with any header lines, and the "-1" compensates
    for the fact that the first line of the memo is numbered "0"*)
sRecord:=memo1.Lines[wQues];
//showmessage('>>'+sRecord+'<<');
ParseRecord(sRecord,sQID,sHist,sAns,sQ);
laQuestion.caption:=sQ;
showmessage(sQID+chr($A)+sHist+chr($A)+sAns+chr($A)+sQ);
end;

Once you get that working, and understand it, move on to...

procedure TDD74f1.buPlayClick(Sender: TObject);
var sRecord, sQID,sHist,sAns,sQ:string;
    wQues:word;

   procedure ParseRecord(sRecord:string;var sQID,sHist,sAns,sQ:string);
   begin
     sQID:=copy(sRecord,1,bQID);
     sHist:=copy(sRecord,bQID+1,bHistory);
     sAns:=copy(sRecord,bQID+bHistory+1,bAns);
     sQ:=copy(sRecord,bQID+bHistory+bAns+1,length(sRecord));
        //The previous line is a little sloppy. It assumes
        //that copy won't return characters from beyond the
        //end of sRecord, which is protection built into
        //copy, I believe.
   end;

begin
wQues:=random(wMaxQNumber)+bFirstQLine-1; (*random(4) returns 0,1,2 or 3... but not 4..
    The "+bFirstQLine" deals with any header lines, and the "-1" compensates
    for the fact that the first line of the memo is numbered "0"*)
sRecord:=memo1.Lines[wQues];
//showmessage('>>'+sRecord+'<<');
ParseRecord(sRecord,sQID,sHist,sAns,sQ);
laQuestion.caption:=sQ;
showmessage(sQID+chr($A)+sHist+chr($A)+sAns+chr($A)+sQ);
end;

That pretty well takes care for the first two steps of "Play", i.e.

Now we're going to have to "scare the horses" with some code that might not make sense immediately. This is because Windows is an event driven environment.

First add four global variables. Do this by putting...

sQID,sHist,sAns,sQ:string;

... just before....

public
  { Public declarations }

... near the top of the code.

Take those four variable OUT of the set of local variables set up a little while ago with....

procedure TDD74f1.buPlayClick(Sender: TObject);
var sRecord, sQID,sHist,sAns,sQ:string;

... by making it....

procedure TDD74f1.buPlayClick(Sender: TObject);
var sRecord:string;

Then, in "TDD74f1.buPlayClick...", just after....

laQuestion.caption:=sQ;
showmessage(sQID+chr($A)+sHist+chr($A)+sAns+chr($A)+sQ);

... add...

buPlay.enabled:=false;
eAns.text:='';
eAns.setfocus;

Put "//" in front of the "showmessage" to "rem it out" (disable it).

Don't run the application at this stage.

Click on the edit box "eAns" and use the Object Inspector to bring up a skeleton eAnsKeyPress procedure. (You do this by selecting the "Events" tab, and then clicking in the cell to the right of the cell with "OnKeyPress" in it.) Fill the skeleton in as follows:

procedure TDD74f1.eAnsKeyPress(Sender: TObject; var Key: Char);
var sMassagedAns:string;
begin
//First turn things like 5 into 05, a onto 0a
sMassagedAns:=eAns.text;
if length(sMassagedAns)=1 then sMassagedAns:='0'+sMassagedAns;
//If user has pressed enter key (13) then see if user's answer matches
//       right answer
if Key=chr(13) then begin
  if sMassagedAns=sAns then laFeedback.caption:='Previous question answered CORRECTLY'
     //no ; here
     else  laFeedback.caption:='Previous question NOT answered correctly';
  buPlay.enabled:=true;
  buPlayClick(self);//and run "Play" again.
  end;
end;

The really scary thing in this is the line saying "buPlayClick(self);

It isn't essential to understand the "self" bit.

What this line does is equivalent to the user clicking on the "Play" button.

The "Play" procedure ran once because the user clicked on the Play button. Once this has happened, as often as the user answers the question generated and displayed by the "Play" procedure, the "Play" procedure is executed again.

Strange, and a little hard to get your head around...but it works!! Mainly because something OTHER THAN "Play" is invoking "Play", and only when the Play procedure is not already executing.

I must admit a lingering guilty disquiet. We are STARTING buPlayClick from WITHIN eAnsKeyPress. The system may be "waiting" for ever increasing numbers of instances of eAnsKeyPress to finish, as we progress down the ever receding hall of mirrors.... but I THINK what we've done is okay. If an expert who knows more about this than I do could contact me with either good news, or bad news, or help with how to do what I want to "properly", that help would be very welcome. An email subject like "DD75 tutorial hall of mirrors" would be appreciated.

One last bit of my confusion to share with you.... It may be that all of the above works just fine because each time we call the buPlayClick procedure, it runs to full completion. If it does that, then we aren't going down the hall of mirrors.... are we??

Returning to the "self" argument again, for a moment: Any time you are using this "trick" to "fake" the clicking of a button, you can put ("self") after the buWhatsItClick. (Without SOMETHING suitable, the application won't run. The compiler will say "Not Enough Actual Parameters".)

N.B. When testing this, you will frequently get the same question presented twice in a row. It will look like it "isn't working", that you "don't have" a "new question". For a real world application, you can do things with another global variable around the "wQues:=random..." line to prevent the same question appearing twice in a row... but we have other fish to fry here.

Before we move on, a small point. The program as presented so far will present the same questions, in the same ("random") order each time you run the application. Put....

randomize;

... in, just before the "end;" of the FormCreate procedure to fix this. (Remming it out can sometimes be useful, if you want to re-create something that arises from a particular sequence of questions.)

===

We've already got the application presenting questions to us, and marking our answers. We're not, by the way, going to add a score-keeping function to this. What we aren't doing... yet... is keeping track of users histories. We are now going to implement that. The program will "remember" whether the user's answers were right or wrong, going back 5 attempts for each question.

That information will be held in the five characters which start life as "nnnnn", which is to say the user has not attempted that question yet. After the user has done the question once, if his/ her answer was right, the five characters will be +nnnn.

Those characters will be part of the memo. Eventually we will add further code to the application so that at the end of a session what is in the memo gets written onto the disc, so that the next time the user starts the application, it will "know" how the user did previously.

A little aside to those who are wondering why we care: Imagine a application which can ask the learner the questions the learner has trouble with, and not waste the learner's time with things he/she knows. A very clever application will even sprinkle in questions the learner DOES know the answer to, if the learner has been having a discouraging run of "WRONG"s.

Back to programming this masterpiece...

First a very slight change to procedure TDD74f1.eAnsKeyPress...

Two "begins", two "ends" have been added, to allow more than one simple statement to be executed upon the "true" or "false" cases of "if sMassagedAns=sAns...". Also a new local variable, boWasRight, has been added, and two lines involving it. It has not... yet... been put to any use. That will come!! Run the following, making sure it works in its limited way....

procedure TDD74f1.eAnsKeyPress(Sender: TObject; var Key: Char);
var sMassagedAns:string;
    boWasRight:boolean;
begin
//First turn things like 5 into 05, a onto 0a
sMassagedAns:=eAns.text;
if length(sMassagedAns)=1 then sMassagedAns:='0'+sMassagedAns;
//If user has pressed enter key (13) then see if user's answer matches
//       right answer
if Key=chr(13) then begin
  if sMassagedAns=sAns then begin
       laFeedback.caption:='Previous question answered CORRECTLY'
       boWasRight:=true;
       end //no ; here
     else begin
       laFeedback.caption:='Previous question NOT answered correctly';
       boWasRight:=false;
       end;
  buPlay.enabled:=true;
  buPlayClick(self);//and run "Play" again.
  end;
end;

Now we'll put the value in boWasRight to use. All of this could have been squeezed into the "then"/" "else" clauses... but I think the following is more readable, easier to follow.

We must once again "promote" a local variable to global status. This time it is wQues, which was originally local to buPlayClick. Take it's declaration out of the "var" after "procedure TDD74f1.buPlayClick...", and put it just before the....

public
  { Public declarations }

... near the top of the code.

Just before the buPlay.enabled:=true;, two lines before the "end" before the "end" of the whole eAnsKeyPress procedure, add....

sHist:=copy(sHist,1,bHistory-1);
if boWasRight then sHist:='+'+sHist //no ; here
              else sHist:='x'+sHist;
memo1.lines[wQues]:=sQID+sHist+sAns+sQ;
buSave.enabled:=true;

===

And finally... saving the contents of the memo back to disc, either manually, or upon an effort to exit the application.

First we need to be able to tell if the memo NEEDS saving. The state of buSave's enabled property to meet all our needs, if we manage it properly.

As we built the program to this point, we made sure that we...

a) Made buSave.enabled FALSE when the application starts. This we did with the Object Inspector.

b) Set buSave.enabled to TRUE any time we revised the user's performance history in the memo. (Consequently, we set it TRUE again and again... when usually, it was already be true, but this is a harmless and simple way to accomplish things we need.)

Double click on buSave. Make the procedure that comes up be....

procedure TDD74f1.buSaveClick(Sender: TObject);
begin
memo1.Lines.SaveToFile('DD74questions.txt');
buSave.enabled:=false;
end;

Again.. in the real world, all sorts of tedious things should be provided for. But for the purposes of this tutorial, that's all you need!

Nearly there!!!!

====

All that's left is to cover the possible circumstance of a user trying to quit the program when there is new data in the MEMO which has not yet been saved TO DISC.

In the early days of computing, it was quite easy to do some work, and turn to a different task without remembering to save the work you had just done. Not only were those computers not capable of doing more than one thing at a time, they didn't ask you "Hey, before you shut down what you were doing, wouldn't you like to save the work you did?" They just shut down, and about 2 seconds later you would realize that you were facing re-typing what you had just inadvertently discarded. When the "The text in the document has changed. Do you want to save your changes?" question started popping up to save us from ourselves, many good vibes flowed towards the programmers responsible. (Now of course, we have uber complex machines that turn themselves off from time to time, taking our work with them, but at least the problem has changed.)

So! How do you program that into your own creations?

First you need to put something into the program to monitor whether a (re-)save of the data is necessary. As previously explained, in the program we've developed over the course of this tutorial, the state of the enabled property of the "Save" button will tell us. All we need is....

if buSave.enabled=true....

... in the right place, and we're set.

First make sure there are no "application.terminate"s in your application. (They're fine for simple shutting down, but they will circumvent the provisions we are about to make.)

Next you need to create a handler for the main form's OnCloseQuery event. Do this in the usual way, via the Object Inspector.

For a start, make that handler....

procedure TDD74f1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
CanClose:=true;
if buSave.enabled=true then CanClose:=false;
end;

If you run the program now, it may look like you've "broken Windows". Even the little red "X" in the window's upper right seems to do nothing if the Save button is enabled, which it is at any time that what's in the memo disagrees with what's stored on the disc. Less surprisingly, your "Quit" button "doesn't work", either. The other Windows-wide way of shutting an application.... Alt-F4... won't "work" either. Actually, you haven't "broken Windows". You've just created, essentially, a situation in which any close attempt is canceled almost as soon as it is started.

You should never let a program get like this. If you want to allow it to refuse a command, you should arrange for a polite message saying "I know you asked me to... but...".

You CAN close the program two ways.

If you click on the Save button, and then, while the memo and the information on the disc still agree, ask for a shutdown (by the red x, or by the Quit button), the program will close. this would be the best way to close it.

Assuming you started the application running from within Delphi, you can also stop it by using Run ! Program Reset. This is sometimes necessary when there are certain kinds of bugs in your code.

What's happening? WHY won't the program close if buSave,enabled is true?

When you invoke the main form's close procedure, as you do....

a) if you click on the little red "X", b) do alt-f4 when the application has the system's focus, or c) if you click on your Quit button when you've programmed

... then, without any extra code from you, during it's attempt to close down the application, Windows executes whatever code is in the form's FormCloseQuery procedure. One of the arguments of that is a "var" argument... it passes something back to whatever calls the procedure. In this case, it passes back "true" or "false", and you can determine which will be passed back.

If "false" comes back from the OnCloseQuery handler, then the closing of the application is aborted.

Have another look at the code you probably just copied and pasted a moment ago. See that variable "CanClose"? I've made assignments to it, but I didn't create it. It was part of the basic "FormCloseQuery" skeleton... with the important "var" that you can see in the first line of the procedure's code.

buSave.enabled will be true when what's in the memo differs from what's on the disc... that's why when it is true, CanClose is set to false. After FormCloseQuery finishes, and hands control back to "Close", which called it, the "false" returned by FormCloseQuery will cause "Close" to abort.

Let's make our FormCloseQuery work more sensibly. We need it to tell us WHY our form won't close, and, probably, give us the option of quitting anyway. (If this program were being used, say, in a school, and the teachers did not want pupils to have the option of discarding any of their performance history, then the option could be left out, or the system could simply be told to automatically do a save whenever the pupil tried to exit the application.)

The MessageDlgPos procedure is just what we need. Here's a much improved FormCloseQuery...

procedure TDD74f1.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
var wTmp:word;
begin
CanClose:=true;
if buSave.enabled=true then begin
  //see accompanying tutorial...
  wTmp:=MessageDlgPos('Performance history has changed.'+chr(10)+
     'Do you want to save the newer data?',
     mtConfirmation, //type of message box
     mbYesNoCancel, //Buttons / return values
     0, //Help context. (Not used here)
     200, 200);  //X/Y of message top left. Screen coords.
  //end "see tutorial"
  if wTmp=mrYes then buSaveClick(self);//Save the data, and leave CanClose true.
  //if wTmp=mrNo then ... do nothing. CanClose will remain true, we close without saving
  if wTmp=mrCancel then CanClose:=false;//This will block attempt to close program
  end;
end;

Don't worry! It's not as bad as it looks!

Remember: FormCloseQuery is called by the Close procedure, and if FormCloseQuery returns "true", the close happens. If "false" is returned, the close doesn't happen, and, unless we've programmed something, nothing has changed.

So what's going on in all that code? Run the program a few ways, looking at the following in conjunction with your experiments.

First we make CanClose true. This is just an initialization. We can change it to false before the procedure is over.

Then we have the MessageDlgPos function call. MessageDlgPos is built into Delphi. you don't have to make it... just learn what it can do for you. Bottom line? It will put a number in wTmp.

As soon as the call to MessageDlgPos is made, a "little window", a DIALOG, appears on the screen. The first parameter....

'Performance history has changed.'+chr(10)+
     'Do you want to save the newer data?',

...specifies the text to appear in the dialog. The chr(10) makes a newline happen.

The second parameter (mtConfirmation) specifies the sort of box to appear... one with a question mark? An exclamation mark? Etc. (See the Delphi help file entry for the function.) (Many of these things, e.g. mtConfirmation, are also "built into" Delphi.)

The third parameter specifies the buttons to appear on the dialog. In this case, we have specified a "Yes" button, a "No", and a "Cancel". (Others that are possible: OK, Help, Abort, Retry, Ignore, All) Just make a parameter starting "mb", and tack on whatever buttons you want. E.g. the selection we've asked for (mbYesNoCancel), or, say, mbOKAbortRetry.

The next parameter, 0 in this case, is to do with where you go in a help file if you ask to get help. See Delphi help file if you want to use this.

And lastly, two numbers to say where the dialog should apear on the screen. Note that the numbers are relative to the SCREEN'S upper left. You can do clever things to make the dialog appear at a specific place on the FORM, but they aren't necessary to this story.

So... that's the lowdown on all the parameters. As I said, when you call the function, a dialog appears. You then click on one of the buttons (or press the relevant key), the dialog goes away, and a number is put into, in this case, wTmp.

That's most of what you need to know about MessageDlgPos. One thing remains: What numbers does it return when you click the buttons? I don't know! I don't NEED to know. Look at the code following the call to MessageDlgPos.

If I clicked on the Yes button, then wtmp will have a number in it which is also in the constant mrYes. Just stick "mr" (message return?) in front of the button's name, and you're all set.

For our program, if wTmp has mrYes in it, we just invoke the code for saving the data. We simply re-cycle the procedure that buSave calls. And we don't need to do anything about CanClose, because it is already set to true, and when everything in FormCloseQuery has run it's course, we'll send true back to "Close", and it will close the application... but we saved the data during FormCloseQuery, so nothing has been lost.

If wTmp has mrNo in it, it means the user didn't want to save the new data, but still wants to close things down. By doing nothing when wTmp equals mrNo, we leave CanClose as true, and the application closes.

And lastly, if wTmp equals mrCancel, it means the user doesn't want to quit after all. Nor does he/ she want to save the data. By changing CanClose to false, we are setting the stage to pass false back to "Close", which will, because of the false, simply abort, and leave the user where he or she was before asking for the program to be closed.

====

That's it! I hope you know things you didn't know before!


To search THIS site....
Click this to search this site without using forms, or just use......
powered by FreeFind
Site search Web search
Be sure to spell the words you are searching for correctly!
The search engine doesn't understand English. Searching for "How do I use repeat" will just return pages with "how", "do", "I", "use", or "repeat".
(Go to my other sites (see bottom of page) and use their search buttons if you want to search them.)...


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... 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 an MS-DOS or Windows 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 email or write this page's editor, Tom Boyd




Valid HTML 4.01 Transitional Page TO BE tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org


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 .....