Folder change notification

by Mark G. Wiseman

I’m going to show you how to do something with C++Builder that sounds very, very complicated, but turns out to be really easy. I’m going to show you how to create a VCL component that uses multithreading to monitor changes to the files and subfolders of folder on a hard drive. The component itself requires less than two hundred lines of code and the C++Builder IDE will even generate some of that for you.

Listings A and B contain the source code for the component that I named TChangeNotification. This source code and the source for a demonstration program can be found on the Bridges Publishing Web site at www.bridgespublishing.com.

 

Monitoring folder changes

The Windows API contains a set of functions that allow a programmer to monitor the file system for changes. These functions are FindFirstChangeNotification(), FindNextChangeNotification() and FindCloseChangeNotification().

Let’s take a look at FindFirstChangeNotification() first. Here is the prototype:


HANDLE FindFirstChangeNotification
(
  LPCTSTR lpPathName,
  BOOL bWatchSubtree,
  DWORD dwNotifyFilter
);

The first argument to this function, lpPathName, is the path of the folder you want to monitor for changes, e.g. "c:\\WinNT\\System".

If the second parameter, bWatchSubtree, is true, not only will the folder be monitored, but all its subfolders will also be monitored.

The third parameter, dwNotifyFilter, is a flag that tells Windows what kinds of changes you want to monitor. You can monitor changes to file names, changes to file size and attributes, last-write and last-access times and changes to file security settings. Take a look at the Windows API online help to see all the values you can use for this flag. I used the FILE_NOTIFY_CHANGE_FILE_NAME and FILE_NOTIFY_CHANGE_DIR_NAME flags for the component. Using these flags, the component will be notified whenever a file or folder is created, deleted, or renamed.

If the call to FindFirstChangeNotification() is successful, it returns a HANDLE to a find-change-notification object. The easiest way to monitor this object is by using one of the Windows API "Wait Functions". I used the WaitForSingleObject() function.

The WaitForSingleObject() function simply stops everything and waits for the find-change-notification object to signal a change. The easiest way to use this function is to wrap it in a separate thread. I’ll discuss how to do this a little later. This function takes two parameters, the handle returned by FindFirstChangeNotification() and a time-out interval. Since I don’t want the component to time out, I set the interval value to INFINITE.

When the find-change-notification object signals a change, WaitForSingleObject() returns with a DWORD return value equal to WAIT_OBJECT_0.

Once WaitForSingleObject() returns, I call FindNextChangeNotification(), passing in the handle to the find-change-notification object as its only parameter. If this function call is successful, I again call WaitForSingleObject() to continue monitoring.

When I want to stop the monitoring, I need to release the find-change-notification object by calling FindCloseChangeNotification() and passing in the handle to the object.

 

Using TCNThread

Since the WaitForSingleObject() function stops and waits for a signal from the find-change-notification object, you can’t call it in your program’s main thread. If you did, the program would freeze until there was a change in the folder being monitored.

This is why I used a separate thread, TCNThread, derived from the TThread class in the VCL. Since TCNThread has only one very specific function, I have embedded its class definition inside the class definition for TChangeNotification. TCNThread gets all the data it needs to work from the TChangeNotification component that owns it.

There really isn’t a lot of coding involved in creating TCNThread. As required for all classes derived from TThread, I have written an Execute() function. Let’s take a look at this function.

The Execute() function performs the tasks I described in the section above. It creates a find-change-notification object and then enters a while loop in which it waits for that object to signal a change.

When a change occurs, Execute() notifies the TChangeNotification component that owns its TCNThread by calling the Notify() function. Execute() then calls FindNextChangeNotification() and cycles back through the while loop to wait for another signal.

The while loop will exit when the TCNThread is terminated. At this point Execute() calls FindCloseChangeNotification() to free the find-change-notification object.

If there should be an error, Execute() notifies its parent component by calling the Error() function and then terminating the TCNThread. I use the Error() function instead of a C++ exception, because throwing an exception out of a thread will crash a program.

If you’ve looked at the Execute() function in TCNThread source code, then you might have noticed that the calls to the Notify() and Error() functions are wrapped in a call to Synchronize(). It is usually not safe to access VCL components from a thread other than the main VCL thread. The Synchronize() function forces Notify() and Error() to execute in the main VCL thread.

 

The TChangeNotification component

The TChangeNotification class encapsulates everything in an easy to use component.

I started by letting the C++Builder IDE generate the skeletal code for TChangeNotification. I did this by choosing File | New… from the IDE menu and then selecting Component from the New tab of the resulting dialog box. I filled in the blanks of the new component wizard and clicked the OK button.

TchangeNotification, which is derived from TComponent, is a very simple component. In addition to a constructor and destructor, the public interface for this component has two functions, Start() and Stop(), and four properties, OnChange, OnError, Folder and WatchSubFolders.

Let’s take a look at how the component works, starting with the Start() and Stop() functions. The Start() function creates a new TCNThread if one doesn’t already exist and the Stop() function deletes the TCNThread and zeroes out the pointer to it.

All the data that the TCNThread needs to run is obtained from data members of the component. These data members are set and read through the four published properties of the component.

The Folder property corresponds to the private data member folder, an AnsiString that contains the path of the folder to monitor. The WatchSubFolders property is a bool that determines whether the folders subfolders are also monitored.

These two properties, Folder and WatchSubFolders use functions to set the values of the underlying data members, folder and watchSubFolders. These functions are SetFolder() and SetWatchSubFolders(). The important thing to notice about these two functions is how they handle a property change while the TCNThread is running. Since the values from these two properties are used in the call to FindFirstChangeNotification(), the running thread must be terminated and deleted, and then a new thread must be created. This is accomplished by wrapping the change in the data members with calls to Stop() and Start().

The properties, OnNotify and OnError, are for programmer defined events that occur when a change in the folder is signaled or when an error occurs within the TCNThread. These two properties also use functions to set the values of the underlying data members, onNotify and onError.

It is not necessary to delete a running thread and then create a new one when changing the values of OnNotify and OnError. However, you don’t want TCNThread calling one of these events while it is being change. To avoid this, I have wrapped the change in data members with calls to the Suspend() and Resume() functions of TCNThread.

That’s really all there is to creating the TChangeNotification component. Now let’s take a quick look at how to use it.

 

Using TChangeNotification

Once you have created the TChangeNotification component you will need to install the component into your IDE’s component palette. Using the component is very easy.

Just drop the TChangeNotification component on a form and assign a path to the folder property in the Object Inspector of the IDE. Next, switch to the Events tab of the Object Inspector and double-click on the OnChange event. Fill in the code you want to execute when a folder change is signaled and you’re ready to go.

You will need to call the Start() function of the component to actually begin monitoring folder changes.

Although it is not necessary, I would recommend that you also assign some error handling code to the OnError event. If you don’t deal with an error, the component stops its thread and will not report folder changes. The most common error you are likely to run into is passing in a non-existent folder to be monitored.

 

Conclusion

Look at the demo program to see how I used TChangeNotificaiton. The program allows the user to set a folder to monitor and to specify whether its subfolders should also be monitored. It allows the user to start and stop the monitoring and logs any folder changes to a TEdit control.

Unfortunately, the find-change-notification object only tells you that a change occurred in the folder. It does not tell you what that change was. If you need to know more, you will have to do a before and after analysis of the folder.

Finally, I think this component is fairly complete, but there are two improvements you might want to consider.

First, you could add a custom editor to the Folder property to let the programmer browse for the folder to be monitored. This may or may not be that useful depending on how much you know about the directory structure of the computer running the program.

Second, you might want to consider adding a property to let the programmer select the flags for the value of parameter, dwNotifyFilter, which is passed into the call to FindFirstChangeNotification(). This way you could use the component to monitor other changes in the file system, for example changes to file size.

 

 

Listing A: Header file for theTChangeNotificion component.

 

#ifndef ChangeNotificationH
#define ChangeNotificationH

#include <SysUtils.hpp>
#include <Controls.hpp>
#include <Classes.hpp>
#include <Forms.hpp>


class PACKAGE TChangeNotification :
public TComponent
{
  __published:
    __property TNotifyEvent OnChange = {read = onChange, write = SetOnChange};
    __property TNotifyEvent OnError = {read = onError, write = SetOnError};
    __property bool WatchSubFolders = {read = watchSubFolders, write = SetWatchSubFolders};
    __property String Folder = {read = folder, write = folder};
  
  public:
    __fastcall TChangeNotification(TComponent *owner);
    __fastcall ~TChangeNotification();
  
    void __fastcall Start();
    void __fastcall Stop();
  
  private:
    void __fastcall SetOnChange(TNotifyEvent event);
    void __fastcall SetOnError(TNotifyEvent event);
    void __fastcall SetWatchSubFolders(bool watch);
    void __fastcall SetFolder(String aFolder);
  
    TNotifyEvent onChange;
    TNotifyEvent onError;
    bool watchSubFolders;
    String folder;
  
  class TCNThread : public TThread
  {
    friend TChangeNotification;
  
  public:
    __fastcall TCNThread(TChangeNotification *owner);
    __fastcall ~TCNThread();
    
    void __fastcall Execute();
    void __fastcall Notify();
    void __fastcall Error();
  
  private:
    TChangeNotification *owner;
    HANDLE handle;
  } *thread;
};

#endif // ChangeNotificationH

Listing B: Source file for theTChangeNotificion component.

 

#include <vcl.h>
#pragma hdrstop

#include "ChangeNotification.h"

__fastcall 
TChangeNotification::TChangeNotification(TComponent *owner) : TComponent(owner)
{
  onChange = 0;
  onError = 0;
  thread = 0;
  watchSubFolders = false;
}

__fastcall
TChangeNotification::~TChangeNotification()
{
  delete thread;
}

void __fastcall TChangeNotification::Start()
{
  if (thread == 0) thread = new TCNThread(this);
}

void __fastcall TChangeNotification::Stop()
{
  delete thread;
  thread = 0;
}

void __fastcall TChangeNotification::SetFolder(String aFolder)
{
  if (thread != 0) {
    Stop();
    folder = aFolder;
    Start();
  } else
    folder = aFolder;
}

void __fastcall TChangeNotification::SetOnChange(TNotifyEvent event)
{
  if (thread != 0) {
    thread->Suspend();
    onChange = event;
    thread->Resume();
  } else
    onChange = event;
}

void __fastcall TChangeNotification::SetOnError(TNotifyEvent event)
{
  if (thread != 0) {
    thread->Suspend();
    onError = event;
    thread->Resume();
  } else
    onError = event;
}

void __fastcall
TChangeNotification::SetWatchSubFolders(bool watch)
{
  if (thread != 0) {
    Stop();
    watchSubFolders = watch;
    Start();
  } else
    watchSubFolders = watch;
}


// Thread ------------------------------------

__fastcall TChangeNotification::TCNThread::
TCNThread(TChangeNotification *owner)
: TThread(false), owner(owner)
{
  handle = 0;
}

__fastcall
TChangeNotification::TCNThread::~TCNThread()
{
  Terminate();

  if (handle != 0) {
    FindCloseChangeNotification(handle);
    handle = 0;
  }
}

void __fastcall
TChangeNotification::TCNThread::Execute()
{
  handle = FindFirstChangeNotification(owner->Folder.c_str(), 
                                       owner->WatchSubFolders,
                                       FILE_NOTIFY_CHANGE_FILE_NAME);

  if (handle == INVALID_HANDLE_VALUE) {
    Synchronize(Error);
    Terminate();
  }

  while (!Terminated) {
    DWORD status =
    WaitForSingleObject(handle, INFINITE);

    if (status == WAIT_OBJECT_0
        && Terminated == false) {
      Synchronize(Notify);
      if (FindNextChangeNotification(handle) == 0) {
        Synchronize(Error);
        Terminate();
      }
    } else
      Terminate();
  }
}

void __fastcall
TChangeNotification::TCNThread::Notify(void)
{
  if (owner->OnChange) owner->OnChange(owner);
}

void __fastcall
TChangeNotification::TCNThread::Error(void)
{
  if (owner->OnError) owner->OnError(owner);
}


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

static inline void ValidCtrCheck(TChangeNotification *)
{
  new TChangeNotification(NULL);
}


#pragma package(smart_init)