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.)
Ini files are great. And for years, I made them more complicated than they are.
They are a great, simple alternative to cluttering the registry.
I have written about them before... but probably not as well as I have done so here, September 2013. (Reviewed Aug 2024 and bits added.)
This is a level 2 tutorial not because .ini files are "really necessary"... but because they, and this tutorial, are really easy. Along the way of a look at ini files, you'll learn some more odds and ends, and simply get some practice at running your Lazarus... or Delphi. Everything here should work there, too.
This tutorial won't tell you "everything"... but I hope it will get you started.
I have posted a zip file with the sourcecode, and a compiled .exe... but you will learn more, if you go though this, and actually build the application as I describe it. You can use Copy/Paste to save typing stuff!
Before I start, two quick things...
First quick thing: 17 Aug 24, I was alarmed that I couldn't find much about "Lazarus ReadInteger". (Yes, I also looked at wiki.freepascal.org/Lazarus_Documentation).
In particular, I wanted to know the largest value ReadInteger could manage. From my own experiments, it seems it can manage 2147483647, but not the next higher number. That's a bit more than 2 billion (10^9). Don't use commas in your ini file. I haven't tested my guess, but it might well turn out that the lowest valid number is about negative 2 billion. In other words, I think that the data type for the value ReadInteger returns is LongInt (signed, 32 bits).
If you give ReadInteger an invalid value, it will return its third parameter, the one it returns if the ini file doesn't have an entry for the section and key that you wanted to read from.
Second quick thing: 7 Nov 21, after many MANY years of having a nagging problem come and go, I finally discovered the source of my problems. Beware... this one really stumped me, for a very long time.
Forgive me shouting? The problems you will have if you forget the following advice can be extremely exasperating... and, as I have indicated, extremely difficult to figure out.
Click the "x" in the menu in the upper right hand corner of the window. Or, if you've made a button that calls "close" to shut the app down, you can use that.
If you are in the habit of creating buttons to close down an app, it might be wise to put a note in the sourcecode within the button's Click event handler..
//DO NOT USE APPLICATION.TERMINATE here. //If you do, the usual "write to ini file" // WILL NOT HAPPEN when you end the app by this button.
We're going to make a small form. It will have the minute of the hour the form was most recently opened on it, and the minute of the hour of the previous opening. (Just work with me here... the "minute of the hour" thing is a pain for me, as well as for you. But if you want me to keep this simpler than it might otherwise be, don't complain!)
As long as the application is left in "restored" state, i.e. not minimized or maximized, it will also remember it's previous size and position on the screen. All of this "remembering" via an ini file called LDN006ini.txt saved where-ever the LDN006.exe file is.
Name the form LDN006f1, save the unit as LDN006u1.pas, and the project as LDN006.
Put two labels on the form; name them laPrevOpening and laThisOpening. Declare two global variables: bMinutePrevOpening and bMinuteThisOpening, both byte-type variables. (You probably ought to create two more labels, I'd call them laTxtPrevOpening and laTxtThisOpening, and I'd make the caption of the first "Minute of hour of previous opening:")
In the OnFormCreate handler....
procedure TLDN006f1.FormCreate(Sender: TObject); var wHour, wMin, wSec, wMilli : Word;//Needed for DecodeTime begin DecodeTime(now, wHour, wMin, wSec,wMilli);//Build into Lazarus and Delphi //"now" is a built in function to pick up the current time from your system. bThisOpening:=wMin; laThisOpening.caption:=inttostr(bThisOpening); end;//FormCreate
So far so good, I hope. Sorry for all the "fog" surrounding DecodeTime, "now", etc.
When you run the application at this stage, you should get the right number for when the application was run... the number for the minutes past the hour when you ran it.
A little wrinkle: If I have 255 in a variable connected with "minutes past the hour", I want my application to interpret that as "The time was unknown". (This is an example of a "rogue value"... a useful programming "trick".)
We're going to need a way to say, when the application is run without the "ini file magic", that we don't know when the application was previously run.
Just keep that at the back of your mind... you'll use it in a moment.
Add 'IniFiles' to the code's "Uses" clause. (Near top.)
And add new stuff to the OnFormCreate handler until it looks like the following. Don't miss the
dfIniFile:TIniFile;//"df" in name from "datafile". Could be anything.
.... in the "var" section.
procedure TLDN006f1.FormCreate(Sender: TObject); var wHour, wMin, wSec, wMilli : Word;//Needed for DecodeTime dfIniFile:TIniFile;//"df" in name from "datafile". Could be anything. begin DecodeTime(now, wHour, wMin, wSec,wMilli);//Build into Lazarus and Delphi //"now" is a built in function to pick up the current time from your system. bThisOpening:=wMin; laThisOpening.caption:=inttostr(bThisOpening); //Heart if reading from ini file dfIniFile:=TIniFile.Create('LDN006ini.txt'); with dfIniFile do begin bPrevOpening:=ReadInteger('Previously','Opened At',255); end;//with... dfIniFile.Free; //End of "Heart of...." //Now use what you read.... if bPrevOpening=255 then laPrevOpening.caption:='No record of previous opening'//no ; here else laPrevOpening.caption:=IntToStr(bPrevOpening); end;//FormCreate
If you run this, you should get a sensible result. Click the "x" at the upper right to shut your application down. Wait a minute. Try again. You should get a different number... but the "Minute of hour of previous opening:" label still says "No record of previous opening". Of course it does! The computer only does what you told it to... not what you have in mind to make it do. So. How do we make it "remember"? (Don't worry yet about why what we've done works! But do rest assured: It is okay to run this program even if you don't have a file called LDN006ini.txt on your hard drive!)
In a moment, I'll show you how to make your application create, or re-save, as need be, its ini file. Just before that, a necessary detail, if what we are doing is going to work.
Remember: The ini file is meant to be keeping track of when the most recent opening of the application took place. We almost have it reporting when the previous opening took place. Now we have to set the stage for saving this instance of opening, so that it will be available next time.
Just after....
//Now use what you read.... if bPrevOpening=255 then laPrevOpening.caption:='No record of previous opening'//no ; here else laPrevOpening.caption:=IntToStr(bPrevOpening);
... at the end of the OnFormCreate handler, add....
//Update the variable holding "when was previous opening", so that it // reflects THIS opening. (We will save this in a moment.) bPrevOpening:=wMin;
Now we're ready for...
Before we can "turn on" "save for next time", we need to check how the application is closed. If you have a button, as I prefer to have, caption "Quit", which when you click it closes things down, the code that executes should be "close". Not "application.terminate", which will also shut something down. (But terminate doesn't trigger the FormClose event, which is where we are putting our "save things to ini file" code!).
Clicking the red "x", upper right of window, will call a FormClose.
Right! With that detail taken care of...
With the application not running, click on a "boring bit" of the form. Go to the Object Inspector. (Pressing f11 should take you there.) Click on the "Events" tab. Double-click on "OnClose".
That should bring up the shell for an OnFormClose event handler. Fill it in as follows...
procedure TLDN006f1.FormClose(Sender: TObject; var CloseAction: TCloseAction); var dfIniFile:TInifile; begin dfIniFile:=TIniFile.Create('LDN006ini.txt'); with dfIniFile do begin WriteInteger('Previously','Opened At',bPrevOpening); end;//with... dfIniFile.Free; end;
Again... that works as you would want it to whether there is a pre-existing LDN006ini.txt or not. Hurrah!
That's pretty much "it". There are some details we'll look at in due course, but the core is in what we've done.
If the application is run at 35 minutes past the hour, the program above creates (so far!) a very small ini file, in the following form...
[Previously] Opened At=35
Note...
In each case (Reading from the ini file, writing back to it), we had a TInifile type variable called dfIniFile, and we did...
dfIniFile:=TIniFile.Create('LDN006ini.txt'); with dfIniFile do begin {{Some stuff}} end;//with... dfIniFile.Free;
The "some stuff" when we were reading from the ini file was...
bPrevOpening:=ReadInteger('Previously','Opened At',255);
Working our way through the above....
bPrevOpening: This is a variable we are going to put something in.
ReadInteger: A built in procedure, provided to us by the IniFiles unit, which we put in the list of units in the "Uses" clause.
'Previously': This specifies the section of the ini file to look in. Sections are defined by putting a word or words ("Previously" in this case) inside square brackets.
'Opened At': This will be the name of the "key" within the section.
The third parameter, 255 in the example, is the value which will be used if the ini file you are looking at does not have the specified key in the specified section. Until we'd written the code in the OnClose handler, which wrote the first copy of the ini file, there was no ini file, and thus no sections, and no keys, and so bPreviously was set to 255.
Now we'll look closely at the "some stuff" used if saving to an ini file. In our example, we have...
WriteInteger('Previously','Opened At',bPrevOpening);
WriteInteger: A built in procedure, supplied from the IniFiles unit.
'Previously': The section we want to write to. If there is no [Previously] section header in the ini file yet, one will be created
'Opened At': The key we are going to assign a value to.
What ever is in the variable bPrevOpening is what will follow "Opened At="
It really is that simple. Once upon a time, I went to great lengths to see if there was an ini file already, before trying to work with it. Not necessary. The .Create and .Free methods take care of all of that... If you take care of them. They must both be present. You need one .free to "partner" every .create. (There's rarely any reason to postpone doing the .free. Do the .create. Read or write from/ to the ini file. And do the .free.
A confession: I am not certain that the OnClose handler is exactly the right place for the code to write back to the ini file. I think it is the place for it.
A warning: If you are using a text editor to look at what is in the ini file, and maybe even tweak it as you work on the program, be careful to think about what is going on. It is easy, for instance, to load the ini file into the text editor, run and close the application, and not realize that what the text editor is showing is what was (previously) in the ini file... which changed when you closed your application! One of the reasons I like using Textpad is that it warns you when things like this happen.
Beware... there is a weird thing I've never quite got to the bottom of: It is almost as if there's some form of caching going on. You edit a text file in your text editor. You save the file. You cause your application to re-read the ini file... but it doesn't see the changes you made. Solution: I think that as long as you close your application before doing external editing of the ini file, then, when you re-open your application, it will pick up the changes you made.
In what I did above, I just sort of assumed that the system would put the ini file someplace sensible.
It would probably be best to explicitly put it somewhere specific.
The easiest thing... which isn't a very good idea... is to put the ini file in your drive's root directory. You would do that as follows....
dfIniFile:=TIniFile.Create('C:\LDN006ini.txt'); with dfIniFile do begin {{Some stuff}} end;//with... dfIniFile.Free;
A better alternative is to put the ini file in the same folder as the .exe is in. The following looks arcane, but it JustWorks, and, if you don't let it intimidate you, is pretty easy to analyze, too....
dfIniFile:=TIniFile.Create(ExtractFilePath(Application.ExeName)+'LDN006ini.txt'); with dfIniFile do begin {{Some stuff}} end;//with... dfIniFile.Free;
So far, we've only read and written integers.
There is also a ReadString and a WriteString command. They work just as the ReadInteger and WriteInteger.
There is also a ReadDateTime and WriteDateTime. It SHOULD have been possible to write my example to show you the date and time that the file was last opened. Lazarus "understands" the words. But I couldn't get the following to work...
dtPrevOpening:=ReadInteger('Previously','Opened At',0);
"Zero" was going to be my rogue value, which would have gone into dtPrevOpening when the ini file was not yet present. I couldn't get it to work. If you have better luck, please let me know that it Can Be Done.
Just to wrap things up, here are the two bits of code you need, if you want the application to remember where you'd put it on the screen, how big you had it, from execution to execution. Put each of these blocks just after the ReadInteger/ WriteInteger lines we already have in the code. (Put these new lines inside the "with" blocks in both cases.)
LDN006f1.top :=ReadInteger('Position and size','Top of window- from top of screen',10); LDN006f1.left :=ReadInteger('Position and size','Left',30); LDN006f1.height:=ReadInteger('Position and size','Height',100); LDN006f1.width :=ReadInteger('Position and size','Width',500); WriteInteger('Position and size','Top of window- from top of screen',LDN006f1.top); WriteInteger('Position and size','Left', LDN006f1.left); WriteInteger('Position and size','Height',LDN006f1.height); WriteInteger('Position and size','Width', LDN006f1.width);
Yes... my first key is a bit overdone. I wanted to show you that the key can be different from the name of the property the value is taken from.
Ini files can be tremendously helpful in some kinds of programming. I am just finishing an application which fetches files from sundry source folders, and makes copies of them to other, destination folders. I use ini files to keep track of which folders are the right folders to be fetched from/ written to. (The "right" folders differ from one machine to another.)
Even if you only use an ini file to make the application remember where you want it on the screen, and how big, many applications will be better for that "frill" being present.
Some time ago, Microsoft tried to get us to give up ini files. Microsoft wanted us to use the registry for such things. Now, for certain jobs, the registry is wonderful. For small bits of housekeeping by minor programs.... and let's face it, most of the things we write for our own needs are minor... ini files do a perfectly adequate job, without getting anywhere near the registry, which is an area of your computer where you really do not want problems.
You don't need them, but you can embed comments in an ini file, if you wish.
The usual practice is to start each comment line with a semicolon...
;ini file for LDN006 [Previously] ;Records minutes past hour application last opened. Opened At=47 [Position and size] ;Records position and size of application's form Top of window- from top of screen=454 Left=49 Height=65 Width=618
That's fine. You can type comments like that into an ini file, and save it, and they will still be there even after the application has opened and closed the ini file a few times, even if it is changing some of the keys' values. (The ini file provisions do sometime do irritating things with blank lines. (Sometimes they are added, sometimes taken away.) (If you create a "blank" line by putting just a semicolon on the line, it won't be deleted.)
"But!", I hear you cry. "What if I want to embed comments in an ini file programmatically, if the application is creating one where there was no ini file before.
Sadly, there is no WriteComment(';My Comment') command.
Here's what you're going to have to do.... And it's a pity, because it "wastes" the ini file system's clever ability to cope, whether there's an ini file present or not. But no harm done, and you get to create an initial ini file, with whatever comments you want.
Before you get to the "dfIniFile:=TIniFile.Create('LDN006ini.txt');", you need to check to see if one already exists. (There is a FileExists function.) If not, fill a memo (you'll have to put one on your form, but you can set its "Visible" property to false. Set "WantWordWrap" false, by the way. (Do that programmatically in your OnFormCreate.)
And save the memo with the .lines.savetofile method.
Here's the code. It goes on a bit, but when you realize that if you've seen one .lines.add() line, you've seen them all, the following isn't so terrible...
procedure TLDN006f1.FormClose(Sender: TObject; var CloseAction: TCloseAction); var dfIniFile:TInifile; sTmp:string; begin sTmp:='LDN006ini.txt'; if FileExists(sTmp)=false then begin Memo1.lines.Clear; Memo1.lines.Add(';ini file for LDN006'); Memo1.lines.Add(';'); Memo1.lines.Add('[Previously]'); Memo1.lines.Add(';Records minutes past hour application last opened.'); Memo1.lines.Add('Opened At=255'); Memo1.lines.Add('[Position and size]'); Memo1.lines.Add(';Records position and size of application''s form'); Memo1.lines.Add('Top of window- from top of screen=10'); Memo1.lines.Add('Left=50'); Memo1.lines.Add('Height=65'); Memo1.lines.Add('Width=620'); Memo1.Lines.savetofile(sTmp); end; dfIniFile:=TIniFile.Create(sTmp); with dfIniFile do begin WriteInteger('Previously','Opened At',bPrevOpening); WriteInteger('Position and size','Top of window- from top of screen',LDN006f1.top); WriteInteger('Position and size','Left', LDN006f1.left); WriteInteger('Position and size','Height',LDN006f1.height); WriteInteger('Position and size','Width', LDN006f1.width); end;//with... dfIniFile.Free; end;
Somewhere in the guts of the operating system, there is a system variable to define your "current directory". (And that may of may not overlap with the sort of thing you get if you call GetCurrentDir).
If there is any chance of your program "wandering off", and visiting different directories while it is executing, then you should probably....
a) Set up a global variable which I am going to call sDrivePathNameOfIniFile.
b) Early in onFormCreate, do...
sDrivePathNameOfIniFile:=ExtractFilePath(Application.ExeName)+'LDN006ini.txt'
c) and then, any time you do a TIniFile.Create, indicate what and where the ini file is, refer to it with sDrivePathNameOfIniFile (Being careful not to change the value in that anywhere. Wouldn't it be neat, if there were a way to "lock" a "variable" once you've filled it with a "constant"?! (Of course, you can use constants... if you know the value that will be needed at the time the code is written.)
End of first idea. Matter arising:
What if you want to run the application, but under the control of multiple ini files?
There are several ways to skin that cat. The simplest is to put the ini file in the application's .exe's folder, and have several copies of .exe and of the .ini file, all in separate folders. Rather wasteful of disk space (not really a problem today, but a source of problems if you need to install an improved version of the .exe.)
You could use a Command Line Parameter (CLP) to pass the name or location (or both!) of the ini file to use.
I once used CLPs a lot... but then I "discovered" ini files. They are Just Nicer.
But a CLP still has a role to play... it can tell the application which ini file to use! (Or where to find the one you want used this time.)
To use a CLP, you have to invoke the application from a shortcut. If you go this route, you might also write the code so that if no CLP were present, the program would ask for the ini file path and name interactively, if you didn't want to write it so that the program just went to a default location and name.
You can access the first command line parameter with...
if paramcount>0 then sCLP:=paramstr(1);
More on CLPs... in a rather ancient, and perhaps less polished, Delphi tutorial on command line parameters and the keywords paramcount and paramstr I wrote a long time ago.
Search across all my sites with the Google search...
|
Page has been tested for compliance with INDUSTRY (not MS-only) standards, using the free, publicly accessible validator at validator.w3.org. It passes in some important ways, but still needs work to fully meet HTML 5 expectations.
. . . . . P a g e . . . E n d s . . . . .