This tutorial has... mostly... been superceded. My Sept 2020 tutorial on using secondary units in Lazarus coding may suit you better... but it lacks the material on using records that is in this, near the end. Start with the other one?
If you work through this, you may find yourself feeling "What's the point?"
Certainly, I do not expect that many people will be very interested in using the unit I am going to create in the course of the tutorial.
The point of the unit is to take you through some mechanisms which are available to you in the Lazarus environment.
So... that's why I say that the question of "what would you use the unit for?" is not the point. Now that's out of the way, here's what the unit does...
The unit which is the subject of this tutorial might, hypothetically, be useful to someone who was writing a lot of programs where currency exchange rate calculations were needed. (Yes, you could do what we are going to do with "in-line" subroutines. But then you wouldn't learn about putting subroutines in a unit, would you?)
It will be a little while before we come back to "units", bear with me?
We'll start with a simple program to convert US dollars ($) to British pounds (£)
Always start with something basic, and build it up.
For a crude, imperfect currency converter, all you need is an edit box and a label...
The edit boxes OnChange event needs to be handled by...
procedure TLDN029f1.Edit1Change(Sender: TObject); var rtmp:real; begin rTmp:=strtofloat(edit1.text); label1.caption:=floattostr(rTmp / 0.6666666); end;
The main problem with the above is that it is entirely non-tolerant of anything but numbers in the edit box.
For now, and for a long time to come, within this tutorial, avoid entering anything other than digits (or using the delete/backspace keys... and even then, don't let the edit box get empty!)
If you accidentally press a non-digit, or make the box empty, the program will crash. Just "break" out of the program (click "break" button of dialog) and "stop" it (button on toolbar, or ctrl-f2).
Also, if you put a non-digit in the edit box, a new tab will open in the source editor, probably labeled "customedit.inc". Right-click the tab label, and invoke "Close Page".
the program, even in it's simple form, will convert pounds to dollars! If you put 100 in the edit box, the label will immediately show (nearly) 150, which is, about, the number of dollars you can buy for 100 pounds.
Yes, I would usually convert pounds to dollars by multiplying by 1.5, but dividing by 0.6666 is equally valid... and there is a reason I did it that way which isn't important to your learning experience, and which you will probably see in due course anyway.
Yes, the program should be "fixed" to present the answer with only a reasonable number of decimal places. Trivial... but "important", if you want to call the result anything but crude. I don't want a polished result. I want to show you other things.
More importantly, the program needs a way to alter the exchange rate, so it can reflect the real rate on the day that interested you. (The "0.666666" in the code above sets the exchange rate used by the program.)
Also, any decent exchange rate calculator would let you enter a number into either the "pounds" box, as you can do in this case, or into the "dollars" box, which you can't do here, seeing as the number of dollars is shown on a label. That fancier answer is again a question for another day.
We're now going to start building a better answer. At first, sometimes the way we are doing things will seem like overkill. But we are "erecting the scaffolding" at this stage. Using a subroutine to set the exchange rate we are using is overkill. But as our application becomes fancier, we may be glad to have a place where all initialization tasks occur.
In the application's OnFormCreate handler, we're going to have a function to INITialize all matters relating to FOReign EXchange calculations...
bTmp:=InitForEx(0.66666);
For now, the value in the "scratch" global variable bTmp will be meaningless... but...
We have set InitForEx up as a function, so that it can return a value to the calling code. That value will be used to send error messages back from the InitForEx subroutine's code.
At this stage, InitForEx is going to consist of...
function TLDN029f1.InitForEx(rTmp:real):byte; begin FXrRate:=rTmp; result:=0; end;//InitForEx
You'll see a new variable appearing in that... "FXrRate".
That's variable intimately connected with the ForEx calculations, hence the FX prefix. The third character, the "r" is to say that this is holding data of the "real" "type". And "Rate" because it holds the rate we want used for our currency conversions.
It is... for now... simply one of the program's global variables. And, for now, overkill.
The OnEdit1Change handler has to be tweaked, very slightly... "0.66666" needs to be replaced with "FXrRate", as shown...
procedure TLDN029f1.Edit1Change(Sender: TObject); var rtmp:real; begin rTmp:=strtofloat(edit1.text); label1.caption:=floattostr(rTmp / FXrRate); end;
If you were to run the code, as described so far, you wouldn't see any changes in its behavior. But the way the code is written is getting better. "Better" in that, as more complexity is added, keeping track of what's going on will not become impossible to discern.
Now we're going to take something we had before, and just "parcel it up". We aren't changing any code, not even by the tiny bit we "changed" the way the calculation was done, when we moved 0.66666 into a variable.
Instead of...
label1.caption:=floattostr(rTmp / FXrRate);
... we're going to have...
label1.caption:=sCalcUSD(rTmp);
... which, I hope you realize means that we will be adding a function...
function TLDN029f1.sCalcUSD(rTmp:real):string; begin result:=floattostr(rTmp / FXrRate); end;//sCalcUSD
(The "s" prefix on the name is meant to imply that the function returns a string.)
No surprises in that, I hope? If there were, re-read the above, until it seems uncomplicated to you.
There are reasons which I hope you do not already grasp... I will go into them later.
But even before we come to them, I hope that you can, if only by simple intuition at this point, see that...
label1.caption:=sCalcUSD(rTmp);
... is just more clear than
label1.caption:=floattostr(rTmp / FXrRate);
... after you "get your head around" the idea that the function "sCalcUSD" returns a string which expresses the value of the pounds in US dollars. We have taken the details of how the program gets the figure, and put them "on one side", put them "away, out of sight", so that we can concentrate on bigger issues in the neighborhood of...
label1.caption:=sCalcUSD(rTmp);
.. and not be distracted by details
The details remain available. The conversion from a string showing the pounds we had to a string showing the equivalent in dollars can be improved. We can limit the answer to however many decimal places we want... but that again will be something that happens inside the function (sCalcUSD). Those details won't be cluttering the part of the program were we need to know the number of pounds. Nor will why we want the figure in pounds be cluttering the part of the program where we create the string.
Finding good names for your subroutines is an art that you must continually try to become better at. "sCalcUSD" must "mean" "Calculate that in dollars, return as string" to the person working with the code. (You'll find it is easier when you are working with your own code, where you have chosen the names.)
I'm leaving the extra code to trim superfluous decimal digits as an exercise for you. It isn't important to this topic of using subroutines, and using subordinate units... which is what we are just about to come to.
So far, so good, I hope.
Now we're going to take the little program we've built so far, and just re-arrange the bits some. We won't be adding code. But we will be adding a unit... a "container" for some of the code we've already made.
Invoke, from the main menu bar of the Lazarus IDE, "File / New Unit".
In the window where your code was appearing previously, you now have a new tab. It is called "unit1" so far. Save it as "forexsrs" (for "FOReign EXchange SubRoutines"), and you will change the name, as well as doing the save we needed.
Add "forexsrs" to the "uses" clause at the top of unit ldn029u1;
Try running the program. Not a lot will have changed, but you shouldn't get any "complaints". (I will say "Run that" several times in the near future. Those two words should imply to you what I said at the start of this paragraph. I will suggest "Run that" at suitable points, so that you can be sure typos have not arise.
Take the "FXrRate:real;" out of ldn029u1, and insert it into "forexsrs", along with a var above it, just before the "implementation" in "forexsrs".
Run that.
So far, nothing very major.
Fancier...
We're going to take the "forward declaration" of InitForEx and the code saying what is wanted, the implementation, out of the program's main unit, ldn029u1, moving it to the forexsrs unit.
Take the line...
function InitForEx(rTmp:real):byte;
.. which was in the interface section of ldn029u1. Move it to just above the "var" you recently added to the "forexsrs" unit. (This is within the unit's interface block, which was also the location of the line within ldn029u1).
Now... you must also move the code...
function InitForEx(rTmp:real):byte; begin FXrRate:=rTmp; result:=0; end;//InitForEx
This goes into the forexsrs unit just after the word "implementation"... but you must make a small change. Did you spot it? What you have above is what should be left in forexsrs when you've finished the cut/ paste/ edit.
Originally, the first line of that read...
function TLDN029f1.InitForEx(rTmp:real):byte;
Run the code.
It should still be running, as far as the user is concerned, as it ran before.
But we have improved our code. It is, once you get used to what is going on, easier to read. We'll look at this more fully in a moment.
In a similar way, move function sCalcUSD(rTmp:real):string; (and its code) from the main unit to forexsrs.
Despite all the "fluff" it entails, the time has come to show you a full listing. It is in two parts, first what is in the main unit ("u1") of this Lazarus Demo (New series), serial 29: "ldn029u1"...
unit ldn029u1; {$mode objfpc}{$H+} interface uses Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, forexsrs; type { TLDN029f1 } TLDN029f1 = class(TForm) Edit1: TEdit; Label1: TLabel; procedure Edit1Change(Sender: TObject); procedure FormCreate(Sender: TObject); private { private declarations } bTmp:byte; public { public declarations } end; var LDN029f1: TLDN029f1; implementation {$R *.lfm} { TLDN029f1 } procedure TLDN029f1.FormCreate(Sender: TObject); begin bTmp:=InitForEx(0.66666); end; procedure TLDN029f1.Edit1Change(Sender: TObject); var rtmp:real; begin rTmp:=strtofloat(edit1.text); label1.caption:=sCalcUSD(rTmp); end; end.
And this is what is in the support unit we have created, "forexsrs", a unit with FOReign EXchange SubRoutineS. (I really dislike the Linux aversion to upper case letters in names! But haven't yet embraced the underscore. Some would say for_ex_srs would be a better name.)
unit forexsrs; {$mode objfpc}{$H+} interface uses Classes, Dialogs; function InitForEx(rTmp:real):byte; function sCalcUSD(rTmp:real):string; var FXrRate:real; implementation function InitForEx(rTmp:real):byte; begin FXrRate:=rTmp; result:=0; end;//InitForEx function sCalcUSD(rTmp:real):string; begin result:=floattostr(rTmp / FXrRate); end;//sCalcUSD end.
A moment ago, I said that we'd improved our code... even though what the user would see was entirely unchanged.
By moving "stuff" to forexsrs, we've created something which, once you get used to what is going on, is easier to read. And when it is easier to read, you can devote more of your little gray cells (brain) to making the program do wonderful things, and have less of your gray cells distracted by trying to stay on top of what you've done so far.
It's a bit like the circus performer with spinning plates on sticks. Anyone can spin one plate on one stick. When you become good, maybe you can do two. If your code isn't difficult to read, maybe you can "spin" lots of "plates".
And when it is easy to read, it is hard for bugs to hide from you.
The HEART of our code, at the moment, is now in the OnChange handler for the edit box...
begin rTmp:=strtofloat(edit1.text); label1.caption:=sCalcUSD(rTmp); end;
If you don't happen at the moment you have to look at this, which may in time be part of a much larger project, all of the details of sCalcUSD() are available to you... but they are tucked away, all nice and tidy, and not underfoot when your main focus is elsewhere.
You need to know... it should eventually be spelled out in rems at the top of the ForExSRs unit... that before you call sCalcUSD(), you should have called InitForEx(). But even what's involved there has been "tidied away" into a discrete, logical place. Most things you do inside a program have some kind of pre-requisites.
I've only touched on some of the advantages to using units in the past few paragraphs. Besides those, if are logical when you are "distributing" what goes into the units you create... you're not restricted to just one!... logically, you may find that something you created for one program may be useful, more or less untouched, for another. And, if it is in a well designed unit, it "transplants" easily. Once you master creating units within a project, it is a small step to creating units which can be "plugged into" a project, without even putting the code for the unit in it. (If you write HTML "by hand", you may be aware of something similar there: the "definitions" (my attempt at a name) "behind" the magic of CSS can be "in-line", inside the page you are viewing, or they can be in external files which the page you are viewing "incorporates by reference". Lazarus units, as done above, are like the "in-line" CSS, and there's an easy way to set things up to "reference" what a unit can supply "by reference".)
You may have noticed that we don't, within the subordinate unit, interact directly with any global variables. Neither do we do anything on the form managed by the main unit, or give rise (directly) to anything a user would see, for example a pop-up dialog. Not even something as simple as the results of a "showmessage()".
This is deliberate. You can cheat, an, say, put at showmessage() in the subordinate unit. But it will come back to haunt you! Especially when, as you will, if you "walk" for a while before you try to "run", start using "standard" units (of your creation) across multiple projects.
An aside: (You don't need to follow every word of this. There's an "end of aside" marker at the end, at the point where you continue skimming at your peril.)
So far, we have been talking about a program to deal with a fairly trivial "need". Here's a quick example "from the real world" of something I use a standard (but created by me) unit for. I frequently want to turn dates into a much under-appreciated format... and back again to the more traditional representations.
I would show 2nd December, 20 15 as 15c02... in essence, that's yymdd, where there's always SOMETHING for every letter, so leading zeros may be involved, as in showing the second of the month as "02", not just "2", and the month is a, b or c for October, November, December.
Just accept that ** I ** want to be able to use that format. (It IS neat, but that's a story for another day.) For now, just admire how EASY it is for me, because of a unit I created...
Oh! And sorry for the distraction, if that's what it is, of the "record" type variable. Just skim over that for the moment. In a crude nutshell, a record-type variable lets you pack multiple "things" into one "parcel". (We'll talk more of creating and using user-defined recordss at the end of this tutorial.)
Just by tacking my unit onto any project of mine which needs this, I can use a created- by- me RECORD TYPE, "TDmyInNumbers", which has three fields: Yr, Mnth, Day.
sTmp:=sDmyToMyFormat(2,dec,2015)
and
var mrtTmpDmy:TDmyInNumbers; mrtTmpDMY:=mrtDmyFrm(15c02);
The first takes a date in an everyday form, split across three elements, and converts it into a short string, representing the same date, in "my" format. (The "2", "dec" and "2015" could, of course, been supplied to the function in variables.)
The second "unpacks" a date represented in my format, giving the programmer access to the day, the month and the year in separate parts within the record-type variable. (If you did showmessage(inttostr(mrtTmpDMY.Yy)) after the code above, you would see 2016)
Whew! Aside is FINISHED!.... Skimming the text is no longer safe!
We're creating a unit, because it is a Good Idea. I've recently been saying "subordinate unit", to distinguish it from the main unit. From now on, unless I say "main unit", assume that I usually talking about the subordinate unit.
I've said that you shouldn't access global variables in the unit. Or send things to the screen.
Here are some examples of how we avoid doing that in places where we might be tempted.
We calculate the number of dollars a given number of pounds represents by dividing by a constant.
Dividing by zero is a Really Bad Idea... but at the moment, we could tell the program to try... simply by using...
procedure TLDN029f1.FormCreate(Sender: TObject); begin bTmp:=InitForEx(0); end;
(It wouldn't try to divide by zero immediately, but it would soon be doing so.)
We could modify that as follows. Remember... this is something from the main unit...
procedure TLDN029f1.FormCreate(Sender: TObject); var rTmpL:real; begin rTmpL:=0; if rTmpL=0 then begin showmessage('There is an error in the hard '+ 'coding. Search on 16512a to see what it is.'); end//no ; here else begin bTmp:=InitForEx(rTmpL); end;//of else end;
Something of a dog's dinner... but it "would do" to guard against setting the exchange rate to zero. and it does avoid having a showmessage inside our (subordinate) unit.
But what a lot of "stuff" to incorporate, and remember to incorporate, into the main unit of any program using InitForEx(). (remember- eventually, you will be re-using these units across multiple programs.
So! Instead!... inside the unit, modify InitForEx as follows...
function InitForEx(rTmp:real):byte; begin FXrRate:=rTmp; if rTmp=0 then result:=1 else result:=0; end;//InitForEx
.. and make the bit in the main unit...
procedure TLDN029f1.FormCreate(Sender: TObject); begin bTmp:=InitForEx(0.6666); if bTmp<>0 then begin showmessage('There is an error in the hard '+ 'coding. Search on 12May16a to see what it is.'); end;//of if.. then.. end;
This is so much more clear, tidy, amenable to further extension, etc.!
(By the way... we would of course, if making a real program, give the user an interactive, at-run-time way to supply the exchange rate, i.e. what we are hard-coding as 0.6666 in this simplified version of the application.)
Why did I say "search on 12May16a"? Because I was writing this on that date, and knew how many error codes I had assigned already that day. (Even I can remember (or use "find" to discover) error codes assigned since dawn.) If I needed more unique- across- all- the- programming- I- have- ever- done ID codes the next day, the first one would be "13May16a". The next 13May16b, etc.
If you feel that "bits" (apart from separating a unit from the project it first appears in) haven't been made clear yet, perhaps re-read the above. I'm going to move on now, into new pastures.
Until now, if you entered a non-digit in the edit box, or made it empty, the program did not respond graciously. We can fix that. Our fix won't be very elegant, but it will be a big improvement over what we had before!
Not only are we going to fix that, but we are also going to make things a little tidier at the highest level...
Cast your mind back to...
At the heart of our code, at the moment, in the main unit (LDN029u1) we have...
begin rTmp:=strtofloat(edit1.text); label1.caption:=sCalcUSD(rTmp); end;
We're going to make that...
begin label1.caption:=sCalcUSD(edit1.text); end;
... because it is more clear. All we need to do is to change the type of the value we pass to sCalcUSD, and move the "strtofloat" to inside sCalcUSD, which we will now implemented as follows, inside ForExSRs....
function sCalcUSD(sTmp:string):string; var sTmp2:string; rTmp:real; begin //Something about the following makes me "itch". //I suspect it could be done better! "But it works". //(The danger of an attempt to divide by zero has // been taken care of elsewhere, at least.) sTmp2:=''; try rTmp:=strtofloat(sTmp); except on exception do sTmp2:='Cannot convert'; end;//try... except... if sTmp2='' then sTmp2:=floattostr(rTmp / FXrRate); result:=sTmp2; end;//sCalcUSD
(If you aren't used to try... except... end, and exception handling, my apologies. "That stuff" is taking care of "bad" input to the edit box; it is taking care of the case when a user enters a non-digit, or makes the box empty. It is Good Stuff! (Maybe I should give it its own tutorial!)
Moving on to something which will be more fun, I hope, and something more generally useful, instead of the little details we've been getting right.
We have "broken out" of the main unit two subroutines. One calculates the value, say, of $100 in British pounds. But! Before we can call that subroutine without tears, we must have called the InitForEx subroutine.
Here is how we can ensure that we don't someday re-use the ForExSRs subroutine, but forget the need for the preliminary call to InitForEx.
First we create a boolean variable within ForExSRs. It is called boIniCalled. In the fragment below, the creation of boIniCalled only takes one line, but I've copied several lines, so you can see where that line goes.
function sCalcUSD(sTmp:string):string; var FXrRate:real; boIniCalled:boolean=false; implementation
Note the "=false" part. That ensures that boIniCalled starts its life equal to false. (This "trick" won't work if the variable is being declared globally in the main unit, as part of the class(TForm).)
We add "boIniCalled:=true;" to InitForEx
and early in sCalcUSD, we could have...q
if boIniCalled=false then showmessage('sCalcUSD called before '+ 'IniForEx called. Not allowed.');
(If we wanted to do that, we would need to have "Dialogs" in the Uses clause of ForExSRs, by the way.)
However, this would not be the best answer.
Everything I've said so far is fine, up to "then showmessage...."
We want (really!) to avoid having anything inside our subordinate unit "talk" directly to the outside world. So. How do we avoid this?
For once a "cheap and cheerful" answer will not only do, but it is not at the same time problematic. (The "showmessage" "solution" was cheerful... but cheerful and problematic!)
What we will do, instead of using showmessage to report the problem, is that we will create a "rogue value" for the result of sCalcUSD. (Much of the code below is "old"... An "if... then..." has inserted, wrapping the old code in its "else". The bit at the top is new. And remember: We took steps to ensure that boIniCalled would be false, unless the InitForEx in the ForExSRs unit has been called.
function sCalcUSD(sTmp:string):string; var sTmp2:string; rTmp:real; begin if boIniCalled=false then begin sTmp2:='sCalcUSD called before'+chr(13)+ 'IniForEx called.'+chr(13)+'Not allowed.'; end//no ; here. else begin //Something about the following makes me "itch". //I suspect it could be done better! "But it works". //(The danger of an attempt to divide by zero has // been taken care of elsewhere, at least.) sTmp2:=''; try rTmp:=strtofloat(sTmp); except on exception do sTmp2:='Cannot convert'; end;//try... except... if sTmp2='' then sTmp2:=floattostr(rTmp / FXrRate); end;//of "else" near top. result:=sTmp2; end;//sCalcUSD
That's it! For that. "Easy when you know how."
When we create sub-units like ForExSRs, we try to avoid having them talk to the outside world... and it can be done, without great hassle. We don't want them popping up ShowMessage boxes, etc. We certainly don't want them directly accessing, to read or to write, global variables outside the scope of the unit. We don't want them fetching values directly from edit boxes on the main unit's form, not writing directly to them.
If these rules are observed, it becomes much easier to "re-cycle" the units... a great "bonus" which is yours, on top of the value of the sub-units towards making the main unit uncluttered, easy to read, easy to work on.
Take, for instance, in our simple exchange rate calculator example: If we sent the "answer" directly to the form, then the sub-unit would only be useful when used with a main unit which created a form with a label of the right name.
I am new to using sub-units. I have to confess that the want which inspired this tutorial seems to break my rule. The unit I want to write will access a file of data on the hard drive. But the specs of WHICH file will be gathered in the main unit. And all access to the file will be within the sub-unit. So I think the virtue of portability between programs will be maintained. I'll know more when I'm done!
I always include in any unit a function which returns a string. The string identifies the version of the code. It can be as simple as putting the following in the implementation part of the sub-unit...
function sVerForExSRs:string; begin result:='16 May 16'; end;
(And you put "function sVerForExSRs:string;" in the interface section of the unit as well, of course.)
And now, if you create a button on the form managed by the main unit of the program which says...
showmessage('Version of ForExSRs unit used in this is: '+ sVerForExSRs);
... you have a way when running the program to see which version of the units was used to compile the program. Of course, this simple answer assumes that you can be sure that the line in the sub-unit will be changed each time you make any changes to the sub-unit.
I usually maintain a small program which merely exercises the functions of the sub-unit, and take a fresh copy from within that whenever starting a new project which re-cycles a unit. (This practice helps me keep relevant improvements to the code propagated across all of the programs using it.)
Beautifully written code doesn't need a lot of documentation. But there are always blemishes in our code, and documentation is a Good Idea anyway!
So, at the head of the sub-unit, be sure to write up what the sub-unit is for.
In the implementation section, at the start of each subroutine, add notes, e.g....
function InitForEx(rTmp:real):byte; //Establish the exchange rate which will be used // in calculating equivalents //Set the flag which says that this subroutine // has been executed. begin FXrRate:=rTmp; if rTmp=0 then result:=1 else result:=0; boIniCalled:=true; end;//InitForEx
It also pays to add comments explaining what your different variables are used for, what limits exist on values they may have. In the case of the example unit, for instance, it would be wise to put next to the declaration of FXrRate that it must not hold zero.
A while ago, we programmed InitForEx to return 0 if no error was encountered during the initialization process, and to return 1 if a user tried to set the exchange rate to zero. We make such a request an "error", because the way the program is written means that we divide another number by the exchange rate, and mathematics doesn't allow dividing by zero.
While what we have is adequate, and it is little trouble to use, if we have access to the code behind the sub-unit ForExSRs, we can make things even easier.
What I am about to discuss is probably over-kill here, but you may well find the techniques useful in real-life circumstances.
We're going to create a new functions in ForExSRs, a function return strings which will say what the error codes mean. The function will have two parameters. The first will say what error code needs interpreting, the other will select the form of the answer: a short string, with an abbreviated explanation, or a longer string, with a longer explanation.
Previously, in the main unit, we had...
procedure TLDN029f1.FormCreate(Sender: TObject); begin bTmp:=InitForEx(0.6666); if bTmp<>0 then begin showmessage('There is an error in the hard coding.'+chr(13)+ 'Search on 16512a to see what it is.'); end;//of if.. then.. end;
Now we can make that...
procedure TLDN029f1.FormCreate(Sender: TObject); begin bTmp:=InitForEx(0.666); if bTmp<>0 then showmessage('Change line in FormCreate: '+ sInterpErrCode(bTmp,true)); end;
... which, in this isolated, simple case may seem cumbersome. A larger program, with a larger set of defined error codes, would be more happily managed through a scheme like this, where all of the possible codes are in one place, in a tidy list, to be used whenever necessary.
If I were doing the above, I'd put a small function in the sub-unit to return to the main unit the highest error code used....
function bHighestUsedErrorCode:byte; begin result:=1; end;
Once that's available, we can get a list of all the error codes present in the sub-unit by putting code like the following into the main unit. I've inserted it as the handler for the click event of a dedicated button. (The following assumes a memo, meErrCodes, on the main form, which is going to be used to list the error code documentation.)
procedure TLDN029f1.buListCodeDocClick(Sender: TObject); var bTmpL:byte; begin meErrCodes.clear; for bTmpL:=0 to bHighestUsedErrorCode do meErrCodes.lines.add('Code '+inttostr(BtmpL)+ ': '+sInterpErrCode(bTmpL,true)); end;
And the function, in ForExSRs...
function sInterpErrCode(bWhatCode:byte;boShortVersion:boolean):string; //This could be done differently, but has been done like this to make // it easy to add new error code interpretations as the code in the // unit develops. //The second parameter, if true, makes the SR yield short explanations // of the error. Longer explanations are the alternative. //Boilerplate... (* :if boShortVersion then result:=''//no ; here else result:=''; *) begin case bWhatCode of 0:if boShortVersion then result:='NoErr'//no ; here else result:='No error seen during subroutine call'; 1:if boShortVersion then result:='Exch rate=zero not allowed.'//no ; here else result:='Setting exchange rate to zero not possible because '+ 'divisions by zero would arise.'; else //of case if boShortVersion then result:='Unrecognized err'//no ; here else result:='The coding of the sub-unit needs revision to handle an '+ 'as-yet un-provided for error code.'; end;//of case end;//sInterpErrCode
I am a "one man band". Only I work on any of my code. However, in a bigger organization, you will sometimes find that "team A" produces a unit for other teams to use, and the other teams are "locked out" of the details of HOW the unit does what it does... they are only told (and only care about) what sub-routines are available from the unit, how to call them, and what they will return.
I tend to write up the "how to call"/ "what is returned" just before the code, in the "implementation" section. However, you can make a good case for putting that information in remarks associated with the forward declarations of the sub-routines, up in the "interface" section of the unit.
Done properly, these remarks can save you a lot of time. It also pays to write them before you write the code. There is nothing like having a clear spec for what something is going to do, while you are writing it, to help you stay on course, do what was needed, rather than something it drifted into being.
I said that the sub-unit should not directly change the values in any global variables.
Suppose you want to a function which returns more than one value? We could, for instance, although it stretches the credulity a little, have wanted an exchange rate calculator which returned the equivalent of a GBP amount both in dollars and in, say, Euros.
We would need to add an exchange rate, similar to FXrRate. We'd call it FXrRateEuro.
Then we could create a function called sCalcDollarsAndEuros.... but it would have to return TWO strings. Other than that, it would work more or less as our earlier sCalcUSD.
How do we get it to return two strings?
If the rest of this is Just Too Much, then let it go. Even without using user-defined records (the subject of the rest of this), you can do a lot with sub-units. My apologies for rushing what follows.
I mentioned user-defined records earlier in this tutorial, and they are one of the subjects in my Lazarus tutorial "User-defined records and reading from disc files".
We use a user-defined data type, specially crafted for the needs of this program. We'll call it TFXudrTwoStrings
We create it, in the sub-unit, with the following code, inserted just after the Uses clause...
type TFXudrTwoStrings = record sDollars, sEuros:string; end;//definition of type TFXudrTwoStrings
Now we have a TYPE, TFXudrTwoStrings, which didn't exist before. How do we use it.
I'm not going to produce all the code to calculate values in dollars an euros, but I will show you something which I hope will suffice to enable you to use these clever, user-defined data types...
In the main program, we add a button, and the code for that button will be as follows...
procedure TLDN029f1.buTwoStringsClick(Sender: TObject); var udrTmpMain:TFXudrTwoStrings; begin udrTmpMain:=sCalcDollarsAndEuros; showmessage('Value returned in sDollars: '+udrTmpMain.sDollars); showmessage('Value returned in sEuros: '+udrTmpMain.sEuros); end;
the local VARIABLE we have created, udrTmpMain, has two "parts". The sDollars part, and the sEuros part. The "part-names" are fixed; they were determined by the code at the top of the subunit which creates the TYPE: TFXudrTwoStrings.
As with any other type, you can have many variables all of that type. We have on in the subroutine above.
In the sub-unit, where I have defined the function sCalcDollarsAndEuros, we have another instance of the type...
function sCalcDollarsAndEuros:TFXudrTwoStrings; var udrTmpL:TFXudrTwoStrings; begin udrTmpL.sDollars:='100'; udrTmpL.sEuros:='85'; result:=udrTmpL; end;
We "fill" the sDollars part of udrTmpL with 100... not with a calculated value, which we would do if we really wanted the program I described.
We "fill" the sEuros part of udrTmpL with 85.
And then we are allowed to say...
result:=udrTmpL;
Because udrTmpL consists of the two parts just mentioned, we can do this. We have arranged for two values ("100" and "85") to pass back to the calling code (in the main unit) from the procedure in the sub-unit.
Magic! and useful! It takes a little "getting used to", but it is how you avoid direct access to multiple variables in the main unit when a subroutine you want to have in the sub-unit computes more than one value. You can do it! It just takes a little practice.
What we've created, if you've been following along, produces something like...
The whole project, with an already compiled .exe can be downloaded in a .zip file. Download sourcecode and .exe for LDN029- Using Units in Lazarus. (Delphi does more or less the same things.) Enjoy! Feedback appreciated. What was helpful to you? What was obscure?
Search across all my sites with the Google search...
|
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.
....... P a g e . . . E n d s .....