A Directory Monitor Class For Delphi

There are times when a directory needs watched.  For reasons of its own, a program may need to know when a file is deleted, updated or renamed.  If .NET is involved, this is a trivial task.  Create an instance of the FileSystemWatcher; set some properties, and the task is completed.  If a non .NET solution is required (and regardless of the hype, some people haven’t embraced .NET in all of its glory.), the ReadDirectoryChangesW function must be used, however the documentation for ReadDirectoryChangesW is sketchy at best.

In an effort to make ReadDirectoryChangesW less of a mystery, this article will briefly explain the ReadDirectoryChangesW function, present a class for Delphi that encapsulates the ReadDirectoryChangesW function and provide an example program that shows how to use the class to monitor a directory for updates, deletes or renames of files in the directory.

(Note : All code in this article has been tested with Delphi 6 and 7, but I can’t think of any reason it won’t work with Delphi 4 or 5 if you declare the function prototype yourself. Delphi 8 is a pure .NET environment, so if you are using Delphi 8 use the FileSystemWatcher class.)

The ReadDirectoryChangesW Function.

At the heart of the class lies the ReadDirectoryChangesW function.  The function monitors a directory and alerts the caller when certain events occur.  It is important to note that the ReadDirectoryChangesW function will only work on Windows NT, 2000 and XP.  The function will not work on any other version of Windows.  The ReadDirectoryChangesW function is defined in the Windows.pas unit in Delphi as :

function ReadDirectoryChangesW(hDirectory: THandle;
                                lpBuffer: Pointer;
                                nBufferLength: DWORD;
                                bWatchSubtree: Bool; 
                                dwNotifyFilter: DWORD;
                                lpBytesReturned: LPDWORD;
                                lpOverlapped: POverlapped;
                                lpCompletionRoutine: FARPROC): BOOL;
                                stdcall;
</pre>

Lets examine the parameters of ReadDirectoryChangesW and look at how the function is called.

The parameters

The hDirectory is the handle of the directory to be watched.  This handle is obtained by calling the CreateFile function with the FILE_LIST_DIRECTORY flag.

The lpBuffer parameter is a pointer to a list of File_Notify_Information records.  The Delphi definition of File_Notify_Information is shown in Figure 1. The File_Notify_Information record contains four fields.  The first is NextEntryOffset. This field contains the number of bytes to the next record.  A value of zero indicates the last record in the list.  The second field is the Action field. The action field holds a value that describes the type of change that occurred.  The possible values for this field are described in Figure 2.  The third field is FileNameLength. The FileNameLength is the length of the filename in bytes (not chars).  This field does not include the null terminating character.  The last field in the File_Notify_Information record is FileName. This contains the name of the file that was effected.  There is no null terminator, and it is unspecified if the field will have the long or short filename, but during all of my testing it always had the long filename.

<pre> TFileNotifyInformation = Record NextEntryOffset : DWORD; Action : DWORD; FileNameLength : DWORD; FileName : Array[0..MAX_PATH] OfWCHAR; end; PFileNotifyInformation = ^TFileNotifyInformation; </pre>

Figure 1 - The File_Notify_Information record

FILE_ACTION_ADDED The file was added to the directory.
FILE_ACTION_REMOVED The file was removed from the directory.
FILE_ACTION_MODIFIED The file was modified. This can be a change in the time stamp or attributes.
FILE_ACTION_RENAMED_OLD_NAME The file was renamed and this is the old name.
FILE_ACTION_RENAMED_NEW_NAME The file was renamed and this is the new name.

Figure 2 - Possible values for the Action field.

The nBufferLength parameter is the size in bytes of lpBuffer.

The bWatchSubtree is a Boolean value that tells the function to watch sub-directories.

The dwNotifyFilter a bit mask that tells the function what events to look for. The values for this function are in figure 3.

FILE_NOTIFY_CHANGE_FILE_NAME Any filename change includes renaming, creating, or deleting a file.
FILE_NOTIFY_CHANGE_DIR_NAME Any directory name change includes creating or deleting a directory.
FILE_NOTIFY_CHANGE_ATTRIBUTES Any file attribute change.
FILE_NOTIFY_CHANGE_SIZE Any file-size change. Caching may delay this notification.
FILE_NOTIFY_CHANGE_LAST_WRITE Any change to the last write-time. Caching may delay this notification.
FILE_NOTIFY_CHANGE_LAST_ACCESS Any change to the last access time.
FILE_NOTIFY_CHANGE_CREATION Any change to the creation time.
FILE_NOTIFY_CHANGE_SECURITY Any security-descriptor change.

Figure 3 - dwNotifyFilter options.

The lpBytesReturned parameter is the number of bytes written to the lpBuffer parameter when thefunction is used in synchronous calls. This parameter is undefined when the function is used in asynchronous calls (read on for more information on synchronous versus asynchronous calls).

The lpOverlapped parameter is a pointer to an TOverlapped record.

The lpCompletionRoutine parameter is a function pointer to an IO completion routine.  An IO completion routine prototype is defined in figure 4.

<blockquote> <pre> procedure FileIOCompletionRoutine(dwErrorCode : DWORD; dwNumberOfBytesTransfered : DWORD; lpOverlapp : POVERLAPPED) Stdcall; </pre> </blockquote>

Figure 4 - An IO completion routine prototype.

Calling ReadDirectoryChangesW

There are two was to call ReadDirectoryChangesW, synchronous or asynchronous. As you might expect when calling ReadDirectoryChangesW synchronously it does not return until one of the events you are looking for happens. This is the easiest way to call ReadDirectoryChangesW but I have found it to be the least effective way to use the function.  I have included an example project that show how to use ReadDirectoryChangesW synchronously. The TDirectoryMonitor class uses the asynchronous method to call ReadDirectoryChangesW.

The TDirectoryMonitor class.

The TDirectoryMonitor class is described in figure 5. The TDirectoryMonitor class hides all of the details about ReadDirectoryChangesW from the caller.  The class exposes only two methods, four properties and one event. The two methods are Start and Stop. The Start method begins directory monitoring; the Stop method ends the monitoring. The properties are DirectoryToWatch, WatchSubFolders, Options, and WorkerThread. The DirectoryToWatch property defines the directory to watch.  The WatchSubFolder property defines if the class will notify the caller if an event occurs on a sub-directory.

<blockquote> <pre> TDirectoryMonitor = Class(TObject) Private FDirectoryToWatch : String; FWorkerThread : TWorkerThread; FOnDirectoryChange : TDirectoryChange; FWindowHandle : THandle; FOptions : TActionsToWatch; FWatchSubFolders : Boolean; procedure InternalWinProc(var Msg : TMessage); procedure SetDirToWatch(Value : String); Public procedure Start; procedure Stop; Constructor Create; Destructor Destroy; override; Property WorkerThread : TWorkerThread Read FWorkerThread; Property DirectoryToWatch : String Read FDirectoryToWatch Write SetDirToWatch; Published Property OnDirectoryChange : TDirectoryChange Read FOnDirectoryChange Write FOnDirectoryChange; Property Options : TActionsToWatch Read FOptions Write FOptions; Property WatchSubFolders : Boolean Read FWatchSubFolders Write FWatchSubFolders Default True; end;

Figure 5 – The TDirectoryMonitor Class.

The two less obvious properties are the OptionsOptions and the WorkerThread. The Options property is a set of TActionToWatch. TActionToWatch is an enumerated type that corresponds to the constants defined in figure  3. This property is not defaulted. It is left as an exercise for the reader to add defaults. The definition of TActionToWatch is shown in figure 6.

<pre>
TActionToWatch = (awChangeFileName,awChangeDirName,awChangeAttributes,awChangeSize,
awChangeLastWrite,awChangeLastAccess,awChangeCreation,awChangeSecurity);

TActionsToWatch = Set Of TActionToWatch;
</pre>

Figure 6 – TActionToWatch

The WorkerThread property is a class of TWorkerThread. TWorkerThread is a descendant of TThread. Why a thread? To explain that a fuller explanation of ReadDirectoryChangesW is needed. The ReadDirectoryChangesW function can be called one of two ways, synchronously or asynchronously. If called synchronously, the ReadDirectoryChangesW funcion doesn’t return until an event occurs. An asynchronous call the function return immediately and event notification is done through a completion port, a callback function, or overlapped I/O. To be able to receive these asynchronous notifications the thread that calls ReadDirectoryChangesW must be put in an alertable state. The up shot of both of these methods is that the calling thread is stopped until an event occurs. For a component to suspend the main thread is at best unexpected, at worst unacceptable. The WorkerThread takes care of this problem by creating a new thread outside the main thread. That way the main thread can continue while the worker thread waits for an event.

The Implementation

The implementation of the TDirectoryMonitor class begins with the TWorkerThread. As mentioned earlier the ReadDirectoryChangesW function can be called synchronously or asynchronously. The synchronous method is the easiest way to use ReadDirectoryChangesW, but there is a problem. The function will not return until an event occurs. This would mean the watch could not be terminated until after an event has occurred. For this reason the TWorkerThread uses the completion port asynchronous call. The completion port method allows the watch to be canceled by calling the PostQueuedCompletionStatus function. Microsoft also provides an example of the completion port option in MSDN. Examples of using ReadDirectoryChangesW synchronously and with a callback function are included in the examples file for this article available for download at my web site:http://www.biggbytesoftware.com/rod/ReadDirectoryChangesDownloads.html

There are two things to focus on in the TWorkerThread. The Create constructor and the Execute method. The TThread Create constructor needs to be replaced because the WorkerThread needs more information. The most obvious piece of information is which directory to monitor. This is passed as the PathToWatch parameter. The next thing the thread will need is to know what events the ReadDirectoryChangesW function will watch for. This is set in the ActionsToWatch parameter. The parameter ParentWindow requires some more explanation.

Since the ReadDirectoryChangesW function is called in a separate thread, the main thread must be notified when one of the events the class is watching for occurs. There are two options to make this notification. The first is to use the Synchronize method of the thread. This will in effect suspend the worker thread until the main thread is done processing the event. Using this method may result in the loss of events. The second option is to send a message to the main thread, but to send a message the worker thread must know where to send the message. That’s what the ParentWindow parameter is for. It is the handle of the window that will receive messages generated by the worker thread.

The last parameter is WatchSubFolders. This determines if the ReadDirectoryChangesW function monitors sub-directories of the PathToWatch parameter. The body of the create function assigns all of the parameters to private variables and opens a handle to the directory. The thread is created in a suspended state so there is time to complete all of the setup tasks. The Create constructor implementation is shown in figure 7.

<pre>
constructor TWorkerThread.Create(const PathToWatch: String; ActionsToWatch: TActionsToWatch;
ParentWindow: THandle; WatchSubFolders : Boolean);
begin
Inherited Create(True);
FWatchSubFolders := WatchSubFolders;
FPathToWatch := PathToWatch;
FActionsToWatch := ActionsToWatch;
FParentWindow := ParentWindow;
FNotifyMask := GetNotifyMask;
FDirHandle := CreateFile(PChar(FPathToWatch),
FILE_LIST_DIRECTORY,
FILE_SHARE_READ Or FILE_SHARE_WRITE Or FILE_SHARE_DELETE,
Nil, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS Or FILE_FLAG_OVERLAPPED,
0);
GetMem(FBuffer,MAX_BUFFER);
FBufferLength := MAX_BUFFER;
Resume;
end
</pre>

Figure 7 – The TWorkerThread Constructor

Once the constructor has resumed the thread, control is passed to the Execute method.  The first thing the Execute method does is open a completion port.  All file event notifications will come through this completion port.  The result buffer is cleared and ReadDirectoryChangesW is called.  After the call to ReadDirectoryChangesW the Execute method enters a Repeat loop and calls GetQueuedCompletionStatus. GetQueuedCompletionStatus will wait for an event and the loop will then call ReadDirectoryChangesW again to wait for the next event until the thread is either terminated or an empty buffer is returned.  The buffer is empty when the completion port is shutdown.

Now that the worker thread is completed attention turns to the implementation of the TDriectoryMonitor class itself. As in the TWorkerThread class the TDirectoryMonitor class replaces the ancestor constructor.  Then new constructor is shown in figure 8.  The main reason for this is so the TDirectoryMonitor can create a window handle to receive the message from the worker thread.  This handle is obtained by calling the AllocateHWnd function and passing the address of the message handler procedure for the allocated window.  In this case the message handler procedure is named InternalWinProc and is shown in figure 9.

<pre>
constructorTDirectoryMonitor.Create;
begin
Inherited Create;
FWindowHandle := AllocateHWnd(InternalWinProc);
FWatchSubFolders := True;
end;
</pre>

Figure 8 – The TDirectoryMonitor Constructor

<pre>
procedureTDirectoryMonitor.InternalWinProc(var Msg: TMessage);
Const
TranslateActions : <b'>Array[FILE_ACTION_ADDED..FILE_ACTION_RENAMED_NEW_NAME] Of TDirectoryAction =
(daFileAdded, daFileRemoved,daFileModified,daFileRenamedOldName,daFileRenamedNewName);
begin
If Msg.Msg = CM_DIRECTORY_EVENT Then
begin
If Assigned(FOnDirectoryChange) then
FOnDirectoryChange(Self,TranslateActions[Msg.wParam], String(Pchar(Msg.LParam)));
StrDispose(PChar(Msg.LParam));
end;
end
</pre>

Figure 9 – The internal winproc.

The next important method in the TDirecotryMonitor class is the Start method. The start method creates the worker thread that watches the directory in the DirectoryToWatch property. The Stop Method cancels the monitoring by posting the completion port with a zero (null) information value.

Using The TDirectoryMonitor Class.

Using the TDirectoryMonitor class requires five steps.
The first step is to create an instance of the class.  The create constructor requires no parameters, so this is familiar to users of TstringList and other classes.
The next step is to set what events to watch for.  This is done by setting the value of the property ActionsToWatch property.
The third step is to set the value of the DirectoryToWatch property. The directory specified must exist; the class will not create it, and setting this property to a directory that does not exist will cause an error when the worker thread calls CreateFile. 
The fourth step is to assign an event handler to the OnDirectoryChange event.  The OnDirectoryChange event handler is where the user will decide what to do when a file notification event occurs.  The OnDirectoryChange event has the following definition :

<pre>
procedure(Sender : TObject; Action : TDirectoryAction; FileName : String) Of Object;
</pre>

Sender is the instance of the class that caused the event.  Action describes what event occurred.  The possible values are described in figure 10. FileName is the name of the file the action occurred on.
The last step is to call the Start method.  A complete example program describing these steps is shown in figure 11 (also available for download at my web site http://www.biggbytesoftware.com/rod/ReadDirectoryChangesDownloads.html.

daFileAdded The file in Filename has been added.
DaFileRemoved The file has been deleted.
DaFileModified The file has been changed.
DaFileRenamedOldName The file has been renamed and FileName is the old file name.
DaFileRenamedNewName The file has been renamed and FileName is the new file name.

Figure 10 – Action value descriptions.

<pre>
{$WARN UNIT_PLATFORM OFF}
unit DirectoryMonitorDemoU;

interface

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

type
TfrmDemo = class(TForm)
Button1: TButton;
Memo1: TMemo;
Button2: TButton;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
private
{ Private declarations }
FDirMon : TDirectoryMonitor;
procedure DirChange(Sender : TObject; Action : TDirectoryAction; FileName : String);
public
{ Public declarations }
end;

var
frmDemo: TfrmDemo;

implementation
Uses
FileCtrl;

{$R *.dfm}

procedure TfrmDemo.FormCreate(Sender: TObject);
begin
FDirMon := TDirectoryMonitor.Create;
FDirMon.OnDirectoryChange := DirChange;
FDirMon.Options := [awChangeFileName,awChangeDirName,awChangeSize,awChangeLastWrite];
end;

procedure TfrmDemo.FormDestroy(Sender: TObject);
begin
FDirMon.Free;
end;

procedure TfrmDemo.Button1Click(Sender: TObject);
var
S : String;
begin
SelectDirectory('Select A Directory To Monitor','',S);
If S <> '' Then
FDirMon.DirectoryToWatch := S;
FDirMon.Start;
end;

procedure TfrmDemo.Button2Click(Sender: TObject);
begin
FDirMon.Stop;
end;

procedure TfrmDemo.DirChange(Sender: TObject; Action: TDirectoryAction; FileName: String);
Const
ActionDesc : Array[TDirectoryAction] Of String =
('FILE_ACTION_ADDED. The file %s was added to the directory.',
'FILE_ACTION_REMOVED. The file %s was removed from the directory.',
'FILE_ACTION_MODIFIED. The file %s was modified.
'This can be a change in the time stamp or attributes.',
'FILE_ACTION_RENAMED_OLD_NAME. The file %s was renamed, and this is the old name.',
'FILE_ACTION_RENAMED_NEW_NAME The file %s was renamed and this is the new name.');

begin
Memo1.Lines.Add(Format(ActionDesc[Action],[FileName]));
end;

end.
</pre>

Figure 11 – A small sample program.

The Wrap Up

Monitoring a directory using ReadDirectoryChangesW can be a daunting task, but by wraping the complexity of it into an easy to use class I hope I have made the task a little less daunting.  BUT do take note, this class is not perfect nor was it meant to be. There needs to be more error handling and maybe some properties added before it would be production ready. I leave these things as an exercise for the reader.

About The Author
Rodney Holiman is a software developer and consultant specializing in Borland Delphi.


If you would like to see your thoughts or experiences with technology published, please consider writing an article for OSNews.

11 Comments

  1. 2004-06-15 8:08 pm
  2. 2004-06-15 9:09 pm
  3. 2004-06-15 9:13 pm
  4. 2004-06-15 10:57 pm
  5. 2004-06-16 1:04 am
  6. 2004-06-16 3:54 am
  7. 2004-06-16 4:15 am
  8. 2004-06-16 7:19 am
  9. 2004-06-16 9:08 am
  10. 2004-06-16 10:10 am
  11. 2004-06-16 8:40 pm