Sample Project:  Monitoring Directory Changes
(By Damon Chandler)
 

DOWNLOAD Demo and Sample Project (147 KB)
 

How do I monitor the contents of a directory?  This is certainly a commonly asked question with a usually simple answer: Use the FindFirstChangeNotification() API function.  While this is indeed the correct approach to take, the methods to actually accomplish this are not so straightforward.   In fact, FindFirstChangeNotification() is just a small part of making this work.  As such, the purpose of this article is to provide an more in-depth look at exactly what's involved in monitoring the contents of a folder.

Why would one want to monitor directories anyway?  Consider the case of using the TFileListBox component to allow the end user to select files which will be transferred via FTP, for example.  Imagine that the user copies a file into the folder referred to by the FileListBox's Directory property, yet fails to see it appear in the list.  That is, the FileListBox does not update upon a change of the folder's contents.  As you can imagine, this could cause frustration and a premature judgment of your application as being less than robust.  As consumers ourselves, we would expect such an application to respond to directory updates.  Consider the hassle of needing to refresh Explorer's display once you drag a file into that folder.  Obviously, there has to be a standard solution to this problem.

As mentioned earlier, the standard solution is to use the FindFirstChangeNotification() API function.  This function will return a handle to a change notification object.  The syntax is as follows:

HANDLE FindFirstChangeNotification
(
    LPCTSTR lpPathName, <--- pointer to a buffer that holds the 
                             absolute path to monitor

    BOOL bWatchSubtree, <--- boolean flag that indicates whether or 
                             not to watch subdirectories of lpPathName

    DWORD dwNotifyFilter<--- used to specify filter conditions that satisfy 
                             a change notification wait.
);
 

In the call to this function, dwNotifyFilter specifies what changes of the directory you want notification of.  The following list outlines the available options (use the logical OR operator, "|", to combine these values:
 
 
Value Attribute
FILE_NOTIFY_CHANGE_FILE_NAME  Gives notification of any changes to the filename of a file within the folder (and subfolders, if specified) being monitored.
FILE_NOTIFY_CHANGE_DIR_NAME  Gives notification of any changes to the directory name of a folder within the folder (and subfolders, if specified) being monitored.
FILE_NOTIFY_CHANGE_ATTRIBUTES  Gives notification of any changes to the attribute of a file or folder within the folder (and subfolders, if specified) being monitored.  For example, changing a file within the watched folder to hidden.
FILE_NOTIFY_CHANGE_SIZE  Gives notification of any changes to the size of a file or folder within the folder (and subfolders, if specified) being monitored.
FILE_NOTIFY_CHANGE_LAST_WRITE  Gives notification of a write-time change of a file within the folder (and subfolders, if specified) being monitored.  Influenced by caching.
FILE_NOTIFY_CHANGE_SECURITY  Gives notification of a security-descriptor change within the folder (and subfolders, if specified) being monitored.

 

So once you receive a change notification handle from the call to FindFirstChangeNotification(), you pass this handle into the WaitForMultipleObjects() function which waits for a change to occur.  The WaitForMultipleObjects() function takes the following parameters:

DWORD WaitForMultipleObjects
(
    DWORD nCount, <---number of handles included in the in the object 
                       handle array

    CONST HANDLE *lpHandles, <--- pointer to an array of object handles
                                  (handles returned by
                                  FindFirstChangeNotification())

    BOOL bWaitAll, <---flag indicating whether to return when one (false),
                        or all of the folders in the object handle array,
                        (true) indicates a change.

    DWORD dwMilliseconds<---time-out interval (in milliseconds)
);
 

You may be asking youself, won't waiting for a change notification cause my program to suspend?  In a sense, yes, execution of your program will not continue until the WaitForMultipleObjects() function returns.  This means that important messages such as WM_PAINT will not be processed, causing what may seem as application lock. This is where multitasking, and in particular, the TThread class comes in handy.  So now you know that the reason you're advised to use a separate thread is due to the use of the WaitForMultipleObjects() function.  As mentioned earlier, this function will wait until it receives notification from the system.  By "waiting" in a separate thread, your main application is free to process vital messages.  Is a thread absolutely necessary?  Well, technically, no, you could actually use the MsgWaitForMultipleObjects() function inside your window procedure, but unless your application is extremely simple, this will yield sluggish performance. 

Once the WaitForMultipleObjets() function returns, the return value will indicate whether a change actually occured or the function timed-out.  After the function returns, simply call the FindNextChangeNotification() function to continue monitoring, or the FindCloseChangeNotification() function to close the change notification handle.  Here's an overview of the entire process necessary to make this work:


(1)  Derive a new thread class from TThread -- the constructor will accept a TControl* called FTargetControl which will be notified of changes via a user defined message.  In this way, we won't limit the scope of our thread class to only work with a particular control.

(2)  Within this class, add a public member function to set the directory to monitor and call the FindFirstChangeNotification() function -- we'll call this method SetWatchPath().

(3)  Within the Execute() method of the thread class, call the WaitForMultipleObjects() function with the handle returned by the FindFirstChangeNotification() function. 

(4)  Once the WaitForMultipleObjects() returns (i.e., a change has taken place in the directory being monitored), call a member function that will notify the main process that a change has occurred -- we'll call this member function UpdateDirectory() and it will send a user defined message to the control specified by the FTargetControl field. 
 



 

KEYWORDS: FindFirstChangeNotification(), WaitForMultipleObjects(), FindNextChangeNotification(), FindCloseChangeNotification(). 
REFERENCES: http://msdn.microsoft.com/library/psdk/winbase/filesio_1545.htm  (example)
                       http://msdn.microsoft.com/library/psdk/winbase/synchro_9xbn.htm
                       http://msdn.microsoft.com/library/psdk/winbase/filesio_9hgu.htm
                       http://msdn.microsoft.com/library/psdk/winbase/filesio_0566.htm
                       http://msdn.microsoft.com/library/psdk/winbase/filesio_1wku.htm
 



//---------------------------------------------------------------------------

// in thread class header...

#define CM_DIRCONTENTSCHANGED WM_APP + 101  // notification message

class TDirMonitorThread : public TThread
{
private:
    TControl *FTargetControl; // control that will be notified of changes
    HANDLE FChangeHandle;     // handle to a find change notification object
    AnsiString FWatchPath;    // directory to monitor

protected:
    void __fastcall Execute();

public:
    __fastcall TDirMonitorThread(bool CreateSuspended, 
                                 TControl *ATargetControl);
    void __fastcall DirMonitorTerminate(TObject *Sender);
    void __fastcall UpdateDirectory();
    void __fastcall SetWatchPath(AnsiString Value);
};

//---------------------------------------------------------------------------

 


 

//---------------------------------------------------------------------------

// in thread class source...

__fastcall TDirMonitorThread::TDirMonitorThread(bool CreateSuspended,
    TControl *ATargetControl) : TThread(CreateSuspended)
{
    FreeOnTerminate = true;

    FChangeHandle = INVALID_HANDLE_VALUE;
    FTargetControl = ATargetControl;
    FWatchPath = "";
    OnTerminate = DirMonitorTerminate;
}

void __fastcall TDirMonitorThread::DirMonitorTerminate(TObject *Sender)
{
   // upon termination, unregister the directory monitoring
   if (FChangeHandle != INVALID_HANDLE_VALUE)
        FindCloseChangeNotification(FChangeHandle);
}

// method to set the directory to monitor (watch)
void __fastcall TDirMonitorThread::SetWatchPath(AnsiString Value)

    if (FWatchPath != Value)
    {
        FWatchPath = Value;

        // if there's a previous directory registered, release it
        if (FChangeHandle != INVALID_HANDLE_VALUE)
        FindCloseChangeNotification(FChangeHandle);

        // register the directory (Value) to monitor using the
        // FindFirstChangeNotification() function -- the dwFlags specify
        // what changes we want notification of, while the "false" 
        // parameter indicates that we don't want to monitor subdirectories
        DWORD dwFlags = FILE_NOTIFY_CHANGE_FILE_NAME |
                        FILE_NOTIFY_CHANGE_DIR_NAME;
        FChangeHandle = INVALID_HANDLE_VALUE;
        FChangeHandle = FindFirstChangeNotification(Value.c_str(),
                                                 false, dwFlags);
    }
}

void __fastcall TDirMonitorThread::Execute()
{
    // while the Terminated property is false and 
    // the handle returned by the FindFirstChangeNotification()
    // function is valid
    while (!Terminated && FChangeHandle != INVALID_HANDLE_VALUE)
    {
        bool TimedOut = true;
        // while the WaitForMultipleObjects() call has timed out (we want
        // to call the function again if it timed out) and not Terminated
        // and not Suspended
        while (TimedOut && !Terminated && !Suspended)
        {
            // wait for notification from the system -- time out after 2 secs.
            DWORD dwStatus = WaitForMultipleObjects(1, &FChangeHandle,
                                                    false, 2000);

            // once you receive notification, one of the following will
            // execute:

            // WAIT_OBJECT_0 indicates a file/folder was 
            // created/deleted/renamed in the directory we're "watching" 
            if (dwStatus == WAIT_OBJECT_0 && !Terminated)
            {
                // call the UpdateDirectory() method to notify
                // the TargetControl
                Synchronize(UpdateDirectory);

                // repeat the directory watch
                bool reset = FindNextChangeNotification(FChangeHandle);
                if (!reset) break;
            }

            // WAIT_TIMEOUT indicates that the WaitForMultipleObjects() 
            // function timed out (to prevent thread lock), 
            // if so, repeat the directory watch
            else if (dwStatus == WAIT_TIMEOUT && !Terminated)
            {
                TimedOut = true;
            }

            // otherwise something possibly went wrong so terminate
            else TimedOut = false;
        }
    }
    Terminate();
}

void __fastcall TDirMonitorThread::UpdateDirectory()
{
    // send the CM_DIRCONTENTS changed message to the TargetControl...
    // you TargetControl should update itself (or act accordingly) when
    // it receives this message
    if (FTargetControl)
        FTargetControl->Perform(CM_DIRCONTENTSCHANGED, 0, 0);
}
 

 


 
 
 

Ok, the hard part is done, now how exactly do we use this thread?  First, we need to map the CM_DIRCONTENTSCHANGED message.  Remember, this is the component message we defined in the thread's unit.  This message will be sent to whatever control we pass into the constructor of the thread as ATargetControl.  We'll pass "Form1" or "this" as the target control so that the message will be sent to the Form's window procedure.  Now all we need to do is implement a message map:
 

//---------------------------------------------------------------------------

// in Form1 header...

    TDirMonitorThread *DirMonitorThread;
    void __fastcall CMDirContentsChanged(TMessage &Msg);

BEGIN_MESSAGE_MAP
    MESSAGE_HANDLER(CM_DIRCONTENTSCHANGED, TMessage, CMDirContentsChanged)
END_MESSAGE_MAP(TForm)
 

//---------------------------------------------------------------------------

 


 
 
 

Now, we need to create a thread object, set the directory to watch, then engage the thead via the Resume() method:
 

//---------------------------------------------------------------------------

// in Form1 source...

#include "DirMonitorThread.h"
#include "Unit1.h"

__fastcall TForm1::TForm1(TComponent* Owner)
    : TForm(Owner)
{
    FileListBox1->Directory = "C:\\Windows\\Desktop";

    // create a new directory monitor thread, initially
    // suspended with Form1 (this) as the TargetControl
    DirMonitorThread = new TDirMonitorThread(true, this);

    // set the thread to monitor the FileListBox's Directory
    DirMonitorThread->SetWatchPath(FileListBox1->Directory);

    // start the process
    DirMonitorThread->Resume();
}

void __fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action)
{
    // terminate the thread
    DirMonitorThread->Terminate();
    DirMonitorThread = NULL;
}
 

// Now comes the CMDirContentsChanged message handler...

void __fastcall TForm1::CMDirContentsChanged(TMessage &Msg)
{
    // the DirMonitorThread has sent the
    // CM_DIRCONTENTSCHANGED message -- refresh the
    // contents of the FileListBox from disk
    FileListBox1->Update();
}
 

 


 
 

DOWNLOAD Demo and Sample Project (147 KB)