Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Два простых способа уведомления

Оформил: DeeCo

Автор: Алексей Еремеев

В своей работе мне частенько приходиться делать разного рода клиент-серверные системы.
И совсем не обязательно на уровне глобальных сетей. Речь пойдет о внутренних подсистемах.
Например, имеем компонент, который эмулирует секундомер. Запустили его с параметром типа "а напомни мне, что будет полночь" и забыли. Ну и конечно событие есть типа OnAlert. И обработчик его честно будет вызван по достижении нужной нам полуночи. Но обработчик один, а захотели узнать об этом событии сразу десять разных объектов. Не вешать же десять будильников?
Конечно, проще в одном обработчике перебрать методы уведомления этих десяти объектов да и дело с концом. Но можно поступить хитрее - заставить объект-будильник самому напоминать всем кто попросит его об этом. Вот о способах такого уведомления и пойдет речь.

Как условие - объект "сервер" ничего не знает об объекте "клиенте". После некоторого размышления и перебрав несколько вариантов я пришел к выводу, что наиболее приемлимые для практики есть два способа. Первый подсмотрен в WinAPI а второй - чисто Дельфи. Оба способа основаны на простой идее регистрации клиента на сервере и оповещении сервером клиентов по внутреннему списку зарегистрированных клиентов.

Способ 1. Оповещение через механизм сообщений Windows.

в модуле объекта-сервера в интерфейсной части определяется пользовательский номер события:
const
  WM_NOTIFY_MSG = WM_USER + 123;
в объекте-сервере реализуются две интерфейсные процедуры (вкупе с объявленным в приватной секции и созданным в конструкторе TList, в деструкторе не забудем его разрушить, естественно)
procedure RegisterHandle(HW: THandle);
var
  i: integer;
begin
  i := FWindList.IndexOf(pointer(HW));
  if i < 0 then
    FWinList.Add(pointer(HW));
end;

procedure UnregisterHandle(HW: THandle)
var
  i: integer;
begin
  i := FWindList.IndexOf(pointer(HW));
  if i >= 0 then
    FWinList.Delete(i);
end;
и создается функция оповещения в приватной секции:
procedure SendNotify(wParam, lParam: integer);
var
  i: integer;
begin
  i := 0;
  while i < FWinList.Count do
  begin
    SendMessage(integer(FWinList.Items[i]), WM_NOTIFY_MSG, wParam, lParam);
    Inc(i);
  end;
end;
можно вместо SendMessage использовать PostMessage, будет асинхронное сообщение, иногда это выгодней, например для исключения возможности бесконечной рекурсии.

Объект-клиент должен иметь хэндл окна, который регистрируется на объекте-сервере и обработчик событий этого окна, который будет вызыватся при оповещении сервером списка клиентов (окон).
У объекта-клиента можно поступить двояко. Если объект-клиент уже имеет хэндл окна (например, форма) то пишется обработчик фиксированного номера события:
procedure ServMsg(var Msg: TMessage); message WM_NOTIFY_MSG;
или если окна нет, то создается универсальный метод-обработчик и невидимое окно при помощи функции AllocateHWND() (пример смотрите в исходниках VCL - объект TTimer)

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

Способ 2. Оповещение через объект-посредник.

В отдельном модуле создаем объект-посредник, который имеет один метод типа SendEvent и одну ссылку на обработчик события OnEvent. Я назвал такой объект TSynaps (да простят меня нейрохирурги)
unit Synaps;

interface

uses
  Windows, Messages, SysUtils, Classes;

type
  TSynaps = class(TObject)
  private
    FOnEvent: TNotifyEvent;
  public
    procedure SendEvent;
    property OnEvent: TNotifyEvent read FOnEvent write FOnEvent;
  end;

implementation

procedure SendEvent;
begin
  if Assigned(FOnEvent) then
  try
    FOnEvent(Self);
  except
  end;
end;

end;
Причем методов и событий может быть много разных на любой вкус. С очередями, асинхронными "прослойками", задержками и другими наворотами. Тут уж кто на что горазд. Я лишь демонстрирую идею. Модуль с объектом-сервером и модуль с объектом-клиентом имеют право знать о модуле Synaps. В объекте-сервере реализуются уже знакомые нам три функции (чуть иначе):
в интерфейсе объекта:
procedure RegisterSynaps(Syn: TSynaps);
var
  i: integer;
begin
  i := FSynapsList.IndexOf(pointer(Syn));
  if i < 0 then
    FSynapsList.Add(pointer(Syn));
end;

procedure UnregisterSynaps(Syn: TSynaps);
var
  i: integer;
begin
  i := FSynapsList.IndexOf(pointer(Syn));
  if i >= 0 then
    FSynapsList.Delete(i);
end;
и приватная функция:
procedure NotifySynapses;
var
  i: integer;
begin
  i := 0;
  while i < FSynapsList.Count do
  begin
    TSynaps(FSynapsList.Items[i]).SendEvent;
    Inc(i);
  end;
end;
Объект-клиент создает в себе объект-синапс, назначает его событию OnEvent свой внутренний обработчик и регистрирует этот синапс на объекте-сервере. Вуаля! И получает оттуда уведомления. Кстати, в деструктор синапса можно встроить вызов события OnDestroy, и тогда объект-сервер, при регистрации клиента, может назначить ему обработчик и автоматически разрегистрировать его при уничтожении. Но это уже навороты.

Такой подход позволяет строить обратные вызовы любой сложности. К тому-же это чистый паскаль-код без привязки к операционке. (а вдруг Kylix :о)

Итог.

Как вы могли заметить, оба способа базируются на двух базовых идеях. Первое - это регистрация клиента на сервере, и второе - вызов сервером некой функции внутри клиента. Разница только в механизмах. И выбирать тут можно исходя из вкусов, предпочтений и неких требований, связанных с ресурсоемкостью, переносимостью и т. п.
На самом деле есть очень широко распространенный и давно известный метод под названием CallBack-функция.
Мы вызываем кого-то и передаем как один из параметров адрес другой функции. И этот метод частенько используется в WinAPI (смотрите, к примеру, справку по функции EnumFonts). Но! Механизм прямого CallBack-а довольно некрасиво ложится на объектную модель Дельфи, так что я не стал описывать его здесь. Тем более, что оба способа - то-же самое, но красивше. И самое последнее - не забывайте разрегистрировать клиента в конце работы и освобождать ресурсы в деструкторе. И да известят вас ваши сервера только о хорошем!
Проект Delphi World © Выпуск 2002 - 2004
Автор проекта: ___Nikolay