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

Using secondary units in Lazarus

"Un-necessary"... but very helpful to code management.

filename: lt4Ng-secondaryunits.htm

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

Two separate, complete Lazarus projects (source code) are free to use with this.

The .zips also contain "ready to run" / "standalone" .exes. It won't "install"... just use it as is. One job. One file. Simples!

LDN164 is the result of the first part of this. The full sourcecode is available for download as a .zip file.

From there, you can go on to LDN165, which is what the second part of this discusses. The full sourcecode is also available for download as a .zip file.

Good for more than ostensible remit

However you have come to be reading this page, if you write programs with Lazarus, I hope you will at least skim it, because it goes into general points about Good Programming Practice. They may not make much sense at first. Why they can be called Good Ideas may not be obvious. But try them. I think they will grow on you. I was set against some of them, when they were first presented to me... but eventually, I saw the light. I hope you won't have to struggle in the darkness for as long as I did.

Using secondary units

If you haven't already been converted to the usefulness of secondary units, try adding one to hold general-purpose subroutines, ones you might want to use again in some other program. (This essay was set off by a wish on my part to write a subroutine that would check whether the xth character in a string was one of the characters in a subset of all available characters... in other words, I wanted to use the standard "pos" function... but I wanted to put it in a "wrapper" to make it easy to ask, say, is the 3rd character in "fishcakes" a digit, or 'x' or 'y'?". (No, for the example given.) I can see myself wanting that function frequently, so it goes in one of my "Generally Useful" secondary units, for easy sharing with other application code.

Using a second unit in your app will help you "divide and conquer".

If you work within certain restraints, the whole thing is EASY. It may not do everything you wanted to do... but will do useful work.

Restraints

Nothing in the second unit may directly access variables, etc, in the main unit.

Things you can do

You CAN pass values to the second unit, and back again to the main unit. All of the second unit (in the simple scenario I am presenting here) is just subroutines for the use of the first unit. Say, for instance, you want to calculate sales tax on an amount.

You'd create a simple function. To Keep Things Simple, we'll express all amounts in pennies, rather than dollars. This avoids the need for more that "integer-type" data. (Converting what I will present to use real-type data would be trivial.)

In a similar bit for simplicity, we'll assume that the tax is never a fraction of a percent, e.g. 6.5%, and we will again use an integer. If the tax is 5%, the value in the integer will be 5. You pay 5 pence on each 100 pennies. Inside the function, we will use a real-type variable, so the function can answer, say, "What would a 5% sales tax add to a $1.27 purchase?

We'll call the function iSalesTax. To USE it from the main unit, you'd say...

iTax:=iSalesTax(iRate,iTaxOn);

Get a new application up and running. Doesn't have to do much. Put an edit box, a button and a label on it. Default names will do fine.

Something that does all of that is presented as LDN164.

And I would start like that- with a subroutine not in a secondary unit if I were doing this "for real", not just in the course of writing this tutorial for you. If you write that well, if you keep in mind that you are going to move it to a secondary unit soon, the moving is simple.

Subroutine/ Function/ Procedure

Functions and procedures are subroutines. It is a bit like saying that vowels and consonants are letters. Although this tutorial only shows you putting a function into a secondary unit, because that is easiest, there are ways to put procedures into subroutines. (There are some lines in the code marked...

{Only de-rem at very end... and de-rem all such

...to show you a procedure in use. Put a "//" to rem-out the start of these rems... and put a // in front of the "}"s that end them... and run the program again, to see a procedure working from inside a secondary until. The "var" in the parameters list is "the secret".

I have made more "different" variables than I needed to. The way all of this works, I could have used iTmp over and over, because of the rules of scope. But why rely on scope, when you can be explicit? There will be times when you will glad of the way scope can help you, though.

A Change

I am "change-phobic". But sometimes we have to "get over" things.

We're going to make what I hope you will agree is a small change to our tax calculating application.

We started with an "obvious" solution to the want.

Fine.

But our first solution is not easily put into a subroutine that does nothing to or with external variables and components. And sub-routines like that are GOOD... because they are easily moved to secondary units!

Our change will mean that we cannot easily have a message on the screen, in a label separate from the label reporting the tax, to say "Enter integer in edit box" when something other than an integer is in it when the "Calculate tax" button is clicked.

Our new application will put "-999" next to the "Tax Would Be". Surely that's not too big a price to pay?

Back to work

(Once we have that, we can finish this off very quickly... we can convert our "simple example" into an example to illustrate how subroutines can be "packaged up" in secondary units.

The heart of our code now looks like...

procedure TLDN165f1.Button1Click(Sender: TObject);
begin
  sTmp:=Edit1.text;
  iTmp:=iTaxInPennies(sTmp,iTaxRate);
  Label1.caption:=inttostr(iTmp);
end;//Of Button1Click

Pretty "clean" and easy to read, I hope you'll agree! (That's one of the great benefits of using sub-routines. That's "divide and conquer" in action.)

When you write code like this, you can go farther. All of the "stuff" that turns the amount to be taxed (which is in sTmp) and the tax rate (in iTaxRate) into either -999 or the answer is now "out from underfoot. If you forget what iTaxInPennies does with the two things you supply it with, you go and look at the details.

(Slight digression...) And at the head of the "details", you include a guide to what the subroutine does. In this case...

//Returns -999 as an error message if what is in sTaxOn
//  is not a positive number
//Otherwise returns what the tax would be, in pennies,
//  rounded to the nearest penny,
//  if the tax rate were whatever
//  is in iTaxRate.

//E.g. If sTaxOn were "200", and iTaxRateToUse 5,
//   function would return 10

(... slight digression ends)

"Bottom up development" (see other) mandates that you would work really hard on the sub-routine to be sure that it works for anything passed to it as soon as it enters the code. That is the time to be really, really sure it does what the rems claim.

(Digression) Even harder than perfect testing of new subroutines as they are introduced is choosing, during the design phase, what each subroutine will do. If Bottom Up Development is an art (which it is) then choosing what subroutines will do verges on a dark art!

Here's what the subroutine looks like. Messy? "Complicated"? Not really... but, I admit, less transparent that the place where we use the function. This is not a problem! You will only be thinking about a few tiny things when you are actively working on code like the following. Once you've done that, the code that uses it can be clean and clear!

Note that the code is not exactly as it was before.

Previously, we used a variable called iTaxRate. In the subroutine we are using "iTaxRateToUse". You will find that in two places. In the first, the parameters list, we create it, as a variable local to this subroutine. (It will be filled from the other variable, "iTaxRate", "known" to the main part of the code at the moment that the subroutine is called. The names could be "x" and "y"... names that were similar to one another were chosen to make things easy for you. Really! You will get used to it!)

In a similar vein, notice that what was "sTmp" is now "sTaxOn"... it's in the bit...

begin
  val(sTaxOn,siTmp,iErrCodeFrmVal)...

You need to think carefully, you need to figure out WHY this was changed. The "old" "sTmp" WILL " work"... for now. But it won't later, and even now it is a viper at your breast, waiting to strike you down.

THERE SHOULD NOT BE REFERENCES to GLOBAL variables INSIDE a subroutine. (We all break that rule, some of us not infrequently... but it is a mistake to do so. And mistakes can come back to haunt us!)

So! The whole of the SR is....

function TLDN165f1.iTaxinPennies(sTaxOn:string;iTaxRateToUse:integer):integer;
//Returns -999 as an error message if what is in sTaxOn
//  is not a positive number
//Otherwise returns what the tax would be, in pennies,
//  rounded to the nearest penny,
//  if the tax rate were whatever
//  is in iTaxRateToUse.

//E.g. If sTaxOn were "200", and iTaxRateToUse 5,
//   function would return 10
var siTmp:single;
  iErrCodeFrmVal:integer;
begin
  val(sTaxOn,siTmp,iErrCodeFrmVal);//string to convert and
        //in what to return error code. (If no error: zero returned)
    if siTmp<0 then iErrCodeFrmVal:=-1;//To impose additional test on sTmp
       //(And make error code -1 ambiguous along the way!)
    if iErrCodeFrmVal<>0 then begin
       result:=-999;
       end //no ; here. End of "then..."
      else begin
       siTmp:=(siTmp*iTaxRateToUse)/100;//NOTE: "iTaxRateToUse"...
                           //no longer just "iTaxRate"
       result:=round(siTmp);
      end;//of "else"
end; // function iTaxinPennies

Finally... and you may find it an anti-climax!

Hurrah! We are very nearly "there".

Because we prepared well, moving the subroutine to an external secondary unit is easy.

BACK UP the work you have done so far. Now. You don't have to close your Lazarus session. Just go to your File Explorer, select the folder your work is in. Do ctrl-c to take a copy. (Or right-click, choose copy).Click in a neutral place, to de-select your work folder. Do ctrl-v. Change the name from "LDN165-copy" to "LDN165BUyymdd-hhmm" where y/m/d/h/m are year, month (0-9 or a-c (for Oct-Dec)),hour min. Always 2 digits for yy, dd, hh and mm.

And then just go back to a Lazarus window and carry on...

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", and so far it is empty. Save it as "SupportSRs.pas", and you will change the name, as well as doing the save we needed.(You don't need to type the ".pas" part, Lazarus will add that for you, if you go with the flow.

Digression... I like to write such things "SupportSRs", whereas the Lazarus IDE seems to push the user towards eschewing upper case... it prefers "supportsrs"... and will change things to that sometimes. Fighting the IDE can end in tears. But you may be able to get away with it. If you don't mind all lower case, that may be better. In a similar vein... I think spaces in names can cause problems. I don't know, because I do eschew those. In fact I generally start any name with a letter, and use only letters and digits after that, with, sometimes hyphens and underscores used, too. (- and _) No other punctuation marks. My "rules" may be more than is necessary... but they seem to help me stay out of "trouble".

While you may "get away" with using mixed case in places... as long as you are working in Windows... you may be building a nuisance for yourself. One of the virtues of Lazarus is that if you've written something for Windows with it, you can re-compile it to run under Linux or the MacOS, too. They may not be so forgiving.

....Digression ends.

Add "SupportSRs" to the "uses" clause at the top of the main unit. (You just change tabs in the source editor to get there.)

Try running the program. Not a lot will have changed, but you shouldn't get any errors. (You will get warned that unit SupportSRs is not used.) (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 arisen.

Don't try to run the application again for a bit....

Near the top of the main unit, above the line that says "implementation", you will find...

function iTaxinPennies(sTaxOn:string;iTaxRateToUse:integer):integer;

Be sure you're looking at exactly that... there's something similar elsewhere.

Take that (the "function iTaxinPennies(sTaxOn:string;iTaxRateToUse:integer):integer;") out of ldn029u1, and insert it into "SupportSRs", just before the "implementation" in "SupportSRs".

Then take the whole of....

function TLDN165f1.iTaxinPennies(sTaxOn:string;iTaxRateToUse:integer):integer;
//Returns -999 as an error message if what is in sTaxOn
//  is not a positive number
//Otherwise returns what the tax would be, in pennies,
//  rounded to the nearest penny,
//  if the tax rate were whatever
//  is in iTaxRateToUse.

//E.g. If sTaxOn were "200", and iTaxRateToUse 5,
//   function would return 10
var siTmp:single;
  iErrCodeFrmVal:integer;
begin
  val(sTmp,siTmp,iErrCodeFrmVal);//string to convert and
        //in what to return error code. (If no error: zero returned)
    if siTmp<0 then iErrCodeFrmVal:=-1;//To impose additional test on sTmp
       //(And make error code -1 ambiguous along the way!)
    if iErrCodeFrmVal<>0 then begin
       result:=-999;
       end //no ; here. End of "then..."
      else begin
       siTmp:=(siTmp*iTaxRateToUse)/100;
       result:=round(siTmp);
      end;//of "else"
end; // function iTaxinPennies

... and put it into "SupportSRs", between the "implementation" and "end." that are already present there.

Take the "TLDN165f1." off of the copy you made in SupportSRs. (Include the "." in what you take off... that "." wasn't a typo!)

And finally, (still in "SupportSRs"....)

Add....

var sTmp:string;

... just before the line saying "implementation".

Now, try running what you have.

So far, nothing very major, I hope? If I didn't give you good instructions, and you have a mess on your hands, shut down Lazarus. Rename your LDN165 folder "LDN165-failure abandoned yymdd-hhmm", make a COPY of the backup you did... you did do the backup when told, didn't you?... (Or otherwise get back to where we were then.) Rename the copy "LDN165"... and start this section again. Sorry!

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.

...Oh no! I could have sworn I'd written a lot more here in the past hour. I hope that this "works" for you. I hope nothing "important" was lost in the stuff that I think Just Vanished.

There are various things in the source code that may be useful to you....

Once you have mastered the above, go through the source code of LDN165, and put a // in front of ALL of the line that say...

{Only de-rem at very end... and de-rem all such
... and... the ones that have the } that goes with the above. De-rem them too. Again, simply by putting a // in front of them.

That will, I hope, show you that procedures can go in a secondary unit, too.

HELP! Please!

PLEASE help me, but more important all the others out there wanting to get better at programming with Lazarus.

If you find any faults in this, if you can explain places where it wasn't absolutely clear, places where you had to make a guess, PLEASE LET ME KNOW. (Contact details at bottom.) Nothing is "trivial". This page is meant for novices, remember. They have enough to struggle with.

And please, please, please mention my Lazarus tutorials on social media, in forums, etc. Producing this has taken HOURS... wasted hours if no one ever reads it.





Search across all my sites with the Google search...

Custom Search


To search THIS site.... (Go to my other sites, below, and use their search buttons if you want to search them.)

index sitemap advanced
search engine by freefind

Site Map    What's New    Search

The search engine merely looks for the words you type, so....
*    Spell them properly.
*    Don't bother with "How do I get rich?" That will merely return pages with "how", "do", "I"....

Please also note that I have two other sites, and that this search will not include them. They have their own search buttons.

My SheepdogSoftware.co.uk site.

My site at Arunet.


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 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 Lazarus Tutorials main page

To email this page's editor, Tom Boyd.... Editor's email address. Suggestions welcomed! Please cite "Lt4Ng-secondaryunits.htm".




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

AND passes... Valid CSS!

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