{

Modifications:
==============

 ie01: OnChange delayed for 500 ms.
 ie02: No usage of BrowseDirectory component
 ie03: Resume suspended thread when terminating the thread

 }


// ==================== DISC DRIVE MONITOR =====================================
//
// Class and Component to encapsulate the FindXXXXChangeNotification API calls
//
// The FindXXXXChangeNotification API calls set up a disc contents change
// notification handle.  You can set a filter to control which change types
// are notified, the directory which is monitored and set whether subdirectories
// from the monitored directory are monitored as well.
//
//------------------------------------------------------------------------------
// This file contains a class derived from TThread which undertakes the disc
// monitoring and a simple component which encapsulates the thread to make
// a non-visual VCL component.  This component works at design time, monitoring
// and notifying changes live if required.
//
// Version 1.00 - Grahame Marsh 14 January 1997
// Version 1.01 - Grahame Marsh 30 December 1997
//      Bug fix - really a Win 95 bug but only surfaces in D3, not D2
//              - see notes in execute method
// Version 1.02 - Grahame Marsh 30 January 1998
//              - adapted to work with version 2.30 TBrowseDirectoryDlg
//
// Freeware - you get it for free, I take nothing, I make no promises!
//
// Please feel free to contact me: grahame.s.marsh@courtaulds.com

{$DEFINE BUGFIX}

unit DiscMon;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls,
  Forms, Dialogs, ShlObj, ActiveX, CompThread {, BrowseDr, DsgnIntf ie02};

//=== DISC MONITORING THREAD ===================================================
// This thread will monitor a given directory and subdirectories (if required)
// for defined filtered changes.  When a change occurs the OnChange event will
// be fired, if an invalid condition is found (eg non-existent path) then
// the OnInvalid event is fired. Each event is called via the Sychronize method
// and so are VCL thread safe.
//
// The thread is created suspended, so after setting the required properties
// you must call the Resume method.


type
  TDiscMonitorThread = class(TCompThread)
  private
    FOnChange : TNotifyEvent;
    FOnInvalid : TNotifyEvent;
    FDirectory : string;
    FFilters : DWORD;
    FDestroyEvent,
    FChangeEvent : THandle;
    FSubTree : boolean;
    fChangeDelay : Integer;  {ie01}
    procedure InformChange;
    procedure InformInvalid;
    procedure SetDirectory (const Value : string);
    procedure SetFilters (Value : DWORD);
    procedure SetSubTree (Value : boolean);
  protected
    procedure Execute; override;
    procedure Update;
  public
    constructor Create;
    destructor Destroy; override;
// The directory to monitor
    property Directory : string read FDirectory write SetDirectory;
// Filter condition, may be any of the FILE_NOTIFY_CHANGE_XXXXXXX constants
// ORed together.  Zero is invalid.
    property Filters : DWORD read FFilters write SetFilters;
// Event called when change noted in directory
    property OnChange : TNotifyEvent read FOnChange write FOnChange;
// Event called for invalid parameters
    property OnInvalid : TNotifyEvent read FOnInvalid write FOnInvalid;
// Include subdirectories below specified directory.
    property SubTree : boolean read FSubTree write SetSubTree;
// specify, how long the thread should wait, before the event OnChange is fired:
    Property ChangeDelay : Integer Read fChangeDelay Write fChangeDelay  {ie01}
                                   Default 500;                          {ie01}
  end;

//===================== DISC MONITORING COMPONENT ==============================

// specify directory string as type string so we can have our own property editor
  TDiscMonitorDirStr = type string;

// enumerated type for filter conditions (not directly usable in thread class)
// see the SetFilters procedure for the translation of these filter conditions
// into FILE_NOTIFY_CHANGE_XXXXXX constants.
  TMonitorFilter = (moFilename, moDirName, moAttributes, moSize,
                    moLastWrite, moSecurity);
// set of filter conditions
  TMonitorFilters = set of TMonitorFilter;

  TDiscMonitor = class(TComponent)
  private
    FActive : boolean;
    FMonitor : TDiscMonitorThread;
    FFilters : TMonitorFilters;
    FOnChange : TNotifyEvent;
    FOnInvalid : TNotifyEvent;
    FShowMsg : boolean;
    function GetDirectory : TDiscMonitorDirStr;
    function GetSubTree : boolean;
    procedure SetActive (Value : boolean);
    procedure SetDirectory (Value : TDiscMonitorDirStr);
    procedure SetFilters (Value : TMonitorFilters);
    procedure SetSubTree (Value : boolean);
    Function  GetChangeDelay : Integer;         {ie01}
    Procedure SetChangeDelay(Value : Integer);  {ie01}
  protected
    procedure Change (Sender : TObject);
    procedure Invalid (Sender : TObject);
  public
    constructor Create (AOwner : TComponent);  Override;
    destructor Destroy; override;
// stop the monitoring thread running
    procedure Close;
// start the monitoring thread running
    procedure Open;
// read-only property to access the thread directly
    property Thread : TDiscMonitorThread read FMonitor;
  published
// the directory to monitor
    property Directory : TDiscMonitorDirStr read GetDirectory write SetDirectory;
// control the appearance of information messages at design time (only)
    property ShowDesignMsg : boolean read FShowMsg write FShowMsg default false;
// event called when a change is notified
    property OnChange : TNotifyEvent read FOnChange write FOnChange;
// event called if an invalid condition is found
    property OnInvalid : TNotifyEvent read FOnInvalid write FOnInvalid;
// notification filter conditions
    property Filters : TMonitorFilters read FFilters write SetFilters default [moFilename];
// include subdirectories below the specified directory
    property SubTree : boolean read GetSubTree write SetSubTree default true;
// specify if the monitoring thread is active
    property Active : boolean read FActive write SetActive default false;
// specify, how long the thread should wait, before the event OnChange is fired:
    Property ChangeDelay : Integer Read GetChangeDelay Write SetChangeDelay  {ie01}
                                   Default 500;                              {ie01}
  end;


procedure Register;


{$IFDEF BUGFIX}
{$Z4}

type TWinBool = (winFalse, winTrue);
function FixFindFirstChangeNotification(const lpPathName: PChar;
                                        bWatchSubtree: TWinBool;
                                        dwNotifyFilter: DWORD): THandle stdcall; external kernel32 name 'FindFirstChangeNotificationA';
{$ENDIF}

implementation

//=== MONITOR THREAD ===========================================================

// Create the thread suspended.  Create two events, each are created using
// standard security, in the non-signalled state, with auto-reset and without
// names.  The FDestroyEvent will be used to signal the thread that it is to close
// down.  The FChangeEvent will be used to signal the thread when the monitoring
// conditions (directory, filters or sub-directory search) have changed.
// OnTerminate is left as false, so the user must Free the thread.

constructor TDiscMonitorThread.Create;
begin
  inherited Create (true);
  FDestroyEvent := CreateEvent (nil, true,  false, NIL);
  FChangeEvent  := CreateEvent (nil, false, false, NIL);
end;

// close OnXXXXX links, signal the thread that it is to close down
destructor TDiscMonitorThread.Destroy;
begin
  FOnChange := nil;
  FOnInvalid := nil;
  IF Suspended Then  {ie03}
  Resume;            {ie03}
  SetEvent (FDestroyEvent);
  FDirectory := '';
  inherited Destroy
end;

// called by the Execute procedure via Synchronize.  So this is VCL thread safe
procedure TDiscMonitorThread.InformChange;
begin
  if Assigned (FOnChange) then
    FOnChange (Self)
end;

// called by the Execute procedure via Synchronize.  So this is VCL thread safe
procedure TDiscMonitorThread.InformInvalid;
begin
  if Assigned (FOnInvalid) then
    FOnInvalid (Self)
end;

// Change the current directory
procedure TDiscMonitorThread.SetDirectory (const Value : string);
begin
  if Value <> FDirectory then
  begin
    FDirectory := Value;
    Update
  end
end;

// Change the current filters
procedure TDiscMonitorThread.SetFilters (Value : DWORD);
begin
  if Value <> FFilters then
  begin
    FFilters := Value;
    Update
  end
end;

// Change the current sub-tree condition
procedure TDiscMonitorThread.SetSubTree (Value : boolean);
begin
  if Value <> FSubTree then
  begin
    FSubtree := Value;
    Update
  end
end;


Function TDiscMonitor.GetChangeDelay : Integer;  {ie01}
begin
  Result := FMonitor.ChangeDelay;
end;


Procedure TDiscMonitor.SetChangeDelay(Value : Integer);  {ie01}
begin
  FMonitor.ChangeDelay := Value;
end;


// On any of the above three changes, if the thread is running then
// signal it that a change has occurred.
procedure TDiscMonitorThread.Update;
begin
  if not Suspended then
    SetEvent (FChangeEvent)
end;

// The EXECUTE procedure
//     -------
// Execute needs to:
// 1. Call FindFirstChangeNotification and use the Handle in a WaitFor...
//    to wait until the thread become signalled that a notification has occurred.
//    The OnChange event is called and then the FindNextChangeNotification is
//    the called and Execute loops back to the WaitFor
// 2. If an invalid handle is obtained from the above call, the the OnInvalid
//    event is called and then Execute waits until valid conditions are set.
// 3. If a ChangeEvent is signalled then FindCloseChangeNotification is called,
//    followed by a new FindFirstChangeNotification to use the altered
//    conditions.
// 4. If a DestroyEvent is signalled then FindCloseChangeNotification is
//    called and the two events are closed and the thread terminates.
//
// In practice WaitForMultipleObjects is used to wait for any of the conditions
// to be signalled, and the returned value used to determine which event occurred.

procedure TDiscMonitorThread.Execute;
// There appears to be a bug in win 95 where the bWatchSubTree parameter
// of FindFirstChangeNotification which is a BOOL only accepts values of
// 0 and 1 as valid, rather than 0 and any non-0 value as it should.  In D2
// BOOL was defined as 0..1 so the code worked, in D3 it is 0..-1 so
// fails. The result is FindF... produces and error message.  This fix (bodge) is
// needed to produce a 0,1 bool pair, rather that 0,-1 as declared in D3

{$IFDEF BUGFIX}
const R : Array [false..true] of TWinBOOL = (WinFalse, WinTrue);
{$ELSE}
const R : Array [false..true] of LongBool = (LongBool(0), LongBool(1));
{$ENDIF}

var A : Array [0..2] of THandle; // used to give the handles to WaitFor...
    B : boolean;                 // set to true when the thread is to terminate

Function StartMonitor : THandle;
Begin
{$IFDEF BUGFIX}
  Result := FixFindFirstChangeNotification (PChar(FDirectory), R[fSubTree], FFilters);
{$ELSE}
  Result := FindFirstChangeNotification (PChar(FDirectory), R[fSubTree], FFilters);
{$ENDIF}
End; {StartMonitor}

begin {Execute}
  B := false;
  A [0] := FDestroyEvent;      // put DestroyEvent handle in slot 0
  A [1] := FChangeEvent;       // put ChangeEvent handle in slot 1
// make the first call to the change notification system and put the returned
// handle in slot 2.

  A [2] := StartMonitor;

  repeat

// if the change notification handle is invalid then:
    if A[2] = INVALID_HANDLE_VALUE then
    begin
  // call the OnInvalid event
      Synchronize (InformInvalid);
  // wait until either DestroyEvent or the ChangeEvents are signalled
      case WaitForMultipleObjects (2, PWOHandleArray (@A), false, INFINITE) - WAIT_OBJECT_0 of
  // DestroyEvent - close down by setting B to true
        0 : B := true;
  // try new conditions and loop back to the invalid handle test
        1 : A [2] := StartMonitor;
      end
    end else
// handle is valid so wait for any of the change notification, destroy or
// change events to be signalled
      case WaitForMultipleObjects (3, PWOHandleArray (@A), false, INFINITE) - WAIT_OBJECT_0 of
        0 : begin
  // DestroyEvent signalled so use FindClose... and close down by setting B to true
              FindCloseChangeNotification (A [2]);
              B := true
            end;
        1 : begin
  // ChangeEvent signalled so close old conditions by FindClose... and start
  // off new conditions.  Loop back to invalid test in case new conditions are
  // invalid
              FindCloseChangeNotification (A [2]);
              A [2] := StartMonitor;
            end;
        2 : begin
  // Notification signalled, so fire the OnChange event and then FindNext..
  // loop back to re-WaitFor... the thread
              Sleep(fChangeDelay);  {ie01 ins}
              Synchronize (InformChange);
              FindNextChangeNotification (A [2])
            end;
      end
  until B Or Self.Terminated;

// closing down so chuck the two events
  CloseHandle (FChangeEvent);
  CloseHandle (FDestroyEvent)
end;

//=== MONITOR COMPONENT ========================================================

// This component encapsulates the above thread.  It has properties for
// directory, sub-directory conditions, filters, whether information messages
// should be given at design time and if the thread is active.
constructor TDiscMonitor.Create (AOwner : TComponent);
begin
  inherited Create (AOwner);
  FMonitor := TDiscMonitorThread.Create;  // create a monitor thread
  FMonitor.ChangeDelay := 500;            {ie01}
  FMonitor.OnChange := Change;            // hook into its event handlers
  FMonitor.OnInvalid := Invalid;
  Filters := [moFilename];                // default filters to moFilename
  SubTree := true                         // default sub-tree search to on
end;

destructor TDiscMonitor.Destroy;
begin
  FMonitor.Free;                          // chuck the thread
  inherited Destroy
end;

// Change notification from the thread has occurred. Call the component's event
// handler and then, if in design mode, and if desired, put up a simple
// notification message
procedure TDiscMonitor.Change;
begin
  if Assigned (FOnChange) then
    FOnChange (Self)
  else
    if (csDesigning in ComponentState) and FShowMsg then
      ShowMessage ('Change signalled')
end;

// Invalid notification from the thread has occurred. Call the component's event
// handler and then, if in design mode, and if desired, put up a simple
// notification message
procedure TDiscMonitor.Invalid;
begin
  if Assigned (FOnInvalid) then
    FOnInvalid (Self)
  else
    if (csDesigning in ComponentState) and FShowMsg then
      ShowMessage ('Invalid parameter signalled')
end;

// Stop the monitor running
procedure TDiscMonitor.Close;
begin
  Active := false
end;

// Run the monitor
procedure TDiscMonitor.Open;
begin
  Active := true
end;

// Control the thread by using it's resume and suspend methods
procedure TDiscMonitor.SetActive (Value : boolean);
begin
  if Value <> FActive then
  begin
    FActive := Value;
    if Active then
    begin
      FMonitor.Resume;
      FMonitor.Update
    end else
      FMonitor.Suspend
  end
end;

// get the current directory from the thread
function TDiscMonitor.GetDirectory : TDiscMonitorDirStr;
begin
  Result := FMonitor.Directory
end;

// get the current sub-tree status from the thread
function TDiscMonitor.GetSubTree : boolean;
begin
  Result := FMonitor.SubTree
end;

// set the directory to monitor
procedure TDiscMonitor.SetDirectory (Value : TDiscMonitorDirStr);
begin
  FMonitor.Directory := Value
end;

// Change the filter conditions.  The thread uses the raw windows constants
// (FILE_NOTIFY_CHANGE_XXXX) but the components uses a set of enumurated type.
// It is therefore necessary to translate from the component format into
// an integer value for the thread.
procedure TDiscMonitor.SetFilters (Value : TMonitorFilters);
const
  XlatFileNotify : array [moFilename..moSecurity] of DWORD =
    (FILE_NOTIFY_CHANGE_FILE_NAME,  FILE_NOTIFY_CHANGE_DIR_NAME,
     FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE,
     FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_SECURITY);
var
  L : TMonitorFilter;
  I : DWORD;
begin
  if Value <> FFilters then
    if Value = [] then
      ShowMessage ('Some filter condition must be set.')
    else begin
      FFilters := Value;
      I := 0;
      for L := moFilename to moSecurity do
        if L in Value then
          I := I or XlatFileNotify [L];
      FMonitor.Filters := I;
    end
end;

// set the sub-tree status in the thread
procedure TDiscMonitor.SetSubTree (Value : boolean);
begin
  FMonitor.SubTree := Value
end;

{ie02 TBrowseDirectoryDlg deleted.}

procedure Register;
begin
  {MP}RegisterComponents ({'Tools'}'DriveDir', [TDiscMonitor]);
  {RegisterPropertyEditor (TypeInfo (TDiscMonitorDirStr), nil, '', TDiscMonitorDirStrProperty);}
end;

end.


