DiscMon.pas 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. {
  2. Modifications:
  3. ==============
  4. ie01: OnChange delayed for 500 ms.
  5. ie02: No usage of BrowseDirectory component
  6. ie03: Resume suspended thread when terminating the thread
  7. }
  8. // ==================== DISC DRIVE MONITOR =====================================
  9. //
  10. // Class and Component to encapsulate the FindXXXXChangeNotification API calls
  11. //
  12. // The FindXXXXChangeNotification API calls set up a disc contents change
  13. // notification handle. You can set a filter to control which change types
  14. // are notified, the directory which is monitored and set whether subdirectories
  15. // from the monitored directory are monitored as well.
  16. //
  17. //------------------------------------------------------------------------------
  18. // This file contains a class derived from TThread which undertakes the disc
  19. // monitoring and a simple component which encapsulates the thread to make
  20. // a non-visual VCL component. This component works at design time, monitoring
  21. // and notifying changes live if required.
  22. //
  23. // Version 1.00 - Grahame Marsh 14 January 1997
  24. // Version 1.01 - Grahame Marsh 30 December 1997
  25. // Bug fix - really a Win 95 bug but only surfaces in D3, not D2
  26. // - see notes in execute method
  27. // Version 1.02 - Grahame Marsh 30 January 1998
  28. // - adapted to work with version 2.30 TBrowseDirectoryDlg
  29. //
  30. // Freeware - you get it for free, I take nothing, I make no promises!
  31. //
  32. // Please feel free to contact me: [email protected]
  33. {$DEFINE BUGFIX}
  34. unit DiscMon;
  35. interface
  36. uses
  37. Windows, Messages, SysUtils, Classes, Graphics, Controls,
  38. Forms, Dialogs, ShlObj, ActiveX, CompThread {, BrowseDr, DsgnIntf ie02};
  39. //=== DISC MONITORING THREAD ===================================================
  40. // This thread will monitor a given directory and subdirectories (if required)
  41. // for defined filtered changes. When a change occurs the OnChange event will
  42. // be fired, if an invalid condition is found (eg non-existent path) then
  43. // the OnInvalid event is fired. Each event is called via the Sychronize method
  44. // and so are VCL thread safe.
  45. //
  46. // The thread is created suspended, so after setting the required properties
  47. // you must call the Resume method.
  48. type
  49. TDiscMonitorThread = class(TCompThread)
  50. private
  51. FOnChange : TNotifyEvent;
  52. FOnInvalid : TNotifyEvent;
  53. FDirectory : string;
  54. FFilters : DWORD;
  55. FDestroyEvent,
  56. FChangeEvent : THandle;
  57. FSubTree : boolean;
  58. fChangeDelay : Integer; {ie01}
  59. procedure InformChange;
  60. procedure InformInvalid;
  61. procedure SetDirectory (const Value : string);
  62. procedure SetFilters (Value : DWORD);
  63. procedure SetSubTree (Value : boolean);
  64. protected
  65. procedure Execute; override;
  66. procedure Update;
  67. public
  68. constructor Create;
  69. destructor Destroy; override;
  70. // The directory to monitor
  71. property Directory : string read FDirectory write SetDirectory;
  72. // Filter condition, may be any of the FILE_NOTIFY_CHANGE_XXXXXXX constants
  73. // ORed together. Zero is invalid.
  74. property Filters : DWORD read FFilters write SetFilters;
  75. // Event called when change noted in directory
  76. property OnChange : TNotifyEvent read FOnChange write FOnChange;
  77. // Event called for invalid parameters
  78. property OnInvalid : TNotifyEvent read FOnInvalid write FOnInvalid;
  79. // Include subdirectories below specified directory.
  80. property SubTree : boolean read FSubTree write SetSubTree;
  81. // specify, how long the thread should wait, before the event OnChange is fired:
  82. Property ChangeDelay : Integer Read fChangeDelay Write fChangeDelay {ie01}
  83. Default 500; {ie01}
  84. end;
  85. //===================== DISC MONITORING COMPONENT ==============================
  86. // specify directory string as type string so we can have our own property editor
  87. TDiscMonitorDirStr = type string;
  88. // enumerated type for filter conditions (not directly usable in thread class)
  89. // see the SetFilters procedure for the translation of these filter conditions
  90. // into FILE_NOTIFY_CHANGE_XXXXXX constants.
  91. TMonitorFilter = (moFilename, moDirName, moAttributes, moSize,
  92. moLastWrite, moSecurity);
  93. // set of filter conditions
  94. TMonitorFilters = set of TMonitorFilter;
  95. TDiscMonitor = class(TComponent)
  96. private
  97. FActive : boolean;
  98. FMonitor : TDiscMonitorThread;
  99. FFilters : TMonitorFilters;
  100. FOnChange : TNotifyEvent;
  101. FOnInvalid : TNotifyEvent;
  102. FShowMsg : boolean;
  103. function GetDirectory : TDiscMonitorDirStr;
  104. function GetSubTree : boolean;
  105. procedure SetActive (Value : boolean);
  106. procedure SetDirectory (Value : TDiscMonitorDirStr);
  107. procedure SetFilters (Value : TMonitorFilters);
  108. procedure SetSubTree (Value : boolean);
  109. Function GetChangeDelay : Integer; {ie01}
  110. Procedure SetChangeDelay(Value : Integer); {ie01}
  111. protected
  112. procedure Change (Sender : TObject);
  113. procedure Invalid (Sender : TObject);
  114. public
  115. constructor Create (AOwner : TComponent); Override;
  116. destructor Destroy; override;
  117. // stop the monitoring thread running
  118. procedure Close;
  119. // start the monitoring thread running
  120. procedure Open;
  121. // read-only property to access the thread directly
  122. property Thread : TDiscMonitorThread read FMonitor;
  123. published
  124. // the directory to monitor
  125. property Directory : TDiscMonitorDirStr read GetDirectory write SetDirectory;
  126. // control the appearance of information messages at design time (only)
  127. property ShowDesignMsg : boolean read FShowMsg write FShowMsg default false;
  128. // event called when a change is notified
  129. property OnChange : TNotifyEvent read FOnChange write FOnChange;
  130. // event called if an invalid condition is found
  131. property OnInvalid : TNotifyEvent read FOnInvalid write FOnInvalid;
  132. // notification filter conditions
  133. property Filters : TMonitorFilters read FFilters write SetFilters default [moFilename];
  134. // include subdirectories below the specified directory
  135. property SubTree : boolean read GetSubTree write SetSubTree default true;
  136. // specify if the monitoring thread is active
  137. property Active : boolean read FActive write SetActive default false;
  138. // specify, how long the thread should wait, before the event OnChange is fired:
  139. Property ChangeDelay : Integer Read GetChangeDelay Write SetChangeDelay {ie01}
  140. Default 500; {ie01}
  141. end;
  142. procedure Register;
  143. {$IFDEF BUGFIX}
  144. {$Z4}
  145. type TWinBool = (winFalse, winTrue);
  146. function FixFindFirstChangeNotification(const lpPathName: PChar;
  147. bWatchSubtree: TWinBool;
  148. dwNotifyFilter: DWORD): THandle stdcall; external kernel32 name 'FindFirstChangeNotificationA';
  149. {$ENDIF}
  150. implementation
  151. //=== MONITOR THREAD ===========================================================
  152. // Create the thread suspended. Create two events, each are created using
  153. // standard security, in the non-signalled state, with auto-reset and without
  154. // names. The FDestroyEvent will be used to signal the thread that it is to close
  155. // down. The FChangeEvent will be used to signal the thread when the monitoring
  156. // conditions (directory, filters or sub-directory search) have changed.
  157. // OnTerminate is left as false, so the user must Free the thread.
  158. constructor TDiscMonitorThread.Create;
  159. begin
  160. inherited Create (true);
  161. FDestroyEvent := CreateEvent (nil, true, false, NIL);
  162. FChangeEvent := CreateEvent (nil, false, false, NIL);
  163. end;
  164. // close OnXXXXX links, signal the thread that it is to close down
  165. destructor TDiscMonitorThread.Destroy;
  166. begin
  167. FOnChange := nil;
  168. FOnInvalid := nil;
  169. IF Suspended Then {ie03}
  170. Resume; {ie03}
  171. SetEvent (FDestroyEvent);
  172. FDirectory := '';
  173. inherited Destroy
  174. end;
  175. // called by the Execute procedure via Synchronize. So this is VCL thread safe
  176. procedure TDiscMonitorThread.InformChange;
  177. begin
  178. if Assigned (FOnChange) then
  179. FOnChange (Self)
  180. end;
  181. // called by the Execute procedure via Synchronize. So this is VCL thread safe
  182. procedure TDiscMonitorThread.InformInvalid;
  183. begin
  184. if Assigned (FOnInvalid) then
  185. FOnInvalid (Self)
  186. end;
  187. // Change the current directory
  188. procedure TDiscMonitorThread.SetDirectory (const Value : string);
  189. begin
  190. if Value <> FDirectory then
  191. begin
  192. FDirectory := Value;
  193. Update
  194. end
  195. end;
  196. // Change the current filters
  197. procedure TDiscMonitorThread.SetFilters (Value : DWORD);
  198. begin
  199. if Value <> FFilters then
  200. begin
  201. FFilters := Value;
  202. Update
  203. end
  204. end;
  205. // Change the current sub-tree condition
  206. procedure TDiscMonitorThread.SetSubTree (Value : boolean);
  207. begin
  208. if Value <> FSubTree then
  209. begin
  210. FSubtree := Value;
  211. Update
  212. end
  213. end;
  214. Function TDiscMonitor.GetChangeDelay : Integer; {ie01}
  215. begin
  216. Result := FMonitor.ChangeDelay;
  217. end;
  218. Procedure TDiscMonitor.SetChangeDelay(Value : Integer); {ie01}
  219. begin
  220. FMonitor.ChangeDelay := Value;
  221. end;
  222. // On any of the above three changes, if the thread is running then
  223. // signal it that a change has occurred.
  224. procedure TDiscMonitorThread.Update;
  225. begin
  226. if not Suspended then
  227. SetEvent (FChangeEvent)
  228. end;
  229. // The EXECUTE procedure
  230. // -------
  231. // Execute needs to:
  232. // 1. Call FindFirstChangeNotification and use the Handle in a WaitFor...
  233. // to wait until the thread become signalled that a notification has occurred.
  234. // The OnChange event is called and then the FindNextChangeNotification is
  235. // the called and Execute loops back to the WaitFor
  236. // 2. If an invalid handle is obtained from the above call, the the OnInvalid
  237. // event is called and then Execute waits until valid conditions are set.
  238. // 3. If a ChangeEvent is signalled then FindCloseChangeNotification is called,
  239. // followed by a new FindFirstChangeNotification to use the altered
  240. // conditions.
  241. // 4. If a DestroyEvent is signalled then FindCloseChangeNotification is
  242. // called and the two events are closed and the thread terminates.
  243. //
  244. // In practice WaitForMultipleObjects is used to wait for any of the conditions
  245. // to be signalled, and the returned value used to determine which event occurred.
  246. procedure TDiscMonitorThread.Execute;
  247. // There appears to be a bug in win 95 where the bWatchSubTree parameter
  248. // of FindFirstChangeNotification which is a BOOL only accepts values of
  249. // 0 and 1 as valid, rather than 0 and any non-0 value as it should. In D2
  250. // BOOL was defined as 0..1 so the code worked, in D3 it is 0..-1 so
  251. // fails. The result is FindF... produces and error message. This fix (bodge) is
  252. // needed to produce a 0,1 bool pair, rather that 0,-1 as declared in D3
  253. {$IFDEF BUGFIX}
  254. const R : Array [false..true] of TWinBOOL = (WinFalse, WinTrue);
  255. {$ELSE}
  256. const R : Array [false..true] of LongBool = (LongBool(0), LongBool(1));
  257. {$ENDIF}
  258. var A : Array [0..2] of THandle; // used to give the handles to WaitFor...
  259. B : boolean; // set to true when the thread is to terminate
  260. Function StartMonitor : THandle;
  261. Begin
  262. {$IFDEF BUGFIX}
  263. Result := FixFindFirstChangeNotification (PChar(FDirectory), R[fSubTree], FFilters);
  264. {$ELSE}
  265. Result := FindFirstChangeNotification (PChar(FDirectory), R[fSubTree], FFilters);
  266. {$ENDIF}
  267. End; {StartMonitor}
  268. begin {Execute}
  269. B := false;
  270. A [0] := FDestroyEvent; // put DestroyEvent handle in slot 0
  271. A [1] := FChangeEvent; // put ChangeEvent handle in slot 1
  272. // make the first call to the change notification system and put the returned
  273. // handle in slot 2.
  274. A [2] := StartMonitor;
  275. repeat
  276. // if the change notification handle is invalid then:
  277. if A[2] = INVALID_HANDLE_VALUE then
  278. begin
  279. // call the OnInvalid event
  280. Synchronize (InformInvalid);
  281. // wait until either DestroyEvent or the ChangeEvents are signalled
  282. case WaitForMultipleObjects (2, PWOHandleArray (@A), false, INFINITE) - WAIT_OBJECT_0 of
  283. // DestroyEvent - close down by setting B to true
  284. 0 : B := true;
  285. // try new conditions and loop back to the invalid handle test
  286. 1 : A [2] := StartMonitor;
  287. end
  288. end else
  289. // handle is valid so wait for any of the change notification, destroy or
  290. // change events to be signalled
  291. case WaitForMultipleObjects (3, PWOHandleArray (@A), false, INFINITE) - WAIT_OBJECT_0 of
  292. 0 : begin
  293. // DestroyEvent signalled so use FindClose... and close down by setting B to true
  294. FindCloseChangeNotification (A [2]);
  295. B := true
  296. end;
  297. 1 : begin
  298. // ChangeEvent signalled so close old conditions by FindClose... and start
  299. // off new conditions. Loop back to invalid test in case new conditions are
  300. // invalid
  301. FindCloseChangeNotification (A [2]);
  302. A [2] := StartMonitor;
  303. end;
  304. 2 : begin
  305. // Notification signalled, so fire the OnChange event and then FindNext..
  306. // loop back to re-WaitFor... the thread
  307. Sleep(fChangeDelay); {ie01 ins}
  308. Synchronize (InformChange);
  309. FindNextChangeNotification (A [2])
  310. end;
  311. end
  312. until B Or Self.Terminated;
  313. // closing down so chuck the two events
  314. CloseHandle (FChangeEvent);
  315. CloseHandle (FDestroyEvent)
  316. end;
  317. //=== MONITOR COMPONENT ========================================================
  318. // This component encapsulates the above thread. It has properties for
  319. // directory, sub-directory conditions, filters, whether information messages
  320. // should be given at design time and if the thread is active.
  321. constructor TDiscMonitor.Create (AOwner : TComponent);
  322. begin
  323. inherited Create (AOwner);
  324. FMonitor := TDiscMonitorThread.Create; // create a monitor thread
  325. FMonitor.ChangeDelay := 500; {ie01}
  326. FMonitor.OnChange := Change; // hook into its event handlers
  327. FMonitor.OnInvalid := Invalid;
  328. Filters := [moFilename]; // default filters to moFilename
  329. SubTree := true // default sub-tree search to on
  330. end;
  331. destructor TDiscMonitor.Destroy;
  332. begin
  333. FMonitor.Free; // chuck the thread
  334. inherited Destroy
  335. end;
  336. // Change notification from the thread has occurred. Call the component's event
  337. // handler and then, if in design mode, and if desired, put up a simple
  338. // notification message
  339. procedure TDiscMonitor.Change;
  340. begin
  341. if Assigned (FOnChange) then
  342. FOnChange (Self)
  343. else
  344. if (csDesigning in ComponentState) and FShowMsg then
  345. ShowMessage ('Change signalled')
  346. end;
  347. // Invalid notification from the thread has occurred. Call the component's event
  348. // handler and then, if in design mode, and if desired, put up a simple
  349. // notification message
  350. procedure TDiscMonitor.Invalid;
  351. begin
  352. if Assigned (FOnInvalid) then
  353. FOnInvalid (Self)
  354. else
  355. if (csDesigning in ComponentState) and FShowMsg then
  356. ShowMessage ('Invalid parameter signalled')
  357. end;
  358. // Stop the monitor running
  359. procedure TDiscMonitor.Close;
  360. begin
  361. Active := false
  362. end;
  363. // Run the monitor
  364. procedure TDiscMonitor.Open;
  365. begin
  366. Active := true
  367. end;
  368. // Control the thread by using it's resume and suspend methods
  369. procedure TDiscMonitor.SetActive (Value : boolean);
  370. begin
  371. if Value <> FActive then
  372. begin
  373. FActive := Value;
  374. if Active then
  375. begin
  376. FMonitor.Resume;
  377. FMonitor.Update
  378. end else
  379. FMonitor.Suspend
  380. end
  381. end;
  382. // get the current directory from the thread
  383. function TDiscMonitor.GetDirectory : TDiscMonitorDirStr;
  384. begin
  385. Result := FMonitor.Directory
  386. end;
  387. // get the current sub-tree status from the thread
  388. function TDiscMonitor.GetSubTree : boolean;
  389. begin
  390. Result := FMonitor.SubTree
  391. end;
  392. // set the directory to monitor
  393. procedure TDiscMonitor.SetDirectory (Value : TDiscMonitorDirStr);
  394. begin
  395. FMonitor.Directory := Value
  396. end;
  397. // Change the filter conditions. The thread uses the raw windows constants
  398. // (FILE_NOTIFY_CHANGE_XXXX) but the components uses a set of enumurated type.
  399. // It is therefore necessary to translate from the component format into
  400. // an integer value for the thread.
  401. procedure TDiscMonitor.SetFilters (Value : TMonitorFilters);
  402. const
  403. XlatFileNotify : array [moFilename..moSecurity] of DWORD =
  404. (FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME,
  405. FILE_NOTIFY_CHANGE_ATTRIBUTES, FILE_NOTIFY_CHANGE_SIZE,
  406. FILE_NOTIFY_CHANGE_LAST_WRITE, FILE_NOTIFY_CHANGE_SECURITY);
  407. var
  408. L : TMonitorFilter;
  409. I : DWORD;
  410. begin
  411. if Value <> FFilters then
  412. if Value = [] then
  413. ShowMessage ('Some filter condition must be set.')
  414. else begin
  415. FFilters := Value;
  416. I := 0;
  417. for L := moFilename to moSecurity do
  418. if L in Value then
  419. I := I or XlatFileNotify [L];
  420. FMonitor.Filters := I;
  421. end
  422. end;
  423. // set the sub-tree status in the thread
  424. procedure TDiscMonitor.SetSubTree (Value : boolean);
  425. begin
  426. FMonitor.SubTree := Value
  427. end;
  428. {ie02 TBrowseDirectoryDlg deleted.}
  429. procedure Register;
  430. begin
  431. {MP}RegisterComponents ({'Tools'}'DriveDir', [TDiscMonitor]);
  432. {RegisterPropertyEditor (TypeInfo (TDiscMonitorDirStr), nil, '', TDiscMonitorDirStrProperty);}
  433. end;
  434. end.