При разработке приложений 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. Здесь можно выделить два варианта работы:
- Мониторинг изменений в директории без вывода информации об изменениях, т.е. простая констатация факта — было изменение, а что именно было изменено не определяется. Для этого способа используется тройка функций: FindFirstChangeNotification, FindNextChangeNotification, FindCloseChangeNotification.
- Мониторинг изменений в директории с выводом информации по измененным элементам. Для этого способа используется пара функций: CreateFile и ReadDirectoryChangesW.
Использование функций 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: boolean — True — указывает на то, что также в результат мониторинга будут попадать изменения в поддиректориях.
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 мы поговорим в следующий раз.
Пример полезный. И хорошо, что обращено внимание на то, что использовать таймер — это крайне не рационально. Единственное замечание: гораздо проще узнать, изменился файл, или нет — сначала по дате изменения (редко кто будет подделывать этот атрибут файла), затем по размеру, и только уже после этого — по контрольной сумме.
P.S.: Слова «следение» в русском языке нет. Опечатка?
sw, спасибо за отзыв :) Да в тексте опечатка. Исправлю.
ter, скоро будет и вторая часть, надо только собрать всю информацию в кучу и примерчик накрапать.
с интересом прочитаю следующую часть (: сам недавно задумывался об отлове изменений в директориях.
Полезная статейка.
С параметрами вызова Win32Check у вас беда приключилась после публикации статьи на сайте.
Логичнее использовать какую-нибудь api-шную ф-цию, типа ReadDirectoryChangesW и таймер не нужен. Самое интересное, что примеров по работе с этой api на delphi в инете полно.
CoolVlad, и где тут в потоке таймер используется?
И, поэтому, более рациональным способом мониторинга изменений в файлах и директориях является использование функций Windows. Здесь можно выделить два варианта работы:
Мужики, выручайте. Делфа пишет что не определенно OnChange
([DCC Error] Unit1.pas(42): E2003 Undeclared identifier: ‘OnChange’)
в строке
FChangeMonitor.OnChange:=OnChange;
Что я делаю не так?
Макс, если именно такая ошибка, то видимо не определен метод OnChange, который должен быть, например, таким:
procedure MyForm.OnChange(Sender: TObject);
begin
ShowMessage('Это обработчик OnChange')
end;
OnChange должна быть описана в каком-либо классе. Вроде бы больше ничего нельзя предположить по такому описанию ошибки…
Вот оно есть описано, и там же делфи материться на TForm1.OnChange
procedure TForm1.OnChange(Sender: TObject);
const
cLogStr = ‘%s — Изменения в директории’;
begin
{выводим сообщение в лог}
mmLog.Lines.Add(Format(cLogStr,[DateTimeToStr(Now)]))
end;
Этим разобрался. А вот 2я статья не поддается никак. Может можно pas файл выложить?
Народ , помогите:
у меня ругается на FChangeMonitor.Start; нету вообще такого свойства.
также ругается на строчку Win32Check(ChangeHandle <> INVALID_HANDLE_VALUE);
видимо при посте статьи символы неправильно отобразились.
И аналогичная проблема с событием OnChange, его тоже не видно, когда пишу procedure TForm1.
я написал:
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’
Костя, OnChange так определена? не важно в какой секции private или public, но она определена или нет?
TForm2 = class
private
procedure OnChange(Sender:TObject);
public
end;
Да, уже получилось! подскажите как сделать , что бы при открытии конкретной папки моя программа выводила сообщение, что папка открыта. Пишут, что нужно использовать FindFirstFile но у меня оно выводит сообщение, если эта папка просто существует…как быть незнаю((
Уважаемый автор, помогите с данным примером, при компиляции вылетает ошибка
[Error] Unit1.pas(42): Undeclared identifier: ‘Start’
[Fatal Error] Project1.dpr(6): Could not compile used unit ‘Unit1.pas’
Ругается на FChangeMonitor.Start; в модуле проекта. Как это исправить? заранее спасибо!
Олег, вы забыли указать версию Delphi. Start — это метод TThread, которым был в свое время заменен метод Resume
у меня delphi7, нужно писать Resume вместо Start верно?
Олег, именно. Раз у вас Delphi 7, то учитывайте плз, что этот пример писался в XE-XE2, поэтому можете встретить и другие «неожиданности», типа отсутствующих методов и свойств — всё-таки Delphi 7 уже старовата стала :)
ну все вроде скомпилилось, только не работает, пишу в эдите например D:\1, создаю файлы, папки, ит.д., но реакции нет…это из за версии самой среды может быть?
Олег, вполне возможно. Я этот пример гонял только на Win7 x64.
окей, спасибо!!!
как сделать чтоб в XP работало?
Федор, да вроде бы эта тема в XP должна без проблем работать…а что конкретно не работает?
По непонятным мне причинам происходит следующее:
При копировании нового файла в отслеживаемую папку, программа фиксирует два изменения.
С чем это связано?
XE5 Win8.1×64
https://dl.dropboxusercontent.com/u/39194222/MonitorTest.7z
У вас реагирует на два события -изменение названия и — изменение размера
Подскажите
Как можно отслеживать например 10 ну или 20 каталогов
Если не хочется использовать массив объектов TChangeMonitor, то можно воспользоваться бесплатным компонентом TJvChangeNotify из JEDI, свойство Notifications которого, позволяет создать неограниченное количество объектов мониторинга с индивидуальными событиями обработки. Допускает создание коллекции объектов наблюдения не только в runtime, но и в дизайне.
TChangeMonitor имеет серьезный недостаток — утечка памяти. Допустим не возникло ни одного события изменения в папке. Вызов Treminate просто устанавливает свойство Terminated. Но что бы выйти из цикла «while no Terminated » нужно чтобы WaitForSingleObject не удерживал больше поток. А в отсутствии событий изменения в папке этого никогда не произойдет при таймауте=INFINITE. Т.е. процесс продолжит ожидание событий. Свойство потока FreeOnTerminate:=true освобождает объект только после выхода из Execute, а так, как он продолжает выполнятся, возникает утечка. Я решил эту проблему использованием WaitForMultipleObjects вместо WaitForSingleObject, которой передаю массив из двух хэндлов. Один как и прежде отвечает за события изменения в папке, а… Подробнее »