(495) 925-0049, ITShop интернет-магазин 229-0436, Учебный Центр 925-0049
  Главная страница Карта сайта Контакты
Поиск
Вход
Регистрация
Рассылки сайта
 
 
 
 
 

Программируем в Delphi

Источник: webdelphi

В предыдущем посте мы остановились на том. что разработали небольшое приложение, которое проводило мониторинг изменений в определенной директории и, в случае обнаружения какого-либо изменения, "сигналило" нам. Для организации мониторинга мы использовали поток (TThread) в котором использовалось три взаимосвязанные функции Windows:  FindFirstChangeNotification , FindNextChangeNotification  и  FindCloseChangeNotification .

Как говорилось ранее, с помощь этих функций нельзя узнать какую-либо специфическую информацию об изменениях. Так, например, при срабатывании события мы не могли узнать изменилось ли имя файла или был добавлен новый файл. Или, если произошла смена имени файла, то мы не можем узнать какое имя было до смены и какое стало после. Все эти нюансы могут натолкнуть неподготовленного разработчика на мысль, что использование приведенных выше функций ограничено - задача мониторинга изменений в директории обычно преследует не абстрактную цель - узнать что что-то поменялось (хотя, иногда и такой информации бывает достаточно), а получить конкретный ответ на вопрос - что изменилось и как (сменилось имя, размер, права доступа и т.д.? Прежде, чем мы перейдем к работе с такой специфической информацией об изменениях, мы немного доработаем наш предыдущий пример и посмотрим как с помощью уже известных нам трех функций можно настроить мониторинг так, чтобы получать максимально конкретизированную (на сколько это возможно) информацию по изменениям.

Итак, сначала вспомним как выглядел 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,INFINITE) of
        WAIT_FAILED: Terminate;                //Ошибка, завершаем поток
        WAIT_OBJECT_0: Synchronize(DoChange);
      end;
      FindNextChangeNotification(ChangeHandle);
    end;
  finally
    FindCloseChangeNotification(ChangeHandle);
  end;
end;

Здесь мы получили всего один хэндл события с помощью которого отслеживали сразу несколько возможных изменений в директории - изменение имени файла, изменение имени директории и изменение размера. Так как хэндл был всего один, то нам вполне достаточно было использовать функцию  WaitForSingleObject  для ожидания события. Но нам ведь никто не запрещает получить не один, а, например, три хэндла событий каждое из которых будет срабатывать на конкретно заданное изменение? Давайте сделаем это, а заодно и разберемся с работой ещё одной функции для обработки событий. Итак, пишем новый Execute потока:

procedure TChangeMonitor.Execute;
var ChangeHandle: THandle;
    WaitHandles: array[0..3] of THandle;{массив хэндлов}
begin
  {получаем хэндл события на изменение имени файла}
  WaitHandles[0]:=FindFirstChangeNotification(PChar(FDirectory),
                                            FScanSubDirs,
                                            FILE_NOTIFY_CHANGE_FILE_NAME
                                            );
 
  {Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
  Win32Check(WaitHandles[0] <> INVALID_HANDLE_VALUE);
 
  {получаем хэндл события на изменение имени поддиректории}
  WaitHandles[1]:=FindFirstChangeNotification(PChar(FDirectory),
                                            FScanSubDirs,
                                            FILE_NOTIFY_CHANGE_DIR_NAME
                                            );
 
  {Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
  Win32Check(WaitHandles[1] <> INVALID_HANDLE_VALUE);
 
  {получаем хэндл события на изменение размера файла}
  WaitHandles[2]:=FindFirstChangeNotification(PChar(FDirectory),
                                            FScanSubDirs,
                                            FILE_NOTIFY_CHANGE_SIZE
                                            );
 
  {Если не удалось получить хэндл - выводим ошибку и прерываем выполнение}
  Win32Check(WaitHandles[2] <> INVALID_HANDLE_VALUE);
 
 try
    {выполняем цикл пока}
    while not Terminated do
    begin
      case WaitForMultipleObjects(3, @WaitHandles,false,INFINITE) of
        WAIT_FAILED      : Terminate;                //Ошибка, завершаем поток
        WAIT_OBJECT_0    : begin
                             FLastChange:='Изменилось название файла';
                             Synchronize(DoChange);
                             if not FindNextChangeNotification(WaitHandles[0]) then
                                break;
                           end;
        WAIT_OBJECT_0 + 1: begin
                             FLastChange:='Изменилось название поддиректории';
                             Synchronize(DoChange);
                             if not FindNextChangeNotification(WaitHandles[1]) then
                               break;
                           end;
        WAIT_OBJECT_0 + 2: begin
                             FLastChange:='Изменился размер файла';
                             Synchronize(DoChange);
                             if not FindNextChangeNotification(WaitHandles[2]) then
                               break;
                           end;
      end;
    end;
  finally
    CloseHandle(WaitHandles[0]);
    CloseHandle(WaitHandles[1]);
    CloseHandle(WaitHandles[2]);
  end;
end;

В приведенном выше Execute мы определили массив на 3 элемента, где каждый элемент - это хэндл события рассчитанного на определенный вид изменений в директории. Таким образом мы получили возможность пусть и не полностью, но конкретизировать изменения. Так как мы планируем ожидать несколько событий, то вместо метода WaitForSingleObject  мы воспользовались методом  WaitForMultipleObjects , который в MSDN имеет следующее описание:

WaitForMultipleObjects

WaitForMultipleObjects(nCount:DWORD; const lpHandles:PWOHandleArray;bWaitAll:boolean; dwMilliseconds:DWORD): DWORD;
 

nCount: DWORD  - количество хэндлов событий. Этот параметр не может быть равен нулю. Максимальное количество ожидаемых событий ограничивается значением константы  MAXIMUM_WAIT_OBJECTS  =  64 ;
lpHandles: PWOHandleArray  - указатель на массив дескрипторов. Массив не должен содержать повторяющихся  THandle .
bWaitAll: boolean  - если значение этого параметра равно  True , то функция вернет значение только в том случае, если будет получен сигнал от всех событий хэндлы которых определены в массиве. В случае, если значение флага равно  False , то функция будет возвращать значения при срабатывание любого из событий, при этом возвращаемое значение будет указывать на объект, который изменил состояние.
dwMilliseconds:DWORD  - время в мс ожидания события. Если значение этого параметра равно  INFINITE , то функция будет возвращать значение только при срабатывании одного из событий.

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

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

Использование функций CreateFile и ReadDirectoryChangesW

Прежде, чем перейдем к примеру на Delphi, посмотрим на описание этих двух функций.
Функция CreateFile создает или открывает файл или другое устройство ввода/вывода (файл, файловый поток, каталог и т.д.

CreateFile

function CreateFile(lpFileName: PChar; dwDesiredAccess:cardinal; dwShareMode:cardinal; lpSecurityAttributes:PSecurityAttributes;      dwCreationDisposition:cardinal; dwFlagsAndAttributes:cardinal; hTemplateFile: THandle): THandle;
 

lpFileName: PChar  - имя файла или устройства ввода/вывода, которое будет создано или открыто.
dwDesiredAccess: cardinal  - флаг, определяющий режим доступа к файлу или устройству (чтение, запись, чтение и запись). Наиболее часто используемые значения GENERIC_READGENERIC_WRITE, или сразу оба (GENERIC_READ or GENERIC_WRITE).
dwShareMode:cardinal  - один или несколько флагов, определяющих режим совместного доступа к файлу или устройству. Для установки значений могут использоваться следующие константы:
FILE_SHARE_DELETE (0x00000004)  - разрешает другим процессам получать доступ к файлу или устройству в том числе и выполнять операции удаления.
FILE_SHARE_READ (0x00000001)  - разрешает другим процессам выполнять операции чтения.
FILE_SHARE_WRITE (0x00000002)  - разрешает другим процессам выполнять операции записи.
lpSecurityAttributes:PSecurityAttributes  - указатель на структуру  SECURITY_ATTRIBUTES , содержащую дополнительные параметры безопасности работы с файлом или устройством. Этому параметру можно присваивать значение nil.
dwCreationDisposition:cardinal  - флаг, который определяет какие действия следует применить к файлу или устройству. Для всех устройств, кроме файлов, этот параметр обычно устанавливается в значение  OPEN_EXISTING . Также может принимать следующие значения:
CREATE_ALWAYS (2)  - всегда создавать новый файл. Если файл уже существует и доступен для записи, то код последней ошибки устанавливается в значение  ERROR_ALREADY_EXISTS (183) . Если файл не существует и указан правильный к нему путь, то создается новый файл. а код последней ошибки устанавливается в 0.
CREATE_NEW (1)  - создать новый файл. Если файл с таким именем уже существует, то вернется код ошибки ERROR_FILE_EXISTS (80) .
OPEN_ALWAYS (4)  - всегда открывать файл. Если файл существует, то функция завершится успешно, а код последней ошибки будет установлен в значение  ERROR_ALREADY_EXISTS (183) . Если же файл не существует и указан верный путь, то создастся новый файл, а значение код последней ошибки будет установлено в 0.
OPEN_EXISTING (3)  - открыть файл или устройство, только если файл или устройство существуют. Если объект не найден, то вернется код ошибки  ERROR_FILE_NOT_FOUND (2) .
TRUNCATE_EXISTING (5)  - открыть файл и урезать его размер до 0 байт. Если файл не существует, то вернется код ошибки ERROR_FILE_NOT_FOUND (2).
dwFlagsAndAttributes:cardinal  - сочетание флагов и атрибутов файла или устройства. Значения обычно начинаются с FILE_ATTRIBUTE_* или FILE_FLAG_*. Наиболее часто используется значение  FILE_ATTRIBUTE_NORMAL . При использовании функции CreateFile совместно с ReadDirectoryChangesW необходимо использовать также флаг FILE_FLAG_BACKUP_SEMANTICS .
hTemplateFile: THandle  - дескриптор временного файла с режимом доступа  GENERIC_READ . Значение этого параметра может быть равно nil.

ReadDirectoryChangesW

function ReadDirectoryChangesW(hDirectory:THandle; lpBuffer:pointer; nBufferLength:cardinal; bWatchSubtree:boolean; dwNotifyFilter:cardinal; lpBytesReturned:cardinal; lpOverlapped:POVERLAPPED; lpCompletionRoutine:POVERLAPPED_COMPLETION_ROUTINE): boolean;
 

hDirectory:THandle  - дескриптор директории за которой будет проводится наблюдение. Это значение мы получим, выполнив  CreateFile .
lpBuffer:pointer  - указатель на буфер в который буду записываться обнаруженные изменения. Структура записей в буфере соответствует структуре  FILE_NOTIFY_INFORMATION  (см. описание ниже). Буфер может записываться как синхронно, так и асинхронно в зависимости от заданных параметров.
nBufferLength:cardinal  - размер буфера  lpBuffer  в байтах.
bWatchSubtree:boolean  -  True  указывает на то, что в результаты мониторинга будут попадать также изменения в подкаталогах.
dwNotifyFilter:cardinal  - фильтр событий. Может состоять из одного или нескольких флагов:
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_LAST_ACCESS (0x00000020)  - изменение времени последнего доступа к файлу или каталогу.
FILE_NOTIFY_CHANGE_CREATION (0x00000040)  - изменение времени создания файла или каталога.
FILE_NOTIFY_CHANGE_SECURITY (0x00000100)  - изменение параметров безопасности файла или каталога.
lpBytesReturned:cardinal  - в случае синхронного вызова функции этот параметр будет содержать количество байт информации, записанной в буфер. Для асинхронных вызовов значение этого параметра остается неопределенным.
lpOverlapped:POVERLAPPED  - указатель на структуру  OVERLAPPED , которая поставляет данные, которые будут использоваться во время асинхронной операции. Это значение может быть равным  nil .
lpCompletionRoutine:POVERLAPPED_COMPLETION_ROUTINE  - указатель на callback-функцию. Этот параметр может устанавливаться в значение  nil .

Описания функций есть. Теперь напишем с помощью этих двух функций новую заготовку для Execute нашего потока для мониторинга изменений в директориях и файлах Windows:

procedure TScanLocalThread.Execute;
var
  WaitHandles: array [0 .. 1] of THandle;
begin
  {хэндл события завершения потока}
  FTermEvent := CreateEvent(nil, True, False, nil);
  {вызывать ReadDirectory... будем асинхронно, используя структуру Overlapped}
  FillChar(FOverlapped, SizeOf(TOverlapped), 0);
  {определяем хэндл события на получение данных по изменениям}
  FOverlapped.hEvent := CreateEvent(nil, True, False, nil);
  {получаем хэндл директории}
  HandleChange := CreateFile(PWideChar(FScannedDir), GENERIC_READ,
    FILE_SHARE_READ or FILE_SHARE_DELETE or FILE_SHARE_WRITE,
    nil,
    OPEN_EXISTING,
    FILE_FLAG_BACKUP_SEMANTICS or
    FILE_FLAG_OVERLAPPED, 0);
  {проверяем, что хэндл успешно получен}
  Win32Check(HandleChange <> INVALID_HANDLE_VALUE);
  {заносим хэндлы в массив для использования в WaitForMultipleObjects}
  WaitHandles[0] := FTermEvent;
  WaitHandles[1] := FOverlapped.hEvent;
  {выполняем мониторинг}  
  try
    while True do
    begin
      ReadDirectoryChangesW(HandleChange, @Buf, Sizeof(Buf), FScanSubDirs,
        FILE_NOTIFY_CHANGE_FILE_NAME or
        FILE_NOTIFY_CHANGE_DIR_NAME or
        FILE_NOTIFY_CHANGE_SIZE or
        FILE_NOTIFY_CHANGE_LAST_WRITE or
        FILE_NOTIFY_CHANGE_CREATION,
        nil, @FOverlapped, nil);
      {ожидаем событие}
      if WaitForMultipleObjects(2, @WaitHandles, False, INFINITE)
        = WAIT_OBJECT_0 then
        Break;
      {смотрим, какие были изменения}
      FindChanges;
    end;
  finally
    {закрываем все хэндлы} 
    CloseHandle(HandleChange);
    CloseHandle(FTermEvent);
    CloseHandle(FOverlapped.hEvent);
    FTermEvent := 0;
  end;
end;

В приведенном выше коде присутствуют переменные, которые мы ранее не использовали в работе, поэтому приведу дополнительно их описание:

TScanLocalThread = class(TThread)
  private
[...]
    Buf: array [0..65535]of byte;
    FOverlapped: TOverlapped;
[...]

Теперь при изменении в директории будет срабатывать событие по которому мы будем получать в буфер информацию по проведенным изменениям. Но получить информацию - это одно, а разобрать е и предоставить в удобном для нас виде - другое. Поэтому я специально вынес весь разбор содержимого буфера в отдельный метод FindChanges. Прежде, чем рассмотрим этот метод немного отвлечемся от Delphi и снова вернемся к MSDN, а именно - посмотрим, что представляет из себя структура FILE_NOTIFY_INFORMATION.

FILE_NOTIFY_INFORMATION

Так как в Delphi описания этой структуры я не обнаружил, но приведу е описание прямо из MSDN:

typedef struct _FILE_NOTIFY_INFORMATION {
  DWORD NextEntryOffset;
  DWORD Action;
  DWORD FileNameLength;
  WCHAR FileName[1];
} FILE_NOTIFY_INFORMATION, *PFILE_NOTIFY_INFORMATION;

NextEntryOffset  - сдвиг в байтах, начиная с которого начинается следующая запись. Если это значение равно нулю, то текущая запись является последней.
Action - сдержит информацию по произведенным изменениям и может принимать одно из следующих значений:
FILE_ACTION_ADDED (0x00000001)  - новый файл был добавлен в директорию или поддиректорию
FILE_ACTION_REMOVED (0x00000002)  - файл или директория были удалены
FILE_ACTION_MODIFIED (0x00000003)  - файл был изменен
FILE_ACTION_RENAMED_OLD_NAME (0x00000004)  - файл или директория были переименованы и FileName содержит старое имя файла или директории
FILE_ACTION_RENAMED_NEW_NAME (0x00000005)  - файл или директория были переименованы и FileName содержит новое имя файла или директории
FileNameLength  - длина имени файла.
FileName[1]  - указатель на строку, содержащую имя файла.
То, что структура FILE_NOTIFY_INFORMATION не определена ещё не значит, что мы её не способны определить сами. Давайте определим в модуле Monitor.pas следующий тип данных:

type
  FILE_NOTIFY_INFORMATION = record
    NextEntryOffset: DWORD;
    Action: DWORD;
    FileNameLength: DWORD;
    FileName: array [0 .. 0] of WCHAR;
  end;

Теперь остается только рассмотреть метод FindChanges:

procedure TScanLocalThread.FindChanges;
var
  fni: ^FILE_NOTIFY_INFORMATION;
  ws: WideString;
begin
 
  fni := @Buf;
  while True do
  begin
    {получаем имя файла}
    SetLength(ws, fni^.FileNameLength div SizeOf(WideChar));
    Move(fni^.FileName, ws[1], fni^.FileNameLength);
 
    case fni^.Action of
      FILE_ACTION_ADDED:  {действие на добавление файла};
      FILE_ACTION_REMOVED:{действие на удаление файла};
      FILE_ACTION_MODIFIED:{действие на изменение файла};
      FILE_ACTION_RENAMED_OLD_NAME, FILE_ACTION_RENAMED_NEW_NAME:{действие на переименование файла}
    end;
    {переходим к следующей записи или выходим из цикла}
    if fni^.NextEntryOffset > 0 then
      fni := pointer(Cardinal(fni) + fni^.NextEntryOffset)
    else
      Break;
  end;
end;

В целом состав этого метода будет зависеть от ваших целей, поэтому я не стал перегружать исходник какими-то своими переменными и дополнительными методами, но думаю, что смысл чтения изменений будет Вам понятен.
На этом рассмотрение темы "Мониторинг изменений в директориях и файлах средствами Delphi" можно было бы считать законченным, но есть несколько моментов, о которых следует сказать. В целом можно отметить, что использование этого способа контроля изменений в директориях вместо использования всякого рада таймеров, речь о которых шла в первой части, является более предпочтительным и правильным выбором. Однако иногда можно столкнуться с некоторыми моментами, когда решения проблемы не "лежит на поверхности". К примеру, может возникнуть проблема отслеживания изменений в файлах Microsoft Office. Дело в том, что Word, Excel и PowerPoint, несмотря на то, что относятся к одному пакету программ, сохраняют свои файлы разными способами. Тот же Word при открытии файла создает временный скрытый файл, куда заносятся изменения, а после нажатия кнопки "Сохранить", вначале копирует всё содержимое файла в новый временный файл и только потом переименовывает его. У Excel алгоритм сохранения и работы с файлом ещё более "замороченный". И все эти создания временных файлов, пересохранения и переименования временных файлов каждый раз будут "провоцировать" наш поток выдавать новые и новые записи по изменениям. И тут, по крайней мере пока, я ещё не нашел более-менее универсального способа "отсеивания" лишних событий на этапе их получения, кроме как воспользоаться способом представленным в самом первом примере этого поста, т.е. разбивать фильтр на несколько событий и каждое событие анализировать по отдельности. Так что, если у Вас есть более правильное решение вопроса "Как правильно отлавливать изменение в файле MS Office?", то буду благодарен, если предложите его решение в комментариях к этому посту.



 Распечатать »
 Правила публикации »
  Обсудить материал в конференции Embarcadero »
Написать редактору 
 Рекомендовать » Дата публикации: 26.08.2011 
 

Магазин программного обеспечения   WWW.ITSHOP.RU
Enterprise Connectors (1 Year term)
Delphi Professional Named User
NauDoc Enterprise 10 рабочих мест
VMware Horizon Apps Standard, v7 : 10 Pack (Named User)
TeeChart Standard VCL/FMX 2 developer license
 
Другие предложения...
 
Курсы обучения   WWW.ITSHOP.RU
 
Другие предложения...
 
Магазин сертификационных экзаменов   WWW.ITSHOP.RU
 
Другие предложения...
 
3D Принтеры | 3D Печать   WWW.ITSHOP.RU
 
Другие предложения...
 
Новости по теме
 
Рассылки Subscribe.ru
Информационные технологии: CASE, RAD, ERP, OLAP
Новости ITShop.ru - ПО, книги, документация, курсы обучения
Программирование на Microsoft Access
CASE-технологии
СУБД Oracle "с нуля"
Вопросы и ответы по MS SQL Server
Все о PHP и даже больше
 
Статьи по теме
 
Новинки каталога Download
 
Исходники
 
Документация
 
Обсуждения в форумах
Пишу программы на заказ для студентов (253)
Пишу для студентов на с, с++, паскаль в средах ms visual studio, qt, builder, borland c, delphi....
 
Разработка программ базы данных (59)
Написание прикладных компьютерных программ (базы данных) на заказ. Разработка корпоративных...
 
Выбор лучшего онлайн казино (1)
Очень важным критерием для составления рейтинга являются честные отзывы клиентов о казино. Люди...
 
Решение задач на оптимизацию с помощью MS Excel (5)
Zdravstvuyte, ya pro4itala danniy material, no ne smogla ponat kakie formuli ispolzovalis v...
 
Актуальное зеркало БК Лигаставок (3)
Актуальное зеркало букмекерской конторы Лигаставок https://superbet.guru/bk-ligastavok-mirror/...
 
 
 



    
rambler's top100 Rambler's Top100