January 2, 2016

DirWatch: Delphi 10 in action

In this article, I will show you how I used Delphi 10 Seattle to create a nice application aimed at watching a directory tree. Source code provided, see at the end.


Background:


I frequently use shared directories as repository for files. Some user on the network just drops the file in the directory where I can grab it. The user is supposed to notify me when he has dropped a file.  Unfortunately some user forgets to notify e and the file stay there for a long time before I notice it myself. In other situation, I like to know which files are affected by a given application or by a setup.


DirWatch will be of great help in those situation and many others. DirWatch will monitor a whole directory tree, even a full disk, for changes and send me an email when a change is detected. A change means a file or subdirectory is added / renamed / deleted / modified.


How it works:


Part 1: Getting notifications


Using Windows, it is actually very easy to be notified of changes in the file system. There is a specific and simple API for the purpose. The main API functions are FindFirstChangeNotification, FindNextChangeNotification and ReadDirectoryChanges. The actual work is done by Windows!


This notification API works much like listing a directory: you do a FindFirst, then if successful, you read the changes that occurred and then call FindNext to be notified for the next change. As simple as that.


OK, things can be a little bit more complex if you want your application to stay responsive you should resort to asynchronous Input/Output and multithreading. That code already exists on the web in numerous instances. I selected to use DWScript implementation because it is simple and efficient and also because I use DWScript for other project and find this open source product excellent. DWScript is available under Mozilla Public License Version 1.1.


dwsDirectoryNotifier is available from DWScript project (https://bitbucket.org/egrange/dwscript). Direct link to the source file at the time of writing is


Part 2: Sending an email


To programmatically send an email, you have many solutions available. In my opinion, the best solution is to directly address the email server, either company email server or Internet Service Provider email server. This may sound difficult but it is not much. An email server makes use of a protocol named SMTP, just like your browser make use of a protocol name HTTP. SMTP stands for Simple Mail Transfer Protocol and as its name implies, it is simple! SMTP is a text based protocol and involves the exchange of a few messages to send an email.


SMTP becomes a little bit more complex when you start talking authentication, compression, and security but it basically remains simple.


I have implemented the SMTP protocol in an open source freeware I published nearly 20 year ago. I mean the Internet Component Suite (ICS for short). See http://wiki.overbyte.be. ICS is a huge component library. We will only use the SMTP component.


ICS (Internet Component Suite) is available from http://wiki.overbyte.be/wiki/index.php/ICS_Download and also can be installed directly in Delphi 10 Seattle using Embarcadero GetIT (Delphi menu Tools / GetIi Package Manager, type ICS into the search box and select "ICS for VCL").


Part 3: be discreet


When I use a utility which I don't have to interact with, I like to have it very discreet. I don't even want to see it on my screen. There are two good solutions for this: write a service application or write a "tray-icon" application. I selected the second which is slightly easier to write and debug.


A tray-icon application is a normal Windows application which once minimized is only noticeable by its icon displayed in the systray (You know, this little area usually on the bottom right of the screen where the time is displayed).


Using Delphi 10, you create an application as a "VCL form application" and drop a TTrayIcon component on it. You have just two event handler to write: FormResize and TrayIconDoubleClick. FormResize is used to hide the form once it is minimized and TrayIconDoubleClick is used to make the form visible again when the user double clicks on the small icon.


There is a side problem when building a tray-icon application: you need a small 16x16 icon. If you don't create your own 16x16 icon, Windows will shrink the large icon to create the small one. The result is frequently horrible.


To create the multiple resolution icon file, I used IconMagick (http://www.imagemagick.org) command line utility "convert" to combine icons created using my favorite paint application.


convert DirWatchIcon32x32.ico DirWatchIcon16x16.ico DirWatchIcon.ico


Once DirWatchIcon.ico was created, I associated it with the application using Delphi project options dialog.


Please note that TrayIcon component dropped on the form must be loaded with the DirWatchIcon16x16.ico file otherwise window will use the 32x32 icon file resized to 16x16 which gives an horrible result.


Part 4: Work offline


I wanted to be notified even for changes occurring while DirWatch was not running. This is especially useful if the watched directory is not local but a shared resource on a file server.


When DirWatch starts, I create notifications for the changes occurred since last run by comparing the list of files actually on disk with the list of files that where on disk when DirWatch was stopped.


This means that at program startup, DirWatch enumerates all the files and directories in the watched directory. This could take some time if the directory contains millions of files!  On my hard drive with about one million files, it takes approximately 15 seconds to enumerate the files, compare that list with the previous list and send the email for the changes.


The implementation I made makes use of Delphi 10 Seattle generics. Internally the list of files is not a list but a dictionary. This gives fast access to each file data. Each file (Or directory entry) is stored with his name, attributes, size and timestamp. This is used to find out what has changed with a very low chance to miss something. Of course it could be possible to write a program which saves the timestamp of a file, update some bytes in the file without changing the size and then restore the timestamp. Actually this never occurs in real life!


It is interesting to look at the code I wrote for the dictionary. There are a few involved classes: TDirDictionary, TDirEntry, TDirDictionaryPair, TDirDictionaryObject and TDirDictionaryEnumerator.


I implemented TDirDictionary by delegation instead of inheritance. I frequently do that to have complete freedom about the interface I expose. In most cases, delegation is a better encapsulation that inheritance. Look at the source code for details and you'll quickly see the code is really very simple.


Part 5: Data persistence


DirWatch must be configured with some parameters. Of course, the directory which must be watched but also the SMTP server information (Those information are the same that Outlook uses for a classical POP3/SMTP account).


I also like that my applications remember where their window was and which size they had when I run the application again.


For all those purposes, I like to use the good old INI file. It is easy to use within the program and easy you me to edit with my favorite text editor. I store the INI file in the "Loacl AppData" folder in the user profile.  The Windows API function SHGetFolderPath is used to query the exact location of the "Local AppData" folder.


Part 6: Hide sensitive data


Sending an email may sometimes involve a usercode and password that the DirWatch must know. As explained above, those values are stored in the INI file and this is a problem if they are saved in clear text.


I wrote two functions Decrypt and EncryptRestore for the purpose. One trick is used to allow encryption without using a special program. When the data is enclosed in angle bracket in reverse order, then it's assumed not encrypted but needing to be encrypted. Then once encrypted the resulting value is saved between angle brackets.  So "]MyName[" will result in "MyName" and a flag is set so that EncryptRestore will produce "[Qyaze2==]" which is an encrypted value. If no angle bracket is used at all, then the value is assumed not encrypted. Here in this implementation I used very weak encryption: base64 encoding. In a fully secured application you should use a real encryption mechanism providing strong security.


Part 7: Command line options


Some information cannot be stored in the INI file and yet be changed at run time. For example the name and location of the INI file itself can be changed.
-i IniFileName        Select INI file
-h            Show this help
-m            Start minimized
-a AppName        Select application name
-c CompanyName    Select company name


To parse command line argument, I used code largely inspired by by code from Stehan Huberdoc  (http://stefan.huberdoc.at/comp/software/delphi/sandkasten.html) which is an implementation of the well-known GNU GetOpt command line parsing function (https://en.wikipedia.org/wiki/Getopt).


Part 8: Source code


Source code is split into two units: DirWatchMain.pas which is the main and only form of the application and DirWatchProcess.pas which contains the code which does the actual work without any user interface. Of course other units are used as explained above: some from ICS, some from DWScript and GetOpt.


Download links:




Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be


April 17, 2014

ICS updated for Delphi XE6

ICS has been updated for Delphi XE6. No new feature, just a simple update for the latest Delphi version. VCL and FMX supported for desktop platforms. Mobile still under way.

Download as usual. See http://wiki.overbyte.be


Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be

March 18, 2014

On the fly form

This article explain how to create a form on the fly. Such a form is created by code, without using the designer.

You will surely ask why you would do that! Granted the Delphi form designer is very easy to use. But in some cases it is not practical because it creates several files and makes more difficult, for example, to hide the form in a component.

This is exactly the case I had: in a component I needed a small helper form. I wanted the code to be in the component source itself. You may already use an "on the fly" form without knowing: InputQuery and InputBox are already such a form. Their code is located in the Dialogs unit.

Actually, there is not much magic to use to create an "on the fly" form. After all a form is just an object as any other object. The only special thing is related to the constructor. The form constructor insist on loading a form from a resource as it had been created with the designer and his DFM file. If there is no DFM and you call Create, you get an exception EResNotFound with a resource name equal to your form class name.

Instead of Create, you have to call CreateNew. This is easy but somewhat misleading. This is why the code I present below override the standard constructor named Create and call CreateNew. This way you can use the standard constructor without worrying.

The sample code I use to demonstrate the "on the fly" form is very easy. The form is a simple form named TComboBoxInputForm with a combobox and a pair of buttons for OK/Cancel. I created an Execute method which calls ShowModal after having initialized the combobox with an array of const values passed as argument.

Here is the code showing the use:

procedure TForm1.Button1Click(Sender: TObject);
var
    Form    : TComboBoxInputForm;
    AResult : String;
begin
    Form := TComboBoxInputForm.Create(Self);
    try
        if Form.Execute(['Delphi', 123, 2.3, TRUE],
                        Edit1.Text, AResult,
                        'Select value') then
            Edit1.Text := AResult;
    finally
        FreeAndNil(Form);
    end;
end;

This code uses TComboBoxInputForm to ask the user to select a value in a list.

The source code is very simple and looks like any other form, except since there is no DFM file, all used components are created and initialized from the constructor.

type
    TComboBoxInputForm = class(TCustomForm)
        ComboBox     : TComboBox;
        OKButton     : TButton;
        CancelButton : TButton;
    protected
        procedure OKButtonClick(Sender: TObject);
        procedure CancelButtonClick(Sender: TObject);
    public
        constructor Create(AOwner : TComponent); override;
        function Execute(const Values   : array of const;
                         const ADefault : String;
                         out   AResult  : String;
                         const ACaption : String = '') : Boolean;
    end;

    TForm1 = class(TForm)
        Button1: TButton;
        Edit1: TEdit;
        procedure Button1Click(Sender: TObject);
    end;

var
    Form1: TForm1;

implementation

{$R *.dfm}

{ TComboBoxInputForm }

constructor TComboBoxInputForm.Create(AOwner: TComponent);
begin
    // We need the following lines to avoid the EResNotFound exception
    // because we have no DFM resource for our form
    GlobalNameSpace.BeginWrite;
    try
        CreateNew(AOwner);
    finally
        GlobalNameSpace.EndWrite;
    end;
    BorderStyle          := bsToolWindow;
    ComboBox             := TComboBox.Create(Self);
    ComboBox.Parent      := Self;
    ComboBox.Top         := 16;
    ComboBox.Left        := 32;
    ComboBox.Style       := csDropDownList;
    OKButton             := TButton.Create(Self);
    OKButton.Parent      := Self;
    OKButton.Top         := ComboBox.Top + ComboBox.Height + 8;
    OKButton.Left        := ComboBox.Left;
    OKButton.Width       := 50;
    OKButton.Caption     := '&OK';
    OKButton.Default     := TRUE;
    OKButton.OnClick     := OKButtonClick;
    CancelButton         := TButton.Create(Self);
    CancelButton.Parent  := Self;
    CancelButton.Top     := OKButton.Top;
    CancelButton.Left    := OkButton.Left + OKButton.Width + 16;
    CancelButton.Width   := OKButton.Width;
    CancelButton.Caption := '&Cancel';
    CancelButton.Cancel  := TRUE;
    CancelButton.OnClick := CancelButtonClick;
    ComboBox.Width       := OKButton.Width + CancelButton.Width + 16;
    ClientWidth          := ComboBox.Left + ComboBox.Width + ComboBox.Left;
    ClientHeight         := ComboBox.Top + OKButton.Top + OKButton.Height;
end;

procedure TComboBoxInputForm.OKButtonClick(
    Sender : TObject);
begin
    Close;
    ModalResult := mrOK;
end;

procedure TComboBoxInputForm.CancelButtonClick(
    Sender : TObject);
begin
    Close;
    ModalResult := mrCancel;
end;

function TComboBoxInputForm.Execute(
    const Values   : array of const;
    const ADefault : String;
    out   AResult  : String;
    const ACaption : String) : Boolean;
var
    I    : Integer;
const
    BoolToStr : array [Boolean] of String = ('FALSE', 'TRUE');
begin
    Caption   := ACaption;
    ComboBox.Items.Clear;
    I := Low(Values);
    while I <= High(Values) do begin
        case Values[I].VType of
        vtUnicodeString: ComboBox.Items.Add(Values[I].VPWideChar);
        vtBoolean:       ComboBox.Items.Add(BoolToStr[Values[I].VBoolean]);
        vtInteger:       ComboBox.Items.Add(IntToStr(Values[I].VInteger));
        vtExtended:      ComboBox.Items.Add(FloatToStr(Values[I].VExtended^));
        end;
        Inc(I);
    end;
    // Preselect default item
    I := ComboBox.Items.IndexOf(ADefault);
    if I >= 0 then
        ComboBox.ItemIndex := I;

    Result := ShowModal = mrOK;
    if Result then
        AResult := ComboBox.Text
    else
        AResult := ADefault;
end;






Follow me on Twitter
Follow me on LinkedIn
Follow me on Google+
Visit my website: http://www.overbyte.be