уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.

При разработке приложений Delphi для синхронизации чего-либо, например, файлов на разных компьютерах, так или иначе приходится разрабатывать алгоритм с помощью которого можно однозначно определять какие из файлов необходимо удалить из определенной директории, какие переместить, переименовать и т.д. Подобные алгоритмы и примеры их использования на практике не являются редкостью — в Сети Вы можете найти массу самых различных вариаций Delphi-кода с помощью которого можно отследить изменения в директориях и файлах Windows.

Не так давно и мне довелось столкнуться с подобной задачей — отследить изменения в определенной директории и сформировать список заданий для синхронизации файлов с сервером. Так как до этого момента мне не доводилось разрабатывать подобные алгоритмы, то пришлось пошерстить просторы Интернета и собрать как можно больше информации на заданную тему. Ну, а результаты моих поисков я решил оформить в виде отдельной статьи в блоге. Итак, сегодняшняя тема — мониторинг изменений в директориях и файлам средствами Delphi.

Самым простым и легкодоступным даже для новичков в программировании способом слежения за изменениями в директории является работа по таймеру. Смысл работы заключается в том, что на старте работы программы создается список файлов и поддиректорий в целевой директории. Затем, в момент срабатывания таймера, создается новый список и сравнивается с предыдущим — определяется какие файлы были добавлены, какие удалены/перемещены и т.д. и по уже определенным изменениям проводятся операции синхронизации.  Как в данном случае определить, что, скажем, файл Test.txt был изменен? Например, можно рассчитывать каждый раз CRC файла и сравнивать эту сумму с предыдущим значением. Вот исходник функции с  www.delphisources.ru, для расчёта CRC файла:

function GetCheckSum(FileName: string): DWORD;
var
  F: file of DWORD;
  P: Pointer;
  Fsize: DWORD;
  Buffer: array[0..500] of DWORD;
begin
  FileMode := 0;
  AssignFile(F, FileName);
  Reset(F);
  Seek(F, FileSize(F) div 2);
  Fsize := FileSize(F) - 1 - FilePos(F);
  if Fsize > 500 then
    Fsize := 500;
  BlockRead(F, Buffer, Fsize);
  Close(F);
  P := @Buffer;
  asm
     xor eax, eax
     xor ecx, ecx
     mov edi , p
     @again:
       add eax, [edi + 4*ecx]
       inc ecx
       cmp ecx, fsize
     jl @again
     mov @result, eax
  end;
end;

Пример использования функции. Создадим новое приложение в Delphi со следующими компонентами на форме:

При открытии файла будем определять его размер и рассчитывать CRC с помощью приведенной выше функции:

uses IOUtils;
[...]
procedure TForm3.Button1Click(Sender: TObject);
begin
  if OpenDialog1.Execute then
    begin
      edFile.Text:=OpenDialog1.FileName;
      lbCRC.Caption:=IntToStr(GetCheckSum(OpenDialog1.FileName));
      lbSize.Caption:=IntToStr(TFile.OpenRead(OpenDialog1.FileName).Size);
    end;
end;

Теперь возьмем создадим текстовый файлик и запишем в него строку, скажем «Hello World!», сохраним его, запустим программу и рассчитаем CRC. Вот, что получилось в программе:

Теперь снова откроем файл и заменим заглавные буквы на прописные, т.е. строка примет вид «hello world!». Снова рассчитаем CRC:

Обратите внимание, что размер файла остался прежним, а содержимое файла изменилось. Это лишний пример того, что использование в качестве критерия изменения файла только его размера — это очень ненадежный вариант и, наверное, даже неправильный.

Что можно сказать по поводу предложенного выше варианта мониторинга изменения в директории с помощью таймера?

Достоинством этого метода можно назвать его простота. Не важно, какой таймер будет использоваться в работе — стандартный TTimer или собственноручно созданный высокоточный таймер. Повесить обработчик на срабатывание таймера сможет кто угодно. Но наряду с простотой этого варианта он также имеет и массу недостатков. И самый главный из недостатков — ненадежность.

Никто не даст Вам гарантий того, что заданный интервал срабатывания таймера будет достаточным для выполнения процедуры обработчика. Как говориться, компьютер пользователя — потёмки. Можно, конечно, задавать большой интервал времени и надеяться на то, что обработчик таймера отработает на 100%, но это всего-лишь «костыль», но никак не решение проблем надежности алгоритма.

Кроме того, каждый раз перебирать большое количество файлов — излишняя трата ресурсов. Конечно, в наш век многоядерных процессоров, ресурсы компьютера очень большие, но и разбрасываться ими по поводу и без — не стоит, тем более, если работа ведется над созданием серьезного ресурсоемкого проекта, где на счету каждый байт.

И, поэтому, более рациональным способом мониторинга изменений в файлах и директориях является использование функций Windows. Здесь можно выделить два варианта работы:

  1. Мониторинг изменений в директории без вывода информации об изменениях, т.е. простая констатация факта — было изменение, а что именно было изменено не определяется. Для этого способа используется тройка функций: FindFirstChangeNotification, FindNextChangeNotification, FindCloseChangeNotification.
  2. Мониторинг изменений в директории с выводом информации по измененным элементам. Для этого способа используется пара функций: CreateFile и ReadDirectoryChangesW.
Оба этих способа в равной степени удобны, но какой из этих способов использовать в конкретной ситуации решать только Вам. Рассмотрим примеры использования функций Windows для мониторинга изменений в директориях.

Использование функций FindFirstChangeNotification, FindNextChangeNotification, FindCloseChangeNotification

Прежде, чем приступим к изучению функций, создадим модуль-заготовку для дальнейшей работы. Следить за изменениями мы будем в потоке (TThread):

unit Monitor;
 
interface
 
uses Classes, Windows, SysUtils;
 
type
  TChangeMonitor = class(TThread)
    private
    public
    protected
      procedure Execute; override;
  end;
 
implementation
 
{ TChangeMonitor }
 
procedure TChangeMonitor.Execute;
begin
 {здесь будем проводить мониторинг}
end;
 
end.

Теперь рассмотрим назначение функций Windows.

FindFirstChangeNotification — создает дескриптор уведомления об изменениях и устанавливает начальные условия отправки уведомления. Функция возвращает дескриптор (THandle) либо INVALID_HANDLE_VALUE в случае ошибки:

FindFirstChangeNotification(lpPathName: PChar; bWatchSubtree: boolean; dwNotifyFilter:DWORD): THandle;

lpPathName: PChar — полный путь к директории за которой проводится слежение. Значение этого параметра не может содержать относительный путь или пустую строку.
bWatchSubtree: booleanTrue — указывает на то, что также в результат мониторинга будут попадать изменения в поддиректориях.
dwNotifyFilter:DWORD — набор флагов, определяющих настройки фильтра. Флаги могут быть следующими:

  • FILE_NOTIFY_CHANGE_FILE_NAME (0x00000001) — любое изменение имени файла в каталоге или подкаталоге.  Изменения включают в себя переименование, создание или удаление файла.
  • FILE_NOTIFY_CHANGE_DIR_NAME (0x00000002) — любое изменение имени директории в каталоге или подкаталоге.  Изменения включают в себя переименование, создание или удаление директории.
  • FILE_NOTIFY_CHANGE_ATTRIBUTES (0x00000004) — любое изменение атрибутов в просматриваемой директории и поддиректориях.
  • FILE_NOTIFY_CHANGE_SIZE (0x00000008) — изменение размера файла в директории или поддиректории. Изменение размера обнаруживается только когда файл записывается на диск.
  • FILE_NOTIFY_CHANGE_LAST_WRITE (0x00000010) — изменение времени последней записи в файл.
  • FILE_NOTIFY_CHANGE_SECURITY (0x00000100) — изменение параметров безопасности в каталоге или подкаталоге.

 

FindNextChangeNotification — указывает, чтобы операционная система вернула сигнал уведомления об изменении THandle в следующий раз, когда обнаруживаются изменения, согласно фильтру, установленному функцией FindFirstChangeNotification.

FindNextChangeNotification(hChangeHandle: THandle):boolean;

hChangeHandle: THandle — дескриптор, полученный с помощью функции FindFirstChangeNotification.

FindCloseChangeNotification — останавливает мониторинг изменений в директории.

FindCloseChangeNotification(hChangeHandle: THandle):boolean;

hChangeHandle: THandle — дескриптор, полученный с помощью функции FindFirstChangeNotification.

Назначение функций теперь более или менее стали нам понятны — осталось закрепить полученные знания на опыте. Итак, прежде всего наш поток должен получать два значения: путь к директории за которой мы будем следить и флаг, указывающий следует ли мониторить подкаталоги. Пишем конструктор потока:

type
  TChangeMonitor = class(TThread)
    private
      FDirectory: string;
      FScanSubDirs: boolean;
    public
      constructor Create(ASuspended: boolean; ADirectory:string; AScanSubDirs: boolean);
    protected
      procedure Execute; override;
  end;
 
implementation
 
{ TChangeMonitor }
 
constructor TChangeMonitor.Create(ASuspended: boolean; ADirectory: string;
  AScanSubDirs: boolean);
begin
  inherited Create(ASuspended);
  FDirectory:=ADirectory;
  FScanSubDirs:=AScanSubDirs;
  FreeOnTerminate:=true;
end;

Теперь создадим следующий Execute:

procedure TChangeMonitor.Execute;
var ChangeHandle: THandle;
begin
  {получаем хэндл события}
  ChangeHandle:=FindFirstChangeNotification(PChar(FDirectory),
                                            FScanSubDirs,
                                            FILE_NOTIFY_CHANGE_FILE_NAME+ 
                                            FILE_NOTIFY_CHANGE_DIR_NAME+
                                            FILE_NOTIFY_CHANGE_SIZE
                                            );
 {Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
 Win32Check(ChangeHandle <> INVALID_HANDLE_VALUE);
 try
    {выполняем цикл пока}
    while not Terminated do
    begin
      case WaitForSingleObject(ChangeHandle,1000) of
        WAIT_FAILED: Terminate; {Ошибка, завершаем поток}
        WAIT_OBJECT_0: {Сообщаем об изменениях};
      end;
      FindNextChangeNotification(ChangeHandle);
    end;
  finally
    FindCloseChangeNotification(ChangeHandle);
  end;
end;

В приведенном выше обработчике Execute мы проводим мониторинг изменения имени файла/директории или размера файла. При этом мы ожидаем любого из заданных в фильтре событий и выводим сообщение. Кстати, создадим событие для вывода сообщения об изменениях, например, такое:

type
  TChangeMonitor = class(TThread)
    private
      [...]
      FOnChange   : TNotifyEvent;
      procedure DoChange;
    public
      [...]
      property OnChange   : TNotifyEvent read FOnChange write FOnChange;
    protected
      procedure Execute; override;
  end;
 
procedure TChangeMonitor.DoChange;
begin
  if Assigned(FOnChange) then
    OnChange(Self)
end;
 
procedure TChangeMonitor.Execute;
var ChangeHandle: THandle;
begin
[...]
     case WaitForSingleObject(ChangeHandle,INFINITE) of
        WAIT_FAILED: Terminate; //Ошибка, завершаем поток
        WAIT_OBJECT_0: Synchronize(DoChange);
      end;
[...]
end;

Теперь всё готово для проверки работоспособности нашего потока. Создадим новый проект Delphi и на главную форму положим следующие компоненты:

В uses подключим модуль с нашим потоком и объявим следующую переменную:

uses
  [...], Monitor;
 
type
  TForm3 = class(TForm)
    [...]
  private
    FChangeMonitor:TChangeMonitor;
  public
    { Public declarations }
  end;

Теперь напишем необходимые обработчики для событий:

procedure TForm3.Button1Click(Sender: TObject);
begin
  mmLog.Lines.Clear;
  {создаем поток}
  FChangeMonitor:=TChangeMonitor.Create(True,edPath.Text,CheckBox1.Checked);
  {определяем обработчик события}
  FChangeMonitor.OnChange:=OnChange;
  {запускаем поток на выполнение}
  FChangeMonitor.Start;
  Button1.Enabled:=False;
  Button2.Enabled:=True;
end;
 
procedure TForm3.Button2Click(Sender: TObject);
begin
  {останавливаем поток}
  FChangeMonitor.Terminate;
  Button1.Enabled:=True;
  Button2.Enabled:=False;
end;
 
procedure TForm3.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if Assigned(FChangeMonitor) then
    FChangeMonitor.Terminate;
end;
 
procedure TForm3.OnChange(Sender: TObject);
const
  cLogStr = '%s - Изменения в директории';
begin
  {выводим сообщение в лог}
  mmLog.Lines.Add(Format(cLogStr,[DateTimeToStr(Now)]))
end;

Запускаем программу, выбираем директорию за которой необходимо следить и нажимаем кнопку «Следить». Теперь попробуем скопировать/удалить какой-нибудь файл и увидим в Memo соответствующее сообщение.
Приведенный выше пример является, наверное, самым простым, когда мы отслеживаем всего лишь одно событие по которому просто констатируем факт — произошли изменения в директории. А какие изменения — об этом мы ничего не сообщаем. Мы даже не можем в этом случае сказать, что именно произошло. Можно слегка подкорректировать приведенный выше обработчик и, используя приведенные выше функции Windows определить различные события на каждый вариант изменений, используя при этом вместо функции WaitForSingleObject функцию WaitForMultipleObjects. Но об этом, а также об использовании методов CreateFile и ReadDirectoryChangesW мы поговорим в следующий раз.

4.7 6 голоса
Рейтинг статьи
уважаемые посетители блога, если Вам понравилась, то, пожалуйста, помогите автору с лечением. Подробности тут.
Подписаться
Уведомить о
28 Комментарий
Межтекстовые Отзывы
Посмотреть все комментарии
sw
sw
18/08/2011 13:42

Пример полезный. И хорошо, что обращено внимание на то, что использовать таймер — это крайне не рационально. Единственное замечание: гораздо проще узнать, изменился файл, или нет — сначала по дате изменения (редко кто будет подделывать этот атрибут файла), затем по размеру, и только уже после этого — по контрольной сумме.
P.S.: Слова «следение» в русском языке нет. Опечатка?

ter
ter
18/08/2011 14:12

с интересом прочитаю следующую часть (: сам недавно задумывался об отлове изменений в директориях.

Страдалецъ
Страдалецъ
04/09/2011 16:33

Полезная статейка.
С параметрами вызова Win32Check у вас беда приключилась после публикации статьи на сайте.

CoolVlad
CoolVlad
04/12/2011 11:59

Логичнее использовать какую-нибудь api-шную ф-цию, типа ReadDirectoryChangesW и таймер не нужен. Самое интересное, что примеров по работе с этой api на delphi в инете полно.

Макс
Макс
20/07/2012 01:04

Мужики, выручайте. Делфа пишет что не определенно OnChange

([DCC Error] Unit1.pas(42): E2003 Undeclared identifier: ‘OnChange’)
в строке
FChangeMonitor.OnChange:=OnChange;

Что я делаю не так?

Макс
Макс
20/07/2012 17:55

Вот оно есть описано, и там же делфи материться на TForm1.OnChange

procedure TForm1.OnChange(Sender: TObject);
const
cLogStr = ‘%s — Изменения в директории’;
begin
{выводим сообщение в лог}
mmLog.Lines.Add(Format(cLogStr,[DateTimeToStr(Now)]))
end;

Макс
Макс
20/07/2012 21:08

Этим разобрался. А вот 2я статья не поддается никак. Может можно pas файл выложить?

Костя
Костя
18/10/2012 12:17

Народ , помогите:
у меня ругается на FChangeMonitor.Start; нету вообще такого свойства.
также ругается на строчку Win32Check(ChangeHandle <> INVALID_HANDLE_VALUE);
видимо при посте статьи символы неправильно отобразились.

И аналогичная проблема с событием OnChange, его тоже не видно, когда пишу procedure TForm1.

Костя
Костя
18/10/2012 12:20

я написал:
procedure TForm2.OnChange (Sender: TObject);
const
cLogStr = ‘%s — Изменения в директории’;
begin
{выводим сообщение в лог}
memo2.Lines.Add(Format(cLogStr,[DateTimeToStr(Now)]))
end;

но компилятор ругается на строчку procedure TForm2.OnChange (Sender: TObject);
пишет: [Error] Unit2.pas(33): Undeclared identifier: ‘OnChange’

Костя
Костя
18/10/2012 14:25

Да, уже получилось! подскажите как сделать , что бы при открытии конкретной папки моя программа выводила сообщение, что папка открыта. Пишут, что нужно использовать FindFirstFile но у меня оно выводит сообщение, если эта папка просто существует…как быть незнаю((

Олег
Олег
04/11/2012 21:46

Уважаемый автор, помогите с данным примером, при компиляции вылетает ошибка
[Error] Unit1.pas(42): Undeclared identifier: ‘Start’
[Fatal Error] Project1.dpr(6): Could not compile used unit ‘Unit1.pas’
Ругается на FChangeMonitor.Start; в модуле проекта. Как это исправить? заранее спасибо!

Олег
Олег
04/11/2012 22:30

у меня delphi7, нужно писать Resume вместо Start верно?

Олег
Олег
04/11/2012 22:43

ну все вроде скомпилилось, только не работает, пишу в эдите например D:\1, создаю файлы, папки, ит.д., но реакции нет…это из за версии самой среды может быть?

Олег
Олег
04/11/2012 23:55

окей, спасибо!!!

Федор
Федор
04/08/2013 20:22

как сделать чтоб в XP работало?

Александр
Александр
29/04/2014 15:55

По непонятным мне причинам происходит следующее:
При копировании нового файла в отслеживаемую папку, программа фиксирует два изменения.
С чем это связано?
XE5 Win8.1×64
https://dl.dropboxusercontent.com/u/39194222/MonitorTest.7z

serg
serg
28/05/2014 16:09
Ответить на  Александр

У вас реагирует на два события -изменение названия и — изменение размера

serg
serg
28/05/2014 16:17

Подскажите
Как можно отслеживать например 10 ну или 20 каталогов

ВалерийК
ВалерийК
01/07/2014 14:42
Ответить на  serg

Если не хочется использовать массив объектов TChangeMonitor, то можно воспользоваться бесплатным компонентом TJvChangeNotify из JEDI, свойство Notifications которого, позволяет создать неограниченное количество объектов мониторинга с индивидуальными событиями обработки. Допускает создание коллекции объектов наблюдения не только в runtime, но и в дизайне.

ВалерийК
ВалерийК
01/07/2014 14:25

TChangeMonitor имеет серьезный недостаток — утечка памяти. Допустим не возникло ни одного события изменения в папке. Вызов Treminate просто устанавливает свойство Terminated. Но что бы выйти из цикла «while no Terminated » нужно чтобы WaitForSingleObject не удерживал больше поток. А в отсутствии событий изменения в папке этого никогда не произойдет при таймауте=INFINITE. Т.е. процесс продолжит ожидание событий. Свойство потока FreeOnTerminate:=true освобождает объект только после выхода из Execute, а так, как он продолжает выполнятся, возникает утечка. Я решил эту проблему использованием WaitForMultipleObjects вместо WaitForSingleObject, которой передаю массив из двух хэндлов. Один как и прежде отвечает за события изменения в папке, а… Подробнее »