├── .gitignore
├── App.config
├── Demo.sln
├── FileSystemWatcherEx.cs
├── LISENCE
├── MainForm.Designer.cs
├── MainForm.cs
├── MainForm.resx
├── Program.cs
├── Properties
├── AssemblyInfo.cs
├── Resources.Designer.cs
├── Resources.resx
├── Settings.Designer.cs
└── Settings.settings
├── README.md
├── demo.csproj
├── result.gif
└── testImages
├── copy.ps1
├── image1.bmp
├── image2.bmp
├── image3.bmp
└── image4.bmp
/.gitignore:
--------------------------------------------------------------------------------
1 | !/DLLs/
2 | *.exe
3 | *.exp
4 | *.ilk
5 | *.lib
6 | *.ncb
7 | *.log
8 | *.pdb
9 | *.vcproj.*.user
10 | *.suo
11 | ._*
12 | [Dd]ebug
13 | [Rr]elease
14 | [Oo]bj/
15 | [Bb]in
16 | !packages/build/
17 | Bak/
18 | packages/
19 | Key/
20 | .vs/
21 | !NuGet.exe
22 |
--------------------------------------------------------------------------------
/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Demo.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 2013
4 | VisualStudioVersion = 12.0.40629.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "demo", "demo.csproj", "{DB263916-B289-4414-BAFC-711B7ED1E66F}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {DB263916-B289-4414-BAFC-711B7ED1E66F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {DB263916-B289-4414-BAFC-711B7ED1E66F}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {DB263916-B289-4414-BAFC-711B7ED1E66F}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {DB263916-B289-4414-BAFC-711B7ED1E66F}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | EndGlobal
23 |
--------------------------------------------------------------------------------
/FileSystemWatcherEx.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 |
9 | ///
10 | /// a simple enhanced file watcher class based on official FileSystemWatcher.
11 | ///
12 | public class FileSystemWatcherEx
13 | {
14 | #region options,ref https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher?view=net-5.0
15 | ///
16 | /// folder to watch, must exist.
17 | ///
18 | public string Path
19 | {
20 | get { return _path; }
21 | set { _path = value; }
22 | }
23 | private string _path = "";
24 |
25 | ///
26 | /// FilterList contains all filters
27 | /// *.* : watch for all files.
28 | /// *.docx : only watch .docx files.
29 | /// a.* : watch file a, all extensions match.
30 | ///
31 | public string Filters
32 | {
33 | get { return _filters; }
34 | set { _filters = value; }
35 | }
36 | private string _filters;
37 |
38 | ///
39 | /// recursive watch.
40 | ///
41 | public bool Recursive
42 | {
43 | get { return _recursive; }
44 | set { _recursive = value; }
45 | }
46 | private bool _recursive = true;
47 |
48 | public NotifyFilters NotifyFilters
49 | {
50 | get { return _notifyFilters; }
51 | set { _notifyFilters = value; }
52 | }
53 |
54 | private NotifyFilters _notifyFilters = NotifyFilters.DirectoryName | NotifyFilters.FileName
55 | | NotifyFilters.LastWrite | NotifyFilters.CreationTime | NotifyFilters.LastAccess
56 | | NotifyFilters.Attributes | NotifyFilters.Size | NotifyFilters.Security
57 | ;
58 |
59 | #endregion
60 |
61 | #region optionex (to avoid same event trigger multiple times and wait for file ready to access)
62 |
63 | ///
64 | /// sometimes, same event may be triggered multiple times.
65 | /// set this flag true to try merge same event.
66 | ///
67 | public bool TryMergeSameEvent
68 | {
69 | get { return _tryMergeSameEvent; }
70 | set { _tryMergeSameEvent = value; }
71 | }
72 | private bool _tryMergeSameEvent = true;
73 |
74 | ///
75 | /// max wait time to handle same event
76 | ///
77 | public int DelayTriggerMs
78 | {
79 | get { return _delayTriggerMs; }
80 | set { _delayTriggerMs = value; }
81 | }
82 | private int _delayTriggerMs = 10;
83 |
84 | ///
85 | /// when copying a huge file to monitor folder, we receive file create event at the beginning,
86 | /// but this file can not be access immediately.
87 | ///
88 | public bool WaitForFileReadyToAccess
89 | {
90 | get { return _waitForFileReadyToAccess; }
91 | set { _waitForFileReadyToAccess = value; }
92 | }
93 | private bool _waitForFileReadyToAccess = true;
94 |
95 | ///
96 | /// max wait time for a file until it can be accessed.
97 | ///
98 | public int MaxWaitMs
99 | {
100 | get { return _maxWaitMs; }
101 | set { _maxWaitMs = value; }
102 | }
103 | private int _maxWaitMs = int.MaxValue;
104 |
105 | ///
106 | /// when file is not ready to access immediately, how frequent to check again.
107 | ///
108 | public int FileAccessCheckIntervalMs
109 | {
110 | get { return _fileAccessCheckIntervalMs; }
111 | set { _fileAccessCheckIntervalMs = value; }
112 | }
113 | private int _fileAccessCheckIntervalMs = 100;
114 |
115 | #endregion
116 |
117 | #region event callback,ref https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher?view=net-5.0
118 |
119 | public event FileSystemEventHandler OnCreated;
120 |
121 | public event FileSystemEventHandler OnDeleted;
122 |
123 | public event FileSystemEventHandler OnChanged;
124 |
125 | public event RenamedEventHandler OnRenamed;
126 |
127 | public event ErrorEventHandler OnError;
128 |
129 | internal void OnFileSystemEventHandler(object sender, FileSystemEventArgs e)
130 | {
131 | System.Diagnostics.Debug.WriteLine(string.Format("[inner] path: {0}, event type: {1}", e.FullPath, e.ChangeType.ToString()));
132 | AddEventData(new FileSystemEventNotifyData { sender = sender, eventArgs = e });
133 | eventFiredEvent.Set();
134 | }
135 |
136 | internal void OnRenamedEventHandler(object sender, RenamedEventArgs e)
137 | {
138 | System.Diagnostics.Debug.WriteLine(string.Format("[inner] old: {0}, new: {1}, event type: {2}", e.OldFullPath, e.FullPath, e.ChangeType.ToString()));
139 | AddEventData(new FileSystemEventNotifyData { sender = sender, eventArgs = e });
140 | eventFiredEvent.Set();
141 | }
142 |
143 | internal void OnErrorHandler(object sender, ErrorEventArgs e)
144 | {
145 | System.Diagnostics.Debug.WriteLine(string.Format("[inner] exception: {0}", e.GetException().ToString()));
146 | AddEventData(new FileSystemEventNotifyData { sender = sender, eventArgs = e });
147 | eventFiredEvent.Set();
148 | }
149 | #endregion
150 |
151 | public string UniqueId
152 | {
153 | get { return _uniqueId; }
154 | protected set { _uniqueId = value; }
155 | }
156 | private string _uniqueId = System.Guid.NewGuid().ToString().ToUpper();
157 |
158 | public List WatcherList
159 | {
160 | get { return _watcherList; }
161 | set { _watcherList = value; }
162 | }
163 | private List _watcherList = new List();
164 |
165 | #region constructors
166 |
167 | /// path to watch, must be existed path
168 | /// watch filters.
169 | /// *.* will monitor all files.
170 | /// *.bmp will monitor all .bmp files.
171 | /// if want to monitor multiple files at the same time, separate by |.
172 | /// e.g. *.txt| *.bmp | *.jpg will monitor .txt & .bmp & .jpg files.
173 | ///
174 | ///
175 | public FileSystemWatcherEx(string path
176 | , string filters = "*.*"
177 | , bool bRecursive = true
178 | , string uniqueId = ""
179 | , FileSystemEventHandler OnCreatedHandler = null
180 | , FileSystemEventHandler OnDeletedHandler = null
181 | , FileSystemEventHandler OnChangedHandler = null
182 | , RenamedEventHandler OnRenamedHandler = null
183 | , ErrorEventHandler OnErrorHandler = null
184 | )
185 | {
186 | if (!string.IsNullOrEmpty(uniqueId))
187 | {
188 | UniqueId = uniqueId;
189 | }
190 |
191 | if (!string.IsNullOrEmpty(path))
192 | {
193 | Path = path;
194 | }
195 |
196 | Filters = filters;
197 |
198 | if (OnCreatedHandler != null)
199 | {
200 | OnCreated += OnCreatedHandler;
201 | }
202 |
203 | if (OnDeletedHandler != null)
204 | {
205 | OnDeleted += OnDeletedHandler;
206 | }
207 |
208 | if (OnChangedHandler != null)
209 | {
210 | OnChanged += OnChangedHandler;
211 | }
212 |
213 | if (OnRenamedHandler != null)
214 | {
215 | OnRenamed += OnRenamedHandler;
216 | }
217 |
218 | if (OnErrorHandler != null)
219 | {
220 | OnError += OnErrorHandler;
221 | }
222 | }
223 |
224 | #endregion
225 |
226 | public bool Start()
227 | {
228 | if (!Directory.Exists(this.Path))
229 | {
230 | return false;
231 | }
232 |
233 | char[] splitter = { '|' };
234 | var filterList = Filters.Split(splitter).ToList();
235 | foreach (var filter in filterList)
236 | {
237 | FileSystemWatcher watcher = new FileSystemWatcher();
238 |
239 | watcher.Filter = filter;
240 | watcher.Path = this.Path;
241 | watcher.IncludeSubdirectories = this.Recursive;
242 | watcher.NotifyFilter = this.NotifyFilters;
243 |
244 | watcher.Created += this.OnFileSystemEventHandler;
245 | watcher.Deleted += this.OnFileSystemEventHandler;
246 | watcher.Changed += this.OnFileSystemEventHandler;
247 | watcher.Renamed += this.OnRenamedEventHandler;
248 | watcher.Error += this.OnErrorHandler;
249 |
250 | watcher.EnableRaisingEvents = true;
251 |
252 | WatcherList.Add(watcher);
253 | }
254 |
255 | return StartEventFireNotifyThread();
256 | }
257 |
258 | public bool Stop()
259 | {
260 | Enable(false);
261 | StopEventFireNotifyThread();
262 | return true;
263 | }
264 |
265 | public bool Enable(bool bEnable)
266 | {
267 | foreach (var watcher in WatcherList)
268 | {
269 | watcher.EnableRaisingEvents = false;
270 | }
271 |
272 | return true;
273 | }
274 |
275 | #region notify thread
276 |
277 | protected bool StartEventFireNotifyThread()
278 | {
279 | if (m_notifyThread == null || !m_notifyThread.IsAlive)
280 | {
281 | m_notifyThread = new System.Threading.Thread(HandleEventFireNotifyWorkProc);
282 | m_notifyThread.Name = string.Format(string.Format("[WatchEx] : {0}-{1}", Filters, Path));
283 | m_notifyThread.IsBackground = true;
284 | m_notifyThread.Start();
285 | }
286 |
287 | return true;
288 | }
289 |
290 | protected bool StopEventFireNotifyThread()
291 | {
292 | bQuit = true;
293 | eventFiredEvent.Set();
294 | return true;
295 | }
296 |
297 | protected void HandleEventFireNotifyWorkProc()
298 | {
299 | var cameraEvents = new WaitHandle[]
300 | {
301 | eventFiredEvent
302 | };
303 |
304 | while (!bQuit)
305 | {
306 | var waitResult = WaitHandle.WaitAny(cameraEvents, System.Threading.Timeout.Infinite);
307 |
308 | if (TryMergeSameEvent)
309 | {
310 | System.Threading.Thread.Sleep(DelayTriggerMs);
311 | }
312 |
313 | List eventDataList;
314 | lock (eventListLocker)
315 | {
316 | eventDataList = m_eventDataList;
317 | m_eventDataList = new List();
318 | }
319 |
320 | NotifyEvents(eventDataList);
321 | }
322 | }
323 |
324 | private void NotifyEvents(List eventDataList)
325 | {
326 | if (eventDataList == null || eventDataList.Count == 0)
327 | {
328 | return;
329 | }
330 |
331 | eventDataList = eventDataList.Distinct().ToList();
332 | foreach (var eventData in eventDataList)
333 | {
334 | if (bQuit)
335 | {
336 | break;
337 | }
338 |
339 | if (NotifyFileSystemEvent(eventData))
340 | {
341 | continue;
342 | }
343 |
344 | if (NotifyRenamedEvent(eventData))
345 | {
346 | continue;
347 | }
348 |
349 | if (NotifyError(eventData))
350 | {
351 | continue;
352 | }
353 | }
354 | }
355 |
356 | private bool NotifyFileSystemEvent(FileSystemEventNotifyData data)
357 | {
358 | FileSystemEventArgs e = data.eventArgs as FileSystemEventArgs;
359 | if (e == null)
360 | {
361 | return false;
362 | }
363 |
364 | if (WaitForFileReadyToAccess)
365 | {
366 | WaitUntilCanAccess(data, MaxWaitMs);
367 | }
368 |
369 | switch (e.ChangeType)
370 | {
371 | case WatcherChangeTypes.Created:
372 | {
373 | if (OnCreated != null)
374 | {
375 | OnCreated(data.sender, e);
376 | }
377 | }
378 | break;
379 | case WatcherChangeTypes.Deleted:
380 | {
381 | if (OnDeleted != null)
382 | {
383 | OnDeleted(data.sender, e);
384 | }
385 | }
386 | break;
387 | case WatcherChangeTypes.Changed:
388 | {
389 | if (OnChanged != null)
390 | {
391 | OnChanged(data.sender, e);
392 | }
393 | }
394 | break;
395 | default:
396 | break;
397 | }
398 |
399 | return true;
400 | }
401 |
402 | private void WaitUntilCanAccess(FileSystemEventNotifyData data, int maxWaitMs)
403 | {
404 | FileSystemEventArgs e = data.eventArgs as FileSystemEventArgs;
405 | if (e == null)
406 | {
407 | return;
408 | }
409 |
410 | bool bNeedWait = (e.ChangeType == WatcherChangeTypes.Created || e.ChangeType == WatcherChangeTypes.Changed);
411 | if (!bNeedWait)
412 | {
413 | return;
414 | }
415 |
416 | var logStartTime = System.DateTime.Now;
417 | var startTime = System.DateTime.Now;
418 | do
419 | {
420 | FileStream stream = null;
421 | try
422 | {
423 | stream = File.Open(e.FullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete | FileShare.Inheritable);
424 | break;
425 | }
426 | catch (IOException ex)
427 | {
428 | if (ex.HResult != -2147024864) // 0x80070020: The process cannot access the file, because it is being used by another process
429 | {
430 | System.Diagnostics.Debug.WriteLine(string.Format("!!!![inner] try opening file [{0}] exception. HResult {1}! GiveUp!", e.FullPath, ex.HResult));
431 | break;
432 | }
433 |
434 | var now = System.DateTime.Now;
435 | if ((now - logStartTime).TotalSeconds > 1) // record exception per second.
436 | {
437 | logStartTime = now;
438 | System.Diagnostics.Debug.WriteLine(string.Format("[inner] fail to open file [{0}], try again!", e.FullPath));
439 | }
440 |
441 | if ((now - startTime).TotalMilliseconds > maxWaitMs)
442 | {
443 | System.Diagnostics.Debug.WriteLine(string.Format("[inner] timeout for waitting file [{0}] to be available, wait ms: {1}!", e.FullPath, maxWaitMs));
444 | break;
445 | }
446 |
447 | Thread.Sleep(FileAccessCheckIntervalMs);
448 |
449 | if (bQuit)
450 | {
451 | break;
452 | }
453 | }
454 | catch (Exception ex)
455 | {
456 | System.Diagnostics.Debug.WriteLine(string.Format("[inner] fail to open file [{0}], unexpected exception {1}!", e.FullPath, ex));
457 | break;
458 | }
459 | finally
460 | {
461 | if (stream != null)
462 | {
463 | stream.Close();
464 | }
465 | }
466 |
467 | } while (true);
468 | }
469 |
470 | private bool NotifyRenamedEvent(FileSystemEventNotifyData data)
471 | {
472 | RenamedEventArgs e = data.eventArgs as RenamedEventArgs;
473 | if (e == null)
474 | {
475 | return false;
476 | }
477 |
478 | if (OnRenamed != null)
479 | {
480 | OnRenamed(data.sender, e);
481 | }
482 |
483 | return true;
484 | }
485 |
486 | private bool NotifyError(FileSystemEventNotifyData data)
487 | {
488 | ErrorEventArgs e = data.eventArgs as ErrorEventArgs;
489 | if (e == null)
490 | {
491 | return false;
492 | }
493 |
494 | if (OnError != null)
495 | {
496 | OnError(data.sender, e);
497 | }
498 |
499 | return true;
500 | }
501 |
502 | public class FileSystemEventNotifyData
503 | {
504 | public object sender;
505 | public EventArgs eventArgs;
506 |
507 | public override int GetHashCode()
508 | {
509 | return 0;
510 | }
511 |
512 | public override bool Equals(object rhs)
513 | {
514 | var data_rhs = rhs as FileSystemEventNotifyData;
515 | if (data_rhs == null)
516 | {
517 | return false;
518 | }
519 |
520 | return Equals(data_rhs);
521 | }
522 |
523 | private bool IsSameChangeType(WatcherChangeTypes lhs, WatcherChangeTypes rhs)
524 | {
525 | return (lhs == rhs)
526 | || (lhs == WatcherChangeTypes.Created && rhs == WatcherChangeTypes.Changed)
527 | || (lhs == WatcherChangeTypes.Changed && rhs == WatcherChangeTypes.Created)
528 | ;
529 | }
530 |
531 | public bool Equals(FileSystemEventNotifyData rhs)
532 | {
533 | var renameEvent_lhs = eventArgs as RenamedEventArgs;
534 | var renameEvent_rhs = rhs.eventArgs as RenamedEventArgs;
535 | if ((renameEvent_lhs != null && renameEvent_rhs != null))
536 | {
537 | return renameEvent_lhs.FullPath == renameEvent_rhs.FullPath
538 | && renameEvent_lhs.Name == renameEvent_rhs.Name
539 | && IsSameChangeType(renameEvent_lhs.ChangeType, renameEvent_rhs.ChangeType)
540 | && renameEvent_lhs.OldFullPath == renameEvent_rhs.OldFullPath
541 | && renameEvent_lhs.OldName == renameEvent_rhs.OldName;
542 | }
543 |
544 | var fileSystemEvent_lhs = eventArgs as FileSystemEventArgs;
545 | var fileSystemEvent_rhs = rhs.eventArgs as FileSystemEventArgs;
546 | if ((fileSystemEvent_lhs != null && fileSystemEvent_rhs != null))
547 | {
548 | return fileSystemEvent_lhs.FullPath == fileSystemEvent_rhs.FullPath
549 | && fileSystemEvent_lhs.Name == fileSystemEvent_rhs.Name
550 | && IsSameChangeType(fileSystemEvent_lhs.ChangeType, fileSystemEvent_lhs.ChangeType);
551 | }
552 |
553 | var errorEvent_lhs = eventArgs as ErrorEventArgs;
554 | var errorEvent_rhs = rhs.eventArgs as ErrorEventArgs;
555 | if ((renameEvent_lhs != null && renameEvent_rhs != null))
556 | {
557 | return true;
558 | }
559 |
560 | return false;
561 | }
562 | }
563 |
564 | public void AddEventData(FileSystemEventNotifyData data)
565 | {
566 | lock (eventListLocker)
567 | {
568 | m_eventDataList.Add(data);
569 | }
570 | }
571 |
572 | private object eventListLocker = new object();
573 | private List m_eventDataList = new List();
574 | private Thread m_notifyThread;
575 | private AutoResetEvent eventFiredEvent = new AutoResetEvent(false);
576 | private bool bQuit = false;
577 |
578 | #endregion
579 | }
580 |
--------------------------------------------------------------------------------
/LISENCE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 AbdelhamidLarachi
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/MainForm.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace TestMonitorImage
2 | {
3 | partial class MainForm
4 | {
5 | ///
6 | /// 必需的设计器变量。
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// 清理所有正在使用的资源。
12 | ///
13 | /// 如果应释放托管资源,为 true;否则为 false。
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows 窗体设计器生成的代码
24 |
25 | ///
26 | /// 设计器支持所需的方法 - 不要
27 | /// 使用代码编辑器修改此方法的内容。
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.pictureBox1 = new System.Windows.Forms.PictureBox();
32 | ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).BeginInit();
33 | this.SuspendLayout();
34 | //
35 | // pictureBox1
36 | //
37 | this.pictureBox1.Location = new System.Drawing.Point(16, 14);
38 | this.pictureBox1.Margin = new System.Windows.Forms.Padding(4);
39 | this.pictureBox1.Name = "pictureBox1";
40 | this.pictureBox1.Size = new System.Drawing.Size(659, 436);
41 | this.pictureBox1.TabIndex = 0;
42 | this.pictureBox1.TabStop = false;
43 | //
44 | // Form1
45 | //
46 | this.AutoScaleDimensions = new System.Drawing.SizeF(8F, 15F);
47 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
48 | this.ClientSize = new System.Drawing.Size(689, 464);
49 | this.Controls.Add(this.pictureBox1);
50 | this.Margin = new System.Windows.Forms.Padding(4);
51 | this.Name = "Form1";
52 | this.Text = "Form1";
53 | this.Load += new System.EventHandler(this.Form1_Load);
54 | ((System.ComponentModel.ISupportInitialize)(this.pictureBox1)).EndInit();
55 | this.ResumeLayout(false);
56 |
57 | }
58 |
59 | #endregion
60 |
61 | private System.Windows.Forms.PictureBox pictureBox1;
62 | }
63 | }
64 |
65 |
--------------------------------------------------------------------------------
/MainForm.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Data;
5 | using System.Drawing;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 | using System.Windows.Forms;
11 |
12 | namespace TestMonitorImage
13 | {
14 | public partial class MainForm : Form
15 | {
16 | public static bool bUsePictureboxLoad = false;
17 | public MainForm()
18 | {
19 | InitializeComponent();
20 | }
21 |
22 | private void Form1_Load(object sender, EventArgs e)
23 | {
24 | var monitorPath = System.AppDomain.CurrentDomain.BaseDirectory;
25 | _fileWatcherEx = new FileSystemWatcherEx(monitorPath, "*.bmp", true, "", OnFileChanged, OnFileChanged, OnFileChanged);
26 | _fileWatcherEx.Start();
27 | }
28 |
29 | public void OnFileChanged(object sender, System.IO.FileSystemEventArgs e)
30 | {
31 | if (e.ChangeType == System.IO.WatcherChangeTypes.Created
32 | || e.ChangeType == System.IO.WatcherChangeTypes.Changed)
33 | {
34 | this.Invoke(new MethodInvoker(delegate()
35 | {
36 | try
37 | {
38 | if (bUsePictureboxLoad)
39 | {
40 | this.pictureBox1.Load(e.FullPath);
41 | }
42 | else
43 | {
44 | using (var imageStream = new FileStream(e.FullPath, FileMode.Open))
45 | {
46 | this.pictureBox1.Image = (Bitmap)Image.FromStream(imageStream);
47 | }
48 | }
49 | }
50 | catch (Exception ex)
51 | {
52 | System.Diagnostics.Debug.WriteLine(string.Format("!!!! {0}", ex.ToString()));
53 | }
54 | }));
55 | }
56 | }
57 |
58 | private FileSystemWatcherEx _fileWatcherEx;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/MainForm.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using System.Windows.Forms;
6 |
7 | namespace TestMonitorImage
8 | {
9 | static class Program
10 | {
11 | [STAThread]
12 | static void Main(string[] args)
13 | {
14 | Application.EnableVisualStyles();
15 | Application.SetCompatibleTextRenderingDefault(false);
16 |
17 | if (args.Length > 0 && args[0] == "-p")
18 | {
19 | MainForm.bUsePictureboxLoad = true;
20 | }
21 |
22 | Application.Run(new MainForm());
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // 有关程序集的常规信息通过以下
6 | // 特性集控制。更改这些特性值可修改
7 | // 与程序集关联的信息。
8 | [assembly: AssemblyTitle("TestMonitorImage")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("TestMonitorImage")]
13 | [assembly: AssemblyCopyright("Copyright © 2021")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // 将 ComVisible 设置为 false 使此程序集中的类型
18 | // 对 COM 组件不可见。 如果需要从 COM 访问此程序集中的类型,
19 | // 则将该类型上的 ComVisible 特性设置为 true。
20 | [assembly: ComVisible(false)]
21 |
22 | // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID
23 | [assembly: Guid("32fb130f-f531-4eac-976c-d024dd394266")]
24 |
25 | // 程序集的版本信息由下面四个值组成:
26 | //
27 | // 主版本
28 | // 次版本
29 | // 生成号
30 | // 修订号
31 | //
32 | // 可以指定所有这些值,也可以使用“生成号”和“修订号”的默认值,
33 | // 方法是按如下所示使用“*”:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // 此代码由工具生成。
4 | // 运行时版本:4.0.30319.42000
5 | //
6 | // 对此文件的更改可能会导致不正确的行为,并且如果
7 | // 重新生成代码,这些更改将会丢失。
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace demo.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// 一个强类型的资源类,用于查找本地化的字符串等。
17 | ///
18 | // 此类是由 StronglyTypedResourceBuilder
19 | // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
20 | // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
21 | // (以 /str 作为命令选项),或重新生成 VS 项目。
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// 返回此类使用的缓存的 ResourceManager 实例。
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("demo.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// 使用此强类型资源类,为所有资源查找
51 | /// 重写当前线程的 CurrentUICulture 属性。
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // 此代码由工具生成。
4 | // 运行时版本:4.0.30319.42000
5 | //
6 | // 对此文件的更改可能会导致不正确的行为,并且如果
7 | // 重新生成代码,这些更改将会丢失。
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace demo.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "12.0.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | FileSystemWatcherEx
2 | ==================
3 |
4 | A simple enhanced class based on .net official [FileSystemWatcher](https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher).
5 |
6 | This class attempts to do something better
7 |
8 | - Try fire same event only one time, [FileSystemWatcher](https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) may fire multiple times.
9 | - check whether file is ready for read write. [FileSystemWatcher](https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) fire event immediately.
10 | - enhanced filter, support monitoring multiple extensions. As far as I know, [FileSystemWatcher](https://docs.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) does not support.
11 |
12 |
13 |
14 | ## Limitation
15 |
16 | **DO NOT** keep the file open in your callback, open it and close it as soon as possible. then you should be safe.
17 |
18 | I demonstrate a bad callback implementation in demo project. run it with `-p` option to see it.
19 |
20 |
21 |
22 | ## Example usage
23 |
24 | ```c#
25 | public void OnFileChanged(object sender, System.IO.FileSystemEventArgs e)
26 | {
27 | if (e.ChangeType == System.IO.WatcherChangeTypes.Created
28 | || e.ChangeType == System.IO.WatcherChangeTypes.Changed)
29 | {
30 | // do something.
31 | }
32 | }
33 |
34 | // monitor .bmp and .jpg and .png files and .docx files begin with a
35 | var filters = "*.bmp|*.jpg|*.png|a*.docx";
36 |
37 | var monitorPath = System.AppDomain.CurrentDomain.BaseDirectory;
38 | var fileWatcherEx = new FileSystemWatcherEx(monitorPath
39 | , filters
40 | , true
41 | , ""
42 | , OnFileChanged
43 | , OnFileChanged
44 | , OnFileChanged
45 | );
46 |
47 | fileWatcherEx.Start();
48 |
49 | // when finish monitoring
50 |
51 | fileWatcherEx.Stop();
52 | ```
53 |
54 |
55 |
56 | ## Demo Project
57 |
58 | this demo project will monitor program directory recursively, and display changed .bmp file.
59 |
60 | 
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/demo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {DB263916-B289-4414-BAFC-711B7ED1E66F}
8 | WinExe
9 | Properties
10 | demo
11 | demo
12 | v4.5
13 | 512
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | bin\Debug\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | Form
50 |
51 |
52 | MainForm.cs
53 |
54 |
55 |
56 |
57 | MainForm.cs
58 |
59 |
60 | ResXFileCodeGenerator
61 | Resources.Designer.cs
62 | Designer
63 |
64 |
65 | True
66 | Resources.resx
67 | True
68 |
69 |
70 | SettingsSingleFileGenerator
71 | Settings.Designer.cs
72 |
73 |
74 | True
75 | Settings.settings
76 | True
77 |
78 |
79 |
80 |
81 |
82 |
83 |
90 |
--------------------------------------------------------------------------------
/result.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BianChengNan/FileSystemWatcherEx/c6d2bde7edbd6edebbd4afb0f9de732ade529ef5/result.gif
--------------------------------------------------------------------------------
/testImages/copy.ps1:
--------------------------------------------------------------------------------
1 | $curPath = $PSScriptRoot
2 | Set-Location $curPath
3 |
4 | $allImages = Get-ChildItem -Path $curPath -Filter "*.bmp"
5 |
6 | for ($idx = 0; $idx -le 1000; ++$idx)
7 | {
8 | foreach ($item in $allImages)
9 | {
10 | Copy-Item $item.FullName (Join-Path $curPath "..\\monitorFolder\\")
11 | Start-Sleep(1)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/testImages/image1.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BianChengNan/FileSystemWatcherEx/c6d2bde7edbd6edebbd4afb0f9de732ade529ef5/testImages/image1.bmp
--------------------------------------------------------------------------------
/testImages/image2.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BianChengNan/FileSystemWatcherEx/c6d2bde7edbd6edebbd4afb0f9de732ade529ef5/testImages/image2.bmp
--------------------------------------------------------------------------------
/testImages/image3.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BianChengNan/FileSystemWatcherEx/c6d2bde7edbd6edebbd4afb0f9de732ade529ef5/testImages/image3.bmp
--------------------------------------------------------------------------------
/testImages/image4.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BianChengNan/FileSystemWatcherEx/c6d2bde7edbd6edebbd4afb0f9de732ade529ef5/testImages/image4.bmp
--------------------------------------------------------------------------------