Gator remembers all your passwords!
  Gator is freeware
   for everybody!

Source code snippets

Running an external application (and waiting)

People have been asking me over and over: how can I run an external program from within a Delphi program? And also: how can I make the Delphi program wait until the external program is terminated?

For just running an external application (or opening a registered file, opening a folder, printing a file, and so on), there are several functions available. In most cases, the Windows API-function ShellExecute is used. It gives some degree of control and at the same time it's not to complicated. Some examples:


Launching an external program and waiting until it is terminated is quite another story. With Delphi 1, this was easily achieved with the function WinExec and then calling GetModuleUsage in a loop until the application terminated.

GetModuleUsage is not available in the 32 bit versions of Delphi.
Fortunately, we can tell if a process has completed by monitoring its process handle. That process handle can be obtained by using one of two Win32 API-functions to start the external program: CreateProcess or ShellExecuteEx.

The simplest method is: start the external application with ShellExecuteEx and then monitor the process handle with WaitForSingleObject.

I wrapped it all up in a ready-to-go mini tutorial project, which demonstrates the use of the functions discussed above. All necessary files are in EXEWAIT.ZIP, that you can find in our download section under "Mini Tutorial Projects".


Date and time of creation/modification of a file

The function FileAge() returns the date/time stamp of a file. The returned value is an integer number; it has to be converted to Delphi's TDateTime format (a floating point number) before you can use it. You can use the following code to test the funtions involved:

procedure TForm1.Button1Click(Sender: TObject);
var
  File_Name: string;
  DateTimeStamp: integer;
  Date_Time: TDateTime;
begin
  File_Name := 'c:\mydocuments\test.doc';
  DateTimeStamp := FileAge(File_Name);
  // FileAge returns -1 if file not found
  if DateTimeStamp < 0 then  
    ShowMessage('File not found')
  else begin
    // Convert to TDateTime format
    Date_Time := FileDateToDateTime(DateTimeStamp);
    Label1.Caption := DateToStr(Date_Time);
    Label2.Caption := TimeToStr(Date_Time);
  end;
end;

Find a file

The following procedure locates all occurrences of a given file and adds the complete path to a stringlist. Note that recursion is used: FindFiles calls itself at the end of the procedure.

Before calling FindFiles, the stringlist has to be created; afterwards, you must free the stringlist.

In StartDir you pass the starting directory, including the disk drive. In FileMask you pass the name of the file to find, or a file mask. Examples:
FindFiles('c:\', 'letter01.doc')
FindFiles('d:\', 'euroen??.dpr')
FindFiles('d:\projects', '*.dpr')

In order to test the procedure, you add some components to the form: two Edits, a Button, a TLabel and a ListBox.

implementation
....
var
  FilesList: TStringList;
...
procedure FindFiles(StartDir, FileMask: string);
var
  SR: TSearchRec;
  DirList: TStringList;
  IsFound: Boolean;
  i: integer;
begin
  if StartDir[length(StartDir)] <> '\' then
    StartDir := StartDir + '\';

  { Build a list of the files (not directories) 
    in directory StartDir                       }
  IsFound :=
    FindFirst(StartDir+FileMask, faAnyFile-faDirectory, SR) = 0;
  while IsFound do begin
    FilesList.Add(StartDir + SR.Name);
    IsFound := FindNext(SR) = 0;
  end;
  FindClose(SR);

  // Build a list of subdirectories 
  DirList := TStringList.Create;
  IsFound := FindFirst(StartDir+'*.*', faAnyFile, SR) = 0;
  while IsFound do begin
    if ((SR.Attr and faDirectory) <> 0) and
         (SR.Name[1] <> '.') then
      DirList.Add(StartDir + SR.Name);
    IsFound := FindNext(SR) = 0;
  end;
  FindClose(SR);

  // Scan the list of subdirectories
  for i := 0 to DirList.Count-1 do
    FindFiles(DirList[i], FileMask);

  DirList.Free;
end;

procedure TForm1.ButtonFindClick(Sender: TObject);
begin
  FilesList := TStringList.Create;
  FindFiles(EditStartDir.Text, EditFileMask.Text);
  ListBox1.Items.Assign(FilesList);
  LabelCount.Caption := 'Files found: ' + IntToStr(FilesList.Count);
  FilesList.Free;
end;
For a more robust code, you should at minimum check if the limit of the stringlist is not exceeded before you add to it (or use a try...except construction). Somebody might look for *.* starting in C:\
In that case, it's best to inform the user with a warning: "Too many files. Not all occurences are listed."



Get 2 months of Computer Shopper
absolutely FREE!
Visit FreeShop's Magazine Shop with over 200 popular titles to choose from!

FreeShop has over 1000 free and trial offers of magazines and catalogs, mostly for US-citizens. Click here.

Not a US resident? Then look at FreeShop's International Offers.


Only numerical input in a TEdit

If you want to limit the input of a TEdit to numerical strings only, you can discard the "invalid" characters in it's OnKeyPress event handler.

In order to test the following code examples, put a TEdit and a TLabel on a form. The TLabel's purpose is for monitoring what happens and to display error messages. Of course, in a real world application you could show error messages in a dialog box.

In the Edit's OnChange handler, you enter the code for displaying the text of the Edit in case a valid key is pressed:

procedure TForm1.Edit1Change(Sender: TObject);
begin
  Label1.Caption := Edit1.Text;
end;
Let's assume you only want to allow positive integer numbers. The code for the OnKeyPress event handler is as follows:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  // #8 is Backspace
  if not (Key in [#8, '0'..'9']) then begin
    Label1.Caption := 'Invalid key';
    // Discard the key
    Key := #0;
  end;  
end;
If you also want numbers with a decimal fraction, you must allow POINT or COMMA, but only once. For an international version that looks at the correct decimal separator, the code could be as follows:
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
begin
  if not (Key in [#8, '0'..'9', DecimalSeparator]) then begin
    Label1.Caption := 'Invalid key: ' + Key
    Key := #0;
  end
  else
    if (Key = DecimalSeparator) and
       (Pos(Key, Edit1.Text) > 0) then begin
      Label1.Caption := 'Invalid Key: twice ' + Key;
      Key := #0;
    end;  
end;
And here for a full blown version of the event handler, accepting decimal separator and negative numbers (minus sign: only accepted once, has to be the first character):
procedure TForm1.Edit1KeyPress(Sender: TObject; var Key: Char);
var
  ErrMess: string;
begin
  ErrMess := '';
  if not (Key in [#8, '0'..'9', '-', DecimalSeparator]) then
    ErrMess := 'Invalid key: ' + Key
  else if ((Key = DecimalSeparator) or (Key = '-'))
            and (Pos(Key, Edit1.Text) > 0) then
    ErrMess := 'Invalid Key: twice ' + Key
  else if (Key = '-') and (Edit1.SelStart <> 0) then
    ErrMess := 'Only allowed at beginning of number: ' + Key;
  if ErrMess <> '' then begin
    Label1.Caption := ErrMess;
    Key := #0;
  end;  
end;
How about giving that same behaviour to several TEdits on the same form? In the Object Inspector, you change the name of the event handler from Edit1KeyPress to Edit1_10KeyPress or something similar. Delphi automatically changes the name in the code editor, don't worry. Then, for each TEdit, you select it's OnKeyPress event and you select Edit1_10KeyPress from the listbox next to the event.

A global UNIT with functions and procedures

For complex projects, I advise you to put your "general" (globally used) functions and procedures in a separate unit. Such a "global" unit does not have a corresponding form. All other units that use the general routines, have to refer to your global unit through a USES-clause. An example of such a unit might look like this:

unit GlobalRoutines;

interface

uses
  SysUtils, Forms, Buttons, StdCtrls, Graphics, ..., ...;
procedure SetTime(Hour, Minute, Second, Sec100: Word);
procedure SetDate(Year, Month, Day: Word);
...
var
  Var1, Var2, Var3: string;
  DoIt: Boolean;
  ...

implementation

procedure SetTime(Hour, Min, Sec, Sec100: Word);
begin
  ...
end;

procedure SetDate(Year, Month, Day: Word);
begin
  ...
end;
...
...
end.

Note that you have to define all variables, procedures and functions that you want to call from other units, BEFORE the IMPLEMENTATION!

Set system-time and system-date

With the procedures SETTIME and SETDATE you can set the system time and date from within your Delphi application. Both procedures are written in Assembler. The Delphi-compiler contains an Assembler-compiler!

In the interface-section you define the procedures:

procedure SetTime(Hour, Min, Sec, Sec100: Word);
procedure SetDate(Year, Month, Day: Word);

In the 'implementation' you write...:
{ SetTime sets the time of the operating system.
  Valid parameters: Hour 0-23, Min 0-59, Sec 0-59 and 
  Sec100 0-99 (hundreds of a second)
  If the time is not valid, the function has no effect }
procedure SetTime(Hour, Min, Sec, Sec100: Word); assembler;
asm
  MOV CH,BYTE PTR Hour
  MOV CL,BYTE PTR Min
  MOV DH,BYTE PTR Sec
  MOV DL,BYTE PTR Sec100
  MOV AH,2DH
  INT 21H
end;

{ SetDate sets the date of the operating system. Valid parameters:
  Year 1980-2099, Month 1-12 en Day 1-31
  If the date is not valid, the function has no effect }
procedure SetDate(Year, Month, Day: Word); assembler;
asm
  MOV CX,Year
  MOV DH,BYTE PTR Month
  MOV DL,BYTE PTR Day
  MOV AH,2BH
  INT 21H
end;

Changing properties for all components of a certain type

To change the font color of all the labels of a form to a certain color, call the following procedure. In the call itself, you have to replace NewColor with an existing color, e.g. SetLabelsFontColor(clRed) sets all the labels' font color to red.

procedure TForm1.SetLabelsFontColor(NewColor: TColor);
var
  i: Integer;
begin
  for i := 0 to ComponentCount - 1 do
    if Components[i] is TLabel then
      TLabel(Components).Font.Color := NewColor;
end;
Of course, you can use this technique to change other properties of other components. To change the color of all edits, the code would be:
procedure TForm1.SetEditsColor(NewColor: TColor);
var
  i: Integer;
begin
  for i := 0 to ComponentCount - 1 do
    if Components[i] is TEdit then
      TEdit(Components).Color := NewColor;
end;

[ TOP ]   [ HOME ]   [ GUESTBOOK ]

Grab the Gator! It remembers passwords!
GATOR is freeware

Click Here!

⌐ Copyright 1999 
Studiebureau Festraets