HOME - - - - - Lazarus Tutorials TOC - - - - - - Other material for programmers
Delicious.Com Bookmark this on Delicious     StumbleUpon.Com Recommend to StumbleUpon

Moving files. Creating HTML pages. Displaying still images. CCTV

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!

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


DE48 demonstrates some simple Lazarus procedures.

I haven't tried this code in Delphi, but would be greatly surprised if anything here didn't work equally well under Delphi.

A zip file with the full Lazarus sourcecode, etc, and a pre-compiled .exe is available for you to download. Before the .exe will do anything sensible, you need to set up some folders with some .jpg files in them, as explained early in the tutorial.

What it does... in a nutshell

This application is a step along the road to a system I am building for monitoring the still images output from IPCams which I have feeding images to an FTP server. The application will run on the same PC as the FTP server, and periodically harvest copies of images the IPCams have "pushed" onto the hard drive, via the FTP service the PC provides to the cameras. The application will also put together a page of HTML to allow anyone on that PC, or accessing the web-server which also runs on that PC, to see the images which have been selected by the application.

The application, as presented here, does not have all of those features. (I will be selling the full application, in due course.) But the application, as presented here, does have many of the features which will be present in the final version.

Furthermore, I have spent hours writing up an account of how the application was developed. I hope you will work your way through the account, if you are trying to become a stronger programmer.... and when you are done, I hope you will feel you benefited from your effort and my effort.

The IP cams will, for reasons we won't go into here, from time to time write files to folders. Each camera has its own folder.

For the purposes of this demo, the contents of those folders are static. However, the program is being written with a view to extending it to examine the folders periodically. A copy of the most recent image in each folder will be made, and a page created to display those copies of the recent images.

DE48 will take one image from each folder, and copy it to another folder (the "page assembly folder"). Along the way, it will pick up information about the images, in particular, their creation dates.

DE48 will create a text file containing html. Calling that page up in a web browser, e.g. Firefox, will display the images, with labels giving the time of the image's creation.

What Lazarus skills are demonstrated

I hope that you will see various general good practices in all of my Lazarus and Delphi code, for example meaningful names for objects.

In addition, in DE48 I am going to illustrate...

(You may pick up a few ideas about HTML along the way, too. You don't need a web server to complete this tutorial. Just point your web browser at the .htm file our application will create, and it should display nicely.

Building a text file

I'm going to use a "cheap and cheerful" method: I will put the text I want in the file into a "memo" (one of the standard Lazarus and Delphi components, and then save the memo to the disc. Not suitable for every task, but well suited to many, including this one.

You can download a completed DE48, if you wish. You will learn much more by building it up with me.

Establish a folder for this project. Call it DE48.

Target for "goto" note above: "zDLtarget"

Establish a shell application "DE48", with two buttons: buQuit and buDoIt. Caption them "Quit" and "Do It".

For the Quit button, I've never quite got to the bottom of whether it is best to leave an application with the form's close method or the application's terminate method. Use whichever you like.

You could put complex code into the buDoIt button's OnClick handler... but this is a Bad Idea.

Instead, create a separate procedure called DoIt, and have the buDoIt OnClick handler just call that. Remember to put the forward reference to your DoIt in the interface part of your code, in the "private" block. (It would work in the "public" block, too, but using "private" for anything which doesn't need to be "public" is, I think, a good habit.)

Just to get things started, DoIt can be just a "showmessage('Hello world');

Here's the code for that much...

unit DE48u1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls;

type

  { TDE48f1 }

  TDE48f1 = class(TForm)
    buDoIt: TButton;
    buQuit: TButton;
    procedure buDoItClick(Sender: TObject);
    procedure buQuitClick(Sender: TObject);
  private
    { private declarations }
    procedure DoIt;
  public
    { public declarations }
  end;

var
  DE48f1: TDE48f1;

implementation

{$R *.lfm}

{ TDE48f1 }

procedure TDE48f1.buDoItClick(Sender: TObject);
begin
 DoIt;
end;

procedure TDE48f1.buQuitClick(Sender: TObject);
begin
  close;
end;

procedure TDE48f1.DoIt;
begin
 showmessage('Hello world');
end;

end.

Put a memo on the form, call it meBuildTextFile

Set the following properties to the values shown...

Replace the showmessage line in DoIt with a call to FillMemo, and create a FillMemo procedure as follows...

procedure TDE48f1.DoIt;
begin
 FillMemo;
end;

procedure TDE48f1.FillMemo;
begin
 meBuildTextFile.clear;//Empty the memo
 meBuildTextFile.lines.add('Demo 1');
 meBuildTextFile.lines.add('Demo 2');
 meBuildTextFile.lines.add('Demo 3');
end;

Now clicking "DoIt" should change what is in the memo, make it hold the three lines shown above.

So! We've got the memo filled. We'll turn to saving our "masterpiece" as a file in a moment. (And later we will return to the "filling" process, and fill the memo with something more useful.) But first....

Laying some groundwork...

Before we turn to saving what is in the memo, we need to build the part of the program which will take care some configuration matters. For a start, we are going to set things up so that the user doesn't have to say where the text from the memo will be saved. (Eventually, we will give users a way to change their choice... but still spare them having to say again and again where things are to be saved.) We will add other configuration features as we go along.

In the "private" section of the declaration of our TDE48f1 class (which you will have if you named the main form for this project DE48f1, which makes sense if you've called the project "DE48")... add the new line shown in...

  private
    { private declarations }
    sPathOfAssmFldr:string;
    procedure DoIt;
    procedure FillMemo;

(Note that all procedure and function forward declarations (there are two in the above) have to be at the end of the "private" block. They have to come after any variable declarations. (There is just one in the above, so far, but usually there are many.) The same rule applies when putting things in the "public" block.)

The variable will be used to establish where the Assembly Folder (AssmFldr) will be. The Assembly Folder will be the folder where we will put the two .htm files we are assembling, and we will also put the .jpg files one of them uses there, too, in a sub-folder. Always "divide and conquer" when you can.

In the DoIt procedure, add, at the start, a call to "FillVariousVariables", and add the procedure to the application, as follows....

procedure TDE48f1.FillVariousVariables;
begin
  sPathToAssmFldr:='';
end;  

Setting sPathToAssmFldr to nothing ('') will cause the application to save the things we want to save in the same folder that the .exe file is in. This will be fine for now, and when we want things saved elsewhere, we only need to supply the "elsewhere" in the bit of code above. We will write the rest of the application to always refer to the "place for saving stuff" via sPathOfAssmFldr.

Now we can move on to the "saving" part, because we have "the ingredients" provided for...

From memo to file on disk...

"The secret to saving the contents of a memo to our hard disk is in just one line of code... but, as ever, we will "insert" that line of code into our application in a way that keeps things organized, allows debugging and extending to go quickly and easily.

Create a procedure called SaveMemo. Make a call to it as part of the procedure DoIt. The SaveMemo procedure is as follows...

procedure TDE48f1.SaveMemo;
begin
 meBuildTextFile.lines.savetofile(sPathToAssmFldr+'DE48pics.htm');
end;

We will be creating a text file. It will, eventually, hold HTML, so the file's extension should be .htm, not .txt. But it is still a text file. (You could read it into a text editor.) But it is also an .htm file, and if we give it the .htm extension, then by default, on most systems, it will open in a web browser... which is what we are going to want it to do, because web browsers are good at rendering .jpg files as pictures, and, via the HTML, we are going to be asking a browser to do just that.

Note that this rather crude implementation will simply over-write any pre-existing DE48pics.htm file which happens to be in the sDE48WriteToFolder when we call SaveMemo. (There are steps you can introduce to bring up the often necessary "File exists, do you want to over-write it". But for now, we are making a simple application.)

Recap. So far...

So far, we have and application which...

Put some USEFUL text in the memo

We're going to make some changes to the text that gets put in the memo. Some of the changes will involve quite lot of text, but keep the principles clear: It is still just text, even though it may start becoming hard to see the forest for all the trees in the way.

Before we do that, in order for the new text to do interesting things, create a sub- folder in the folder where you have Lazarus creating the application. (The folder should be "DE48", if you did as I asked earlier. That can be where-ever you want it on you hard drive. (I have a "folder of folders" called something like "LazarusProjects" for holding... you can guess, I hope!))

Call the subfolder "Images".

Put four different small .jpgs in the folder. Call them TmpImg01.jpg, TmpImg02.jpg, TmpImg03.jpg, and TmpImg04.jpg. It won't matter if you have other files with similar names elsewhere... the place where you find the folder they are in will help you remember what these copies of TmpImgXX.jpg are for.

Let's digress for a moment: There is nothing in the operating system taking advantage of my "rule", but I never start the name of a file "Tmp" if that file is something I need to keep if I come across it later, and wonder what it is. By using that naming convention, you can save yourself worry during disk tidying sessions. Digression ends.

For the moment, our web-page will always display the same images. Later, we are going to get an unchanging web page to display changing pictures by changing what is in TmpImg01.jpg, etc.

Whew! All this preparation work! We're nearly there now.

Remembering what I said a moment ago about forests and trees, revise FillMemo, to make it...

procedure TDE48f1.FillMemo;
begin
 meBuildTextFile.clear;//Empty the memo first
 meBuildTextFile.lines.add('<html>');
 meBuildTextFile.lines.add('<head>');
 meBuildTextFile.lines.add('</head>');
 meBuildTextFile.lines.add('<body>');
 meBuildTextFile.lines.add('<img width="20%" src="Images\TmpImg01.jpg">');
 meBuildTextFile.lines.add('<br><br>');
 meBuildTextFile.lines.add('<img width="20%" src="Images\TmpImg02.jpg">');
 meBuildTextFile.lines.add('</body>');
 meBuildTextFile.lines.add('</html>');
end;

... and run the app. (You may want to rearrange, re-size the layout of the buttons and memo on your form.)

Then open your file browser (e.g. Windows Explorer). Navigate to the newly (re-) created Tmp.htm. Double-click on that, and your web browser should pop up, displaying the two images you specified!

Not all that we wanted, exactly... but getting there. (You don't have to keep going off to your file browser... just hit your internet browser's "refresh" (or "reload") button after each time you run the application, which changes what is in Tmp.htm. But what you see in the browser only changes when you tell it to go look at the new Tmp.htm.

Boring details

In this section, I'm just refining the form of the "img" lines. It will look messy, but there's no rocket science here. And we aren't yet beginning to address the requirement that the images displayed should be copies of the most recently arrived images in the folders we are supposed to be (but are not yet) watching.

We're going to display two images per line, by using align="left" and align="right" in our "img" commands. Because we need to craft a number of similar lines, we will write a little function (sLineOfHTML) to help us "assemble" them.

Remember: We are just filling the memo with some text, text which doesn't change between executions of "FillMemo". (Later, we will be adding some things that do change (labels for the images), or we could just write the page once and be done with it.)

Since we are "just filling the memo with some text", don't let the complexity of the text distract you.

To make a start, we will forget we are doing images for the moment, and, in place of the "img" lines we had before, we'll have something simpler, which will just say "HelloWorldX" where "X" will be a digit.

Create the function sLineOfHTML inside the FillMemo procedure, and use it, as follows. Note that sLineOfHTML takes two parameters. Remember: We are still "just putting some text in the memo."

procedure TDE48f1.FillMemo;
   function sLineOfHTML(sTmp1,sTmp2:string):string;
    (*This is done a little crudely, in respect of the "L" and "R"*)
    begin
      if sTmp2='L' then
      sLineOfHTML:='<img align="left" '+
                'width="45%" src="Images\TmpImg0'+sTmp1+'.jpg" '+
                'alt="No such file">';
      if sTmp2='R' then
      sLineOfHTML:='<img align="right" '+
                'width="45%" src="Images\TmpImg0'+sTmp1+'.jpg" '+
                'alt="No such file">';
      if (sTmp2<>'L') and (sTmp2<>'R') then
      sLineOfHTML:='<p>There''s an error in the app''s code. Err 13921a</p>';
    end;

begin
 meBuildTextFile.clear;//Empty the memo first
 meBuildTextFile.lines.add('<html>');
 meBuildTextFile.lines.add('<head>');
 meBuildTextFile.lines.add('</head>');
 meBuildTextFile.lines.add('<body>');
 meBuildTextFile.lines.add(sLineOfHTML('1','L'));
 meBuildTextFile.lines.add(sLineOfHTML('2','R'));
 meBuildTextFile.lines.add('<br clear=left><br clear=right>');
 meBuildTextFile.lines.add(sLineOfHTML('3','L'));
 meBuildTextFile.lines.add(sLineOfHTML('4','R'));
 meBuildTextFile.lines.add('<br clear=left><br clear=right>');
 meBuildTextFile.lines.add('</body>');
 meBuildTextFile.lines.add('</html>');
end;

The application should now, when you look at Tmp.htm in a web browser, give you a way to look at 4 jpegs, nicely set out in two columns.

Some details about SaveMemo

Here are some details about the way we programmed the SaveMemo procedure, which I didn't go into earlier....

Because it is simpler, in this application, we will determine the name of the file the memo is saved to within the code of the application. I.e., the user will have no direct choice in the matter at the time the file is saved. (In due course, the choice of filename can be made be "settable" in the ini file.)

Also to make this simpler, we will set up a folder for the memo (and other things) to be saved in, the folder specified in sAssmFldr. We assigned a value to sAssmFldr within the code. In due course, it will be possible to change that in the ini file.

(Detail: Also, against the day when we want to use something other than the .exe's folder, just after "sPathToAssmFldr:='';", add the following to insure a "\" at the right hand end of the path name...

 if sPathToAssmFldr<>'' then if
    sPathToAssmFldr[length(sPathToAssmFldr)]<>'\' then
      sPathToAssmFldr:=sPathToAssmFldr+'\';

Onward!...

Changing what is in the TmpImg01.jpg, etc, files...

Once again, before we can go very far, we need to set up some resources. Remember: This tutorial arises along the way of my creation of a program to scan some folders, and make copies of the most recent files in each, and then generate two .htm files based on those images and when the files changed.

Note that all of this is being done in ways which make it easy to "move things", if the way I've set things up doesn't suit my needs the next time I want to deploy a copy of the application. For the purposes of completing the tutorial, I suggest you put things where I'm about to describe I put them. With some OSs, working in the root can be a pain, hence, in part, the subordinate folder "DE48DataRoot".

In the root of your C: drive, create a folder "DE48DataRoot".

In that, create a folder called "pseudoroot".

Within that, create the following folders. (You could start with just one of them, and just one .jpg (see below))

(The names, "IVA152", etc, come from the IDs I've assigned to my IPCams. And no, I don't have 213 of them. The IVA prefix is shared!)

Within each of THOSE put some .jpg files. It would be best if you didn't use the same one over and over. In the first, have one called Img152.jpg, in the second one called Img155.jpg, and so on. (Once we have the program doing simple things, we may go on, make it fancier. For now one .jpg per folder will suffice.)

A first stage

To made a VERY crude start, create a procedure CopyFiles as follows, and put a call of it in DoIt, just before the FillMemo call.

Whenever you use this technique, starting with something crude, and building it up....

Beware starting off before you know where you are going. PLAN ahead (before you start building), but BUILD by stages.

Before I show you the crude CopyFiles, a reminder of what it needs to do....

We are writing an application which will "harvest" images from a repository on a hard disk. They will be arriving their by FTP from various IPCams capable of writing to an FTP server... but where they are coming from is a mere detail, as far as DE48 is concerned. DE48 does need to know where they are arriving. It needs to know that new ones come in from time to time, and that it is DE48's job to maintain copies of the current most recent file in each source folder in one "Assembly Area" (the path to that being given to DE48 in sPathToAssmFldr). (The "most recent file in folder" element of this will probably not be addressed in this tutorial. We will only give DE48 a way to keep copying something, chosen more or less at random, from each of the source folders.

So. Strip the requirements down a bit. Say, for now, that CopyFiles only needs to copy four files. That it will be the same four files each time CopyFiles is called. That they will come from locations we can hard-code into the application.

Those requirements can be met by the following simple little thing, which is just four calls of the built in "copyfile" procedure....

procedure TDE48f1.CopyFiles;
begin
 copyfile('C:\DE48DataRoot\pseudoroot\IVA152\Img152.jpg',
              sPathToAssmFldr+'Images\TmpImg01.jpg');
 copyfile('C:\DE48DataRoot\pseudoroot\IVA152\Img152.jpg',
              sPathToAssmFldr+'Images\TmpImg02.jpg');
 copyfile('C:\DE48DataRoot\pseudoroot\IVA152\Img152.jpg',
              sPathToAssmFldr+'Images\TmpImg03.jpg');
 copyfile('C:\DE48DataRoot\pseudoroot\IVA152\Img152.jpg',
              sPathToAssmFldr+'Images\TmpImg04.jpg');
end; 

An aside: CopyFile is actually a function. It returns "true" or "false". It can be used, as I did above, as a procedure, and the returned value is just "throw away". The code really ought to be improved along the following lines...

....
  if (copyfile( SOURCE, DESTINATION )=false) then
     showmessage('An invalid copy was requested');
...

To finish this project: We'll need to set up a system to make "DoIt" happen over and over. (Trivial.) For it to do something actually useful, the matter of choosing as source the most recent file in the folder will have to be dealt with. To make this a "good" application, we will have to build in ways for the user to have more or fewer images in the display, and to (easily- depending in part upon an ini file.) change the source folders visited. (We're about to embark upon those two challenges.) (And, to fully meet what was set out as our goals, we also need to create the secondary .htm file, about which I've so far said little.)

Building in flexibility

While this application may not interest a wide audience, the programming techniques I am about to explain have very wide applicability. Pretend for a moment that you do want the file harvesting application!

Arrays will be an important part of programming for flexibility. For instance, as I hope you would immediately anticipate, will there be an array of strings, holding the "where" of the source folders.

To make it easy to "tweak" the program, near the beginning of the code we will establish a constant which tells all that follows the maximum index which will be wanted for any of those arrays. The following goes just after the "Uses" clause...

const vers='21Sept13';
      //Code started 21 Sep 13, NC
      kMaxArr=5;

In the third line, we set kMaxArr to five. We are saying that there won't be more than 6 of the things that crop up in the program in "sets" (using that term in the general sense, not the Pascal sense.) We'll revisit this in a moment.

Note, by the way, the "=" rather than the more usual (in Pascal) ":=" for assignment.

In passing: The first line of my "const" clause is just one of my conventions: The constant "vers" holds a string, usually a date, to identify the version of this edition of the code. The next line, just a comment, is there because it amuses me to know how old some of my projects are. It is the "kMaxArr" line that is "special" to DE48. "k" for "constant" ("c" already being "taken" for other things.)

To the "private" section of the declaration of our TDE48f1 class (which you will have if you named the main form for this project DE48f1, which makes sense if you've called the project "DE48")... add the new lines shown in...

  private
    { private declarations }
    sImageFolder:array[0..kMaxArr]of string;
    sPathToImages:string;
    procedure DoIt;
    procedure FillMemo;

By setting kMaxArr to 5 earlier, we've set the stage so that the above creates...

sImageFolder[0];
sImageFolder[1];
sImageFolder[2];
sImageFolder[3];
sImageFolder[4];
sImageFolder[5];

We will also have a variable, which will never hold more than kMaxArray, to tell anyone who needs to know just how many of the available array elements are in use at the present time. In our crude example, if it were done with arrays, because we are harvesting four images, we'd need four array elements. But, as these things are usually numbered from zero (and as we are doing "the usual") the number in the variable would be 3. We'll call the variable bMaxArrElInUse. ("(Index of the) Maximum Array Element In Use (at this time)".)

(Add "bMaxArrElInUse:byte;" to the variable declarations at the start of the code, and "bMaxArrElInUse:=3;" to "FillVariousVariables", so that they are in place when we come to use them.)

Also in "FillVariousVariables", put the following...

  sImageFolder[0]:='C:\DE48DataRoot\pseudoroot\IVA152\';
  sImageFolder[1]:='C:\DE48DataRoot\pseudoroot\IVA155\';
  sImageFolder[2]:='C:\DE48DataRoot\pseudoroot\IVA199\';
  sImageFolder[3]:='C:\DE48DataRoot\pseudoroot\IVA200\';

(Be sure to include the terminal "\")

(If you were sure that all of the source folders for the images would be on a common path, you could, of course, store that in a variable of its own.)

Now that we've provided a filled "sImageFolder[]" array, we can make CopyFiles easier to read. And easier to read equals "less capable of hiding a bug". The new version is also, of course, more flexible.

procedure TDE48f1.CopyFiles;
begin
 copyfile(sImageFolder[0]+'Img152.jpg',
              sPathToAssmFldr+'Images\TmpImg01.jpg');
 copyfile(sImageFolder[1]+'Img155.jpg',
              sPathToAssmFldr+'Images\TmpImg02.jpg');
 copyfile(sImageFolder[2]+'Img199.jpg',
              sPathToAssmFldr+'Images\TmpImg03.jpg');
 copyfile(sImageFolder[3]+'Img200.jpg',
              sPathToAssmFldr+'Images\TmpImg04.jpg');
end;

What else can we make more elegant?

It won't be immediately obvious why I am doing it this way, but think about it later, and you should see the reason... even if the full benefits don't arise until the application is extended to find the newest file in each source folder.

I'm going to use a function to supply the "Img152.jpg", "Img155.jpg", etc bits in the above. I'm calling it "sFileNSource", for "File NAME, Source file". And when the function is called, a parameter will be passed.

function TDE48f1.sFileNSource(bWhich:byte):string;
begin
  case bWhich of
    0:sFileNSource:='Img152.jpg';
    1:sFileNSource:='Img155.jpg';
    2:sFileNSource:='Img199.jpg';
    3:sFileNSource:='Img200.jpg';
      //(Should also have "el se" clause to catch inadvertent overflows
      //  which could easily arise during careless tweaking of program;
    end;//case
  end;

With sFileNSource available, our procedure CopyFiles becomes even more elegant. (There's no need to understand the snippet below, if trying to do so doesn't amuse you. The previous, crude, code does the same job.)

procedure TDE48f1.CopyFiles;
begin
 copyfile(sImageFolder[0]+sFileNSource(0),
              sPathToAssmFldr+'Images\TmpImg01.jpg');
.....

(An aside: I chose the name for CopyFiles without sufficient care. Seeing as Lazarus/Delphi/Pascal has the built-in function copyfile (no "s"), it would be as well to use a more-different name for our Copy-the-fileS procedure.)

CopyFiles is, of course, now just one step away from becoming very elegant...

procedure TDE48f1.CopyFiles(bHowMany:byte);
var bCount:byte;
function sDestFN(bWhich:byte):string;
//There's no need to build in flexibility in the
//  naming of these files, they are "internal",
//  in a dedicated folder of sAssmFldr
  begin
    case bWhich of
    0:sDestFN:='Images\TmpImg01.jpg';//This can also be done "cleverly"
    1:sDestFN:='Images\TmpImg02.jpg';// ... but why bother, here?
    2:sDestFN:='Images\TmpImg03.jpg';
    3:sDestFN:='Images\TmpImg04.jpg';
    //(Should also have "el se" clause to catch inadvertent overflows
    //  which could easily arise during careless tweaking of program;
    end;//case
   end;//sDestFN
begin
 for bCount:=0 to bHowMany do begin
 copyfile(sImageFolder[bCount]+sFileNSource(bCount),
              sPathToAssmFldr+sDestFN(bCount));
    end;//for
end;

An example of using an ini file

There are many things in this application which are, at the moment, hard-coded. Many of them should be made "settable" via an ini file. We'll do what is necessary in two cases, and leave changing the others to the student as an exercise, if interested.

The application fetches images from a set of "source" folders.

The application knows where source folders are from what is in various elements of the string array sImageFolder[]. (Note that every value in those array elements should end with a "\".)

Those variables get filled with values during the one call of FillVariousVariables, early in the program's execution, by simple hardcoded assignment statements.

We're going to replace....

  sImageFolder[0]:='C:\DE48DataRoot\pseudoroot\IVA152\';
  sImageFolder[1]:='C:\DE48DataRoot\pseudoroot\IVA155\';
  sImageFolder[2]:='C:\DE48DataRoot\pseudoroot\IVA199\';
  sImageFolder[3]:='C:\DE48DataRoot\pseudoroot\IVA200\';

... with something cool.

It will...

Look to see if there is an .ini file in the folder the .exe file was in.

If so, it will open that file, and attempt to fetch some data from it. If the ini file is not found, the program still run okay.

The ini file is just another text file, but if the right things are in it (more on this in a moment), your application will draw things from it.

So... First put "INIFiles" in the "uses" clause at the top of the code.

Next get the system to start an OnFormActivate event handler for you. (Object Inspector, select the form, go to Events tab, double-click on the OnFormActivate's edit box.)

Revise the OnFormActivate shell to make it...

procedure TDE48f1.FormActivate(Sender: TObject);
var dfIniFile:TInifile;
begin
  dfIniFile:=TIniFile.Create('DE48ini.txt');
  with dfIniFile do begin
   sImageFolder[0]:=ReadString('SourcePaths','1','C:\DE48DataRoot\pseudoroot
\IVA152\');
   sImageFolder[1]:=ReadString('SourcePaths','2','C:\DE48DataRoot\pseudoroot
\IVA155\');
   sImageFolder[2]:=ReadString('SourcePaths','3','C:\DE48DataRoot\pseudoroot
\IVA199\');
   sImageFolder[3]:=ReadString('SourcePaths','4','C:\DE48DataRoot\pseudoroot
\IVA200\');
   end;//with...
  dfIniFile.Free;
end;

Take OUT of the code you had previously the lines in FillVariousVariables which gave values to sImageFolder[0], sImageFolder[1], etc.

Run the application again. You should find that it runs exactly as it did before.

Here's what's happening...

When the program comes to...

dfIniFile:=TIniFile.Create('DE48ini.txt');

...it looks for a file called DE48ini.txt. (It looks in the folder the application's .exe is in, unless told to look elsewhere, e.g. 'C:\Misc\DE48ini.txt').

In our case, at the moment, it finds none. It doesn't let this be a problem.

It then sees....

sImageFolder[0]:=
   ReadString('SourcePaths',
   '1',
   'C:\DE48DataRoot\pseudoroot\IVA152\');

...and, there being no ini file, it ignores most of that, and treats it as if it said...

sImageFolder[0]:=
   'C:\DE48DataRoot\pseudoroot\IVA152\');

.. which is the line we took out of our crude version of the program!

Now... what if there IS an ini file present?

The application looks through the ini file, looking for a line that says....

[SourcePaths]

If it finds one, then in that section of the ini file, the application looks for a line that says....

1=

If it finds a line starting "1=", in the "[SourcePaths] section of the ini file, then the application will fill sImageFolder[0] with whatever came after the "1=".

That's "all" there is to using ini files to fill variables as a program is starting up!

Where do ini files come from?

The simple way to provide an ini file, to, in our case, change where the application looks for the images to copy, is to type one by hand into a text editor. One for DE48, as it stands at this point in the tutorial, but to make the application look in a different place for the sources for the first two images, would be...

[SourcePaths]
1=C:\DE48DataRoot\DifferentSet\IVA189\
2=C:\DE48DataRoot\DifferentSet\IVA180\
3=C:\DE48DataRoot\pseudoroot\IVA152\
4=C:\DE48DataRoot\pseudoroot\IVA155\

However, there is another route available to you.

You can get your application to save a datafile (or over-write an existing one) with code like the following. Slotting the code into you code so that it executes at the right time is a little tricky, but not rocket science.

In DE48, I've used the crudest possible "solution": I just put the following code in an OnFormDestroy handler. Because I have done it this way, the ini file will be created, if none existed before, or over-written (without warnings) if one did exist before. Now... at the moment, over-writing the file is, to a degree, pointless. (It does provide an easy way to get a syntactically correct .ini file, as a starting point for tweaking.) However, we will add a little extra bit in a moment, and once we have done that, over-writing the previous ini file each time you exit makes sense. Here's the code so far...

procedure TDE48f1.FormDestroy(Sender: TObject);
var dfIniFile:TInifile;
begin
 dfIniFile:=TIniFile.Create('DE48ini.txt');
 with dfIniFile do begin
  WriteString('SourcePaths','1',sImageFolder[0]);
  WriteString('SourcePaths','2',sImageFolder[1]);
  WriteString('SourcePaths','3',sImageFolder[2]);
  WriteString('SourcePaths','4',sImageFolder[3]);
  end;//with...
 dfIniFile.Free;
end;

Now, to illustrate a case where there's a point to re-writing the ini file at the end of each execution of the application, we're going to add a feature commonly found in applications... DE48 will be made to remember where it was on the desktop the last time it was opened.

"The secret" is in some of the properties of the form: top, left, height, and width.

Add the following to the FormDestroy handler. (Note, by the way, the WriteInteger procedure, which is very like the WriteString, but it writes integers, not strings. Duh.)

  WriteInteger('PosnAndSize','Top'   ,DE48f1.top);
  WriteInteger('PosnAndSize','Left'  ,DE48f1.left);
  WriteInteger('PosnAndSize','Height',DE48f1.height);
  WriteInteger('PosnAndSize','Width' ,DE48f1.width);

(All the names... "PosnAndSize", "Top", "Height" are entirely up to you. (The property names, "top", "height", etc after the DE48f1, are not... they are defined properties.)

If you now run the application again and close it, you will find new entries in the ini file. Do it again, but move and re-size DE48's window on the desktop before you close it, and the values of top, etc, will change, if you look again at the ini file's contents.

We'll come to making the application USE the values in the ini file in a moment. But first a warning: It can get quite confusing, working with ini files. Is what you are seeing in your text editor actually what is now in the ini file? It is easy to get confused.

Also, beware: Save and changes you want to make in the ini file from your text editor AFTER closing the application which will read it, or sometimes the application doesn't see those changes. (It may simply be a matter of the application overwriting the changes you made, as the application closes... but I think it is more subtle than that.)

Anyway, onward... let's make our application respond to the values in the PosnAndSize section of the ini file.

Easy! Just add the following to the OnActivate handler. The numbers at the ends of the lines are what will be used if the ini file does not yet have an entry for that property.

   DE48f1.top   :=ReadInteger('PosnAndSize','Top'   ,10);
   DE48f1.left  :=ReadInteger('PosnAndSize','Left'  ,20);
   DE48f1.height:=ReadInteger('PosnAndSize','Height',300);
   DE48f1.width :=ReadInteger('PosnAndSize','Width' ,500);

A frill...

Remember: This application is meant to be harvesting copies of images which are appearing in various folders due to devices beyond the ken of DE48. The images "just appear". And then DE48 assembles a web page to display copies of the images.

A feature I will not be explaining in the tutorial... eventually, I will be selling an improved DE48 with this feature... is that the images shown on the web page will be the newest image in each folder, when the folder was checked. So it makes sense to include the datestamp of the image in the web page display. Not terrible difficult, though spelling out all the details may take a few words.

We're going to start some changes to the sFileNSource function. The function will continue, directly, to return the name of a file. But, in the parameters, I am adding a "var" parameter, dtDateStamp, which will, during the execution of the function call, be filled with the datestamp on the file fetched. Well. It will, in the final version of this application. For now the "datestamp" will be an arbitrary and unchanging number, hardcoded into the internal code of sFileNSource. However... even though that number is arbitrary, it will be reported on the generated web page.

To accomplish these things, I created a global array, dtDateStamp[], and sFileNSource became....

function TDE48f1.sFileNSource(bWhich:byte; var
dtTmpDateStamp:TDateTime):string;
begin
  case bWhich of
    0:begin
      sFileNSource:='Img152.jpg';
      dtTmpDateStamp:=14;
      end;
    1:begin
      sFileNSource:='Img155.jpg';
      dtTmpDateStamp:=15;
      end;
    2:begin
      sFileNSource:='Img199.jpg';
      dtTmpDateStamp:=16;
      end;
    3:begin
      sFileNSource:='Img200.jpg';
      dtTmpDateStamp:=17;
      end;
      //(Should also have "el se" clause to catch inadvertent overflows
      //  which could easily arise during careless tweaking of program;
    end;//case
  end;

... and in CopyFiles, we add a line...

begin
 for bCount:=0 to bHowMany do begin
     copyfile(sImageFolder[bCount]+sFileNSource(bCount,dtTmpDateStamp),
              sPathToAssmFldr+sDestFN(bCount));
     dtDateStamp[bCount]:=dtTmpDateStamp;// <<<
    end;//for
end;

That leaves us in the position of being able to improve FillMemo as illustrated by the fragment below....

meBuildTextFile.lines.add(sLineOfHTML('02','R'));
 meBuildTextFile.lines.add('<br clear=left><br clear=right>');
 meBuildTextFile.lines.add('<p>The next two images had datestamps of: '+datetostr
(dtDateStamp[2])+
                 ' and '+datetostr(dtDateStamp[3])+'</p>');
 meBuildTextFile.lines.add(sLineOfHTML('03','L'));

Doing it over and over...

Well, we're nearly there. The application does much of what I wanted it to do... if I click the "DoIt" button. And then it copies some files from the source folders to the "scratch" folder used by the .htm page. It creates the .htm page we wanted.

What it doesn't do, yet, is execute the "DoIt" over and over. Arranging this is easy!

Put a timer component on your form. You should find it as the first component on the "System" tab.

Make the OnTimer handler....

procedure TDE48f1.Timer1Timer(Sender: TObject);
begin
  buDoIt.enabled:=false;//not essential... see below.
  DoIt;
  buDoIt.enabled:=true;
end;

Set the timer's "interval" property to 5000.

And run the program. It wasn't essential to disable the DoIt button while the OnTimer handler was executing "DoIt", but you should see the button's state "flicker" every five seconds because of the new code. (Don't leave the application running for long, in this state, will you? That would be a lot of file writing inflicted on your disk.)

We're Done!

There you have it! A little bit of code which illustrates various techniques. I hope you will find them useful in your own projects.

I'm off to create the bigger, better version of this, so I will have a webpage with recent pictures from my cameras available to me! Don't hold your breath for the commercial version, sorry. The fun part is writing the "good enough" version to meet my need. That takes maybe 20% of the time it takes to complete a commercial product, and I just don't get enough people buying my shareware to encourage me to rush to spend the time on the CCTV monitoring application. But who knows... maybe someday...





            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?"

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 Lazarus 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. Mostly passes. There were two "unknown attributes" in Google+ button code. Sigh.


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