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

Delphi: User defined records
How and why to use them in Delphi and Lazarus programming.
Also: external standalone units for Delphi and Lazarus.

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!

Not extensively tested, but the ideas presented here seem to work the same way under Lazarus

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


Welcome! User Defined Records for easy subroutine work.

And later... external code units

You should already be using subroutines in your programming. If you aren't, try one of my several tutorials on using subroutines. Using them... procedures and functions... is an important part of programming well.

You should be passing data to the subroutines, and getting data back.

This tutorial will show you an advanced data structure, the "user defined record", which will allow you to create a function which can pass back values for more than one "variable".

And then, we go further, and see how the "innards" of the function and of the user definition of the record can be "parceled up" in an external unit.

To clarify the first goal:

I hope you already know how, say, to create a function called bMyDoubleIt which will make the following work...

bTmp:=6;
bTmp:=bMyDoubleIt(bTmp);
showmessage(inttostr(bTmp));//Message would say "12"

What this tutorial will do is allow you to do something like this....

udrMyUserDefinedRecord.x:=7;
udrMyUserDefinedRecord.y:=40;

udrMyUserDefinedRecord:=udrDoubleMyUDR(urdMyUserDefinedRecord);

showmessage(inttostr(udrMyUserDefinedRecord.x));//Message would say "14"
showmessage(inttostr(udrMyUserDefinedRecord.y));//Message would say "80"
showmessage(udrMyUserDefinedRecord.sSumAsString);//Message would say "94"

And yes, you DO want to be able to do things like that, even if you don't yet know it!


Review...

First a little review. Without getting fancy, if a subroutine is a function, we anticipate that the function will return just one string, or just one number.

Consider the following, which relies on a global variable called "iRate", to tell you the price WITH tax of something from its price without tax. (All of the variables and the function are of type "single", hence the "si" prefix on the names.) I've done the code so that the siWithTax function will be called by clicking on a button. The heart of that (calling the function, and the function itself), is....

procedure TDD93f1.Button1Click(Sender: TObject);
begin
siRate:=0.15;//i.e. 15%
siTmp:=siWithTax(1000);
showmessage(FloatToStr(siTmp));
end;

function TDD93f1.siWithTax(siNet:single):single;
begin
result:=siNet+siNet*siRate;
end;

The whole application for that is as follows.

(The name DD93u1 will be used and re-used, i.e. in the course of this essay, many different DD93u1's will be developed and discussed.)

So... the whole code for the application with bits shown a moment ago...

unit DD93u1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

type
  TDD93f1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    siTmp,siRate:single;
    function siWithTax(siNet:single):single;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  DD93f1: TDD93f1;

implementation

{$R *.DFM}

procedure TDD93f1.Button1Click(Sender: TObject);
begin
siRate:=0.15;//i.e. 15%
siTmp:=siWithTax(1000);
showmessage(FloatToStr(siTmp));
end;

function TDD93f1.siWithTax(siNet:single):single;
begin
result:=siNet+siNet*siRate;
end;

end.

That's a pretty poor way to do things... The worst bit of Bad Programming is using siRate the way we have. There are sometimes reasons to break the rules, but in general, it is much better NOT to use global variables, especially within a subroutine.

The code above is easy to fix....

procedure TDD93f1.Button1Click(Sender: TObject);
begin
siRate:=0.15;//i.e. 15%
siTmp:=siWithTax(1000,siRate);
showmessage(FloatToStr(siTmp));
end;

function TDD93f1.siWithTax(siNet,siR:single):single;
begin
result:=siNet+(siNet*siR);
end;

That's fine for passing data TO a subroutine. How about getting things BACK from a subroutine?

We've already seen how to get a number back. Functions can also return strings.

You can also use procedures, with the "var" keyword... Look closely at the following. There are some extra bits that are only there to show you how these things work. I'll explain more in a moment. Look at the code first....

procedure TDD93f1.Button1Click(Sender: TObject);
begin
siRate:=0.15;//i.e. 15%
siAmnt:=1000;
siWithTax(siAmnt,siRate);
showmessage('Gross: '+FloatToStr(siAmnt)+'  || Rate: '+FloatToStr(siRate));
end;

procedure TDD93f1.siWithTax(var siA:single;siR:single);
begin
//showmessage(floattostr(siA));
siA:=siA+siA*siR;
siR:=siR*5;//There's no point... but it will show you something
end;

This is much better, because we are not using the global variable "inside" the subroutine. It also shows you a way to get data BACK from a procedure. When we called this, siAmnt held 1000, After the procedure had executed, siAmnt (outside the procedure) held 1150. But siRate STILL held 15, in spite of the fact that, inside the subroutine, we multiplied what was in the variable filled from siRate (siR) by 5. What gives? Note the "var" in the procedure declaration. It makes the difference we have observed.

All of the above should just be review. See other tutorials if you are already struggling.


Getting multiple values back from a function call

Now we're going to do something "fancy".. We're going to create our own data type. What's that? Why do it? We'll see. First, the heart of things...

procedure TDD93f1.Button1Click(Sender: TObject);
begin
udrMyGeoPoint.x:=10;
udrMyGeoPoint.y:=10;
FindAltitude(udrMyGeoPoint);
iTmp:=udrMyGeoPoint.altitude;
showmessage(inttostr(itmp));
end;

procedure TDD93f1.FindAltitude(var udrTmp:TDD93DemoType);
begin
udrTmp.altitude:=udrTmp.x+udrTmp.y;
end;

"udr" for "User Defined Record"

We fill "x" and "y" with values, and then call FindAltitude, which fills the "altitude" part of the record with a number. (In our simplified geography, the "altitude" just (helpfully) happens to be the sum of the x and the y.

The thing to notice is not HOW we find the altitude, but how the code passes things around, and how it refers to the parts of a given instance of a TDD93DemoType datum. These user defined records are very helpful in keeping your code tidy when your application is dealing with complex objects, like points on the surface of a landscape.

(Here's the full code... and a comment that may help you understand what I was trying to get across... for the example above....)

unit DD93u1;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  StdCtrls;

//Create our own, "custom" type. Name doesn't HAVE to start with
//  a "T", but that's traditional. Note this done before the type
//  declaration for the TDD93f1, so that records of our custom
//  "type" can be created as part of the instances of the TDD93f1 class.

//Imagine that we have some geographic data available to the computer.
//Further imagine that if we specify a place with an "x" and a "y"
//   representing the latitude and longitude of a place, that we can
//   look up the altitude of the ground there. In this DEMO, the "altitude"
//   will always equal the sum of the x and the y values... you have a
//   world with the high ground in the NE, sloping down to low ground
//   in the SW (assuming x increases from west to east, and y increases
//   from south to north. Hey! This isn't about DOING a geographical
//   data program... it is to show you something about passing data
//   to and from procedures.

type TDD93DemoType = record //no ; here
  x,y:integer;
  altitude:integer;
  end;//of declaration of type TDD93DemoType

type
  TDD93f1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
    procedure FindAltitude(var udrTmp:TDD93DemoType);
  private
    iTmp:integer;
    udrMyGeoPoint:TDD93DemoType;
    { Private declarations }
  public
    { Public declarations }
  end;

var
  DD93f1: TDD93f1;

implementation

{$R *.DFM}

procedure TDD93f1.Button1Click(Sender: TObject);
begin
udrMyGeoPoint.x:=10;
udrMyGeoPoint.y:=10;
FindAltitude(udrMyGeoPoint);
iTmp:=udrMyGeoPoint.altitude;
showmessage(inttostr(itmp));
end;

procedure TDD93f1.FindAltitude(var udrTmp:TDD93DemoType);
begin
udrTmp.altitude:=udrTmp.x+udrTmp.y;
end;

end.

=== A "real world" example.

I was doing some programming of a temperature sensor chip from the Dallas Semiconductor 1-Wire family chips.

My UDR is going to "wrap" into a nice tidy parcel...

=sChipID: a string, always 16 characters drawn from 0-9 + A-F =iResult: an integer =iErr: an integer =bTKBErr: a byte =sErrMsg:a string of up to 30 characters

I will use variables with the structure above as follows...

First I will put something into the sChipID string. EACH Dallas 1-Wire chip has a unique ID number inside it. You can have several 1-Wire chips all "on" the same wire, and "talk" to one or another of them, if you know the ID number of the chip you want to speak to.

Then I will call a procedure, with the UDR as a "var" type parameter of the procedure call.

When the application gets back from executing the procedure, I will have the temperature that chip was sensing, IF there isn't an error message in iErr. Usually with the 1-Wire chips, iErr will be zero if there was no error, but that doesn't have to be the case.

If there WAS an error, the number in iResult is probably meaningless, but bTKBErr and sErrMsg will also have been filled, with details of what and where the error was, details in addition to the information available from WHICH non-zero number is in iErr.

Using an UDR is just NICER....

You "could" do something like...

sID:='12341111ABCD2828';
GetDallasTture(sID:string;
   var iResult: integer;
   var iErr: integer;
   var bTKBErr: byte;
   var sErrMsg: string;);
if iErr><0 then begin
    //deal with fact that something went
    //   wrong during execution of procedure
  end //no ; here
 else begin
    // use the value in iResult...
  end;// of "else" block

... but the UDR based answer just seems nicer. And it seems a lot nicer if you have a lot of calls to make to GetDallasTemperature.

(The UDR based code looks like....)

udrMyRecord.sID:='12341111ABCD2828';
GetDallasTture(var udrMyRecord);
if udrMyRecord.iErr><0 then begin
    //deal with fact that something went
    //   wrong during execution of procedure
  end //no ; here
 else begin
    // use the value in udrMyRecord.iResult...
  end;// of "else" block

=== We're now going to take things even further, and move the procedure FindAltitude, and the declaration of type TDD93DemoType out of the main unit of the application, and put them in a unit which the main unit can call upon, a unit it "uses". If you choose carefully what you put where, you can re-use things. for instance, in my real world example, I will be creating some code for reading the temperature a chip is sensing.. and then to be able to use that in a subsequent project, I will only have to make the .dcu file available to the main unit, and remember what "magic incantations" are required, what data types are available.

Now... I've written tutorials in the past about using external units. This tutorial, drafted in May 2011 isn't trying to do anything "new" or "different"... but it may seem that it is, just because I am now a more experienced Delphi programmer. I was working with a Delphi 4 system, by the way... not that I would expect it to matter.

I left DD93 under development, didn't close anything.

Invoked File|New|Unit, and a new tab opened in my Delphi code editor, with...

unit Unit1;

interface

implementation

end.

I promptly TRIED TO save it as DD93sau.pas... "sau" for "Stand Alone Unit". (I somehow fluffed that.. which I knew as the tab name hadn't changed from "Unit 1". Soon got things sorted out. I hope you don't have similar problems. The Object Inspector is irrelevant while you are working on your new unit. While it will be important to your program, it has no "objects" for the Inspector to work with.

For a quick test, before doing the "tricky" bits, I added....

const ver='27 May 11';

... Just after the "interface" line, in the unit's code. I then added...

showmessage(dd93sau.ver);

,.. to the Button1 OnClick handler in DD93u1, and tried to run the application... but wasn't surprised when it refused to run. "Undeclared identifier, 'dd93sau'", I was told.

In DD93u1's code, I scrolled up to the "uses" clause, added 'DD93sau', and tried the compile again... And things worked as expected.

See what's happening? Once I'd put the "DD93sau" in DD93u1's "uses" clause, when the application saw "dd93sau.ver", it said to itself: "Ah! I need the constant "ver"... FROM the dd93sau code."

Now we'll go further....

Take the....

type TDD93DemoType = record //no ; here
  x,y:integer;
  altitude:integer;
  end;//of declaration of type TDD93DemoType

... OUT of DD93u1, and put it in DD93sau, just after the "const..." line we put in a moment ago.

Re-run the application... it still works!

Because there is no TDD93DemoType defined in DD93u1, and because we said in DD93u1 "use DD93sau", when DD93u1 needs something it can't find in its own code, it looks further... including in DD93sau... for what it needs.

In actual fact.

showmessage(dd93sau.ver);

... could have been just....

showmessage(ver);

... but prefixing the ver with the DD93sau makes it certain that you will get THAT ver. If there were also one in DD93u1, as normally there would be, without the "DD93sau." part, you'd get the DD93u1 ver string.

So... if we are going to use our user defined record type....

type TDD93DemoType = record //no ; here
  x,y:integer;
  altitude:integer;
  end;//of declaration of type TDD93DemoType

... frequently, in many programs, it would make sense to set up an sau to "hold" it. Then adding it to any program would just be a matter of adding the .dcu file for the sau to any folder where there's other code using that sau, and adding the sau's name to the "uses" clause of the program... um... "using" the sau. (For once the names make perfect sense!)


But that's not all!

Not only can the definition of the type go in the sau, the procedure we created can go there too. Move....

procedure TDD93f1.FindAltitude(var udrTmp:TDD93DemoType);
begin
udrTmp.altitude:=udrTmp.x+udrTmp.y;
end;

... from DD93u1 to DD93sau. Put it just after the word "implementation"... but that's not all you need to do.

Take out the "TDD93f1." from the first line of the procedure, now that it is in DD93sau.

Put the single line...

procedure FindAltitude(var udrTmp:TDD93DemoType);

.. in DD93sau, just BEFORE the word "implementation". (You're putting it at the bottom of the "interface" section.)

The single line in the interface section "introduces" a word you are going to be using ("FindAltitude"), and the small block in the implementation section explains HOW to do "FindAltitude" when the moment comes.

That's it! Apart from some comment lines which should move, you don't want to take anything else out of DD93u1.

We USE our user defined record, and the procedure FindAltitude in the main unit of this application, DD93u1 (which directly drives the form DD93f1). However, "behind the scenes" inside DD93sau, some of the boring details of creating that user defined record, and working out the Altitude when it is needed, are taken care of.

The full source of this "final" DD93 is available online for download. It doesn't do very much... There's a button. When you click it, you get two message boxes. The first says "20" (the "altitude" at x=10, y+10) and the second says "DD93sau: 27 May 11"... the value ver was set to inside DD93sau


Over to you...

In this simple demo, the idea of an external, "stand alone" unit might not be very exciting. They can be excellent resources, especially if regularly you find yourself copy/ pasting certain procedures from old applications to new ones.

If that information about creating external units almost made sense, and you want another shot at mastering the skills, have a look at an older tutorial I wrote about creating stand alone units.





            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


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