Улика, найденная в грязи 

Дон Тейлор

Пока Эйс идет по следу, бесчестный Дельфийский Мститель поглощает его опыт, нажитый тяжелым трудом, в самых разных областях: от экранов-заставок до вытесняющей мультизадачности.

В контору Эйса ворвалась Хелен. 

— Мне ужасно жаль, что ты потерял свой Дневник. Все будет хорошо, бэби,— сказала она, обнимая Эйса и прижимаясь к нему щекой. — Я бы пришла раньше, но на улицах сейчас небезопасно. 

Хелен Хайуотер происходила из вполне обеспеченной семьи, но решила самостоятельно строить свою карьеру. Глядя на ее изящную фигуру и светлые волосы, спадающие до плеч, трудно было предположить, насколько решительной она могла быть в ответственный момент. К настоящему моменту она успела закончить колледж и поступить в магазин на должность менеджера. Но ее заветная (хотя и до сих пор не сбывшаяся) мечта — стать женой Эйса Брейкпойнта. 

— Не потерял, Хелен. Дневник был украден. Все это было подстроено, от начала и до конца, а я попался, словно какой-нибудь лопух из Бэйпорта. 

Эйс поведал историю о том, что произошло прошлой ночью, и рассказал о своем утреннем разговоре с Мардж Рейнольдс. 

— Так что у нас по крайней мере имеется неплохое описание похитителя, — закончил он. — Правда, я не уверен, что это к чему-нибудь приведет. Мы оставили записку на двери управляющего с просьбой позвонить, когда он вернется. 

— Разве ты не видишь? — скептически спросила Хелен. — Это наверняка был Мелвин Бохакер. Описание подходит. Я уверена, что он затаил злобу после «Дела о двойной демонстрации» и пытается отомстить нам обоим. Вероятно, он заплатил этой женщине за ложный телефонный звонок. Готова поспорить, что он сейчас сидит дома и злорадствует. 

— По-моему, все не так просто, Хелен, — ответил Эйс. — Ты не видела лица Бохакера, когда я… 

Внезапно внимание Эйса привлекло что-то, происходящее снаружи. Сквозь жалюзи бокового окна он увидел человека, передвигающегося между кустами. Человек был высокого роста, носил низко надвинутую шляпу и длинный плащ цвета хаки. Он подошел ближе к окну и заглянул в комнату. 

— Это он! — закричал Эйс. — Тот человек, которого описала Мардж, — это он украл мой Дневник! Он вернулся, как в «Кошмаре на улице Вязов», — и я сейчас с ним потолкую! 

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

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

— Эй, что тут происходит? — раздался голос сзади. Эйс с трудом повернул голову и увидел приближающегося управляющего, Марвина Гарденса. 

— Я поймал чужака, которого Мардж видела прошлой ночью, — ответил Эйс, тщетно пытаясь стряхнуть с ресниц дождевые капли. — Того, кто украл мой Дневник. — Эйс не без труда поднялся на ноги, не ослабляя железной хватки на руке человека в плаще. Оба были покрыты грязью с головы до ног. 

— Да, я прочел записку, — сказал Гарденс, пережевывая дешевую сигару кривыми, пожелтевшими зубами, — и как раз собирался позвонить. Но это не чужак, Брейкпойнт. Поздоровайся с моим новым садовником, Сергеем Стакупоповым. Он плохо говорит по-английски, но это поймет. 

— Постой, — запротестовал Эйс. — Прошлой ночью этого человека видели с каким-то оружием. Два раза его пытались задержать, и оба раза он убегал. Садовник он или нет, но это говорит о том, что он виновен. 

— Там, откуда он приехал, люди живут в страхе перед секретной полицией,— ответил Гарденс, затянувшись сигарой. — Если в этой стране кто-то позовет на помощь, то это может стать его последним криком. Он получил «зеленую карту» и до смерти боится потерять ее — тогда его семье придется возвращаться на родину. Поэтому он много и усердно работает. А прошлым вечером он просто подстригал кусты, наверное, Мардж увидела его с садовыми ножницами, вот и все. 

Эйс ослабил хватку, отпустил садовника и извинился. Сергей насторожен но наблюдал за ним, потом вежливо улыбнулся и сказал: «Хэлло». 

Сыщик вернулся в контору, где его ждала Хелен с миллионом вопросов. Он пересказал ей события последних минут и задумался. 

— Эй, все будет нормально, — заверила Хелен. — Просто временная неудача. А теперь снимай свой грязный плащ, пока не простудился. 

Эйс неохотно подчинился. 

— Хорошо, если временная, — сказал он, выбирая в шкафу чистые плащ и шляпу. 

— Конечно, временная, милый, — ответила она. — Послушай, обед заканчивается, и мне нужно возвращаться в магазин. Днем обязательно позвони мне. Я зайду после работы, чтобы узнать, как дела. 

Она поцеловала его в щеку и вышла под проливной дождь. 

Масштабирование форм 

Мститель открыл новый пакет чипсов и набил рот. Он решил не возвращаться в контору Брейкпойнта. Это было рискованно, но скорее всего, ничего страшного не произойдет. Шансы на то, что бывший сыщик сможет найти случайно оставленную улику, близки к нулю. Лучше как можно быстрее впитать побольше информации. 

Дневник №16, 25 марта. Меня часто интересовало, как некоторые приложения ограничивают минимальный размер масштабируемого окна. Я решил узнать, как это делается. Мне даже в голову не приходило, как это просто. Тем не менее я заподозрил, что это должно иметь какое-то отношение к сообщениям. К тому времени я уже понял, что все, происходящее в Windows, связано с сообщениями. 

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

Мне нужна была форма с четырьмя компонентами как минимум . Я создал набросок формы, содержащей оперативную кнопку (TSpeedButton), поле Memo и две обычные кнопки (см. рис. 15.1). Прежде всего я решил ограничить пределы масштабирования минимальным размером формы, который должен задаваться программистом. Решение скрывалось в сообщении WM_GETMINMAXINFO

Рис. 15.1. Форма для демонстрации масштабирования, изображенная в режиме конструирования 

С помощью сообщения WM_GETMINMAXINFO приложение узнает о том, что система проверяет размер окна, и имеет возможность изменить параметры,
принятые по умолчанию. Среди этих параметров — значения, определяющие интервалы, в которых должен находиться размер окна. По умолчанию минимальный размер совпадает с размером значка (icon), а максимальный — с размером всего экрана. 

Фактический параметр, передаваемый обработчику WM_GETMINMAXINFO, представляет собой точку, которая определяет смещения X, Y (в пикселях) от левого верхнего угла окна. 

Следовательно, мне потребуется написать свой обработчик и определить в нем точки, описывающие минимальный и максимальный размер окна. Прежде всего я объявил переменные для минимальной высоты и ширины, чтобы упростить манипуляции с этими величинами. 

Затем потребовалось определить точку, представляющую правый нижний угол формы. Параметр lParam стандартного обработчика WM_GETMINMAXINFO является указателем на массив из пяти структур-точек. К счастью, волшебники из Borland предусмотрительно создали тип сообщения TWMGetMinMaxInfo, избавляющий вас от многих трудностей. 

В листинге 15.1 приведен полный исходный текст программы, в которой я экспериментировал с масштабированием. Листинг содержит обработчик, получившийся после нескольких неудачных попыток (удивительно, какие «интересные» эффекты могут возникнуть, если забыть о некоторых мелочах — например, о вызове унаследованного обработчика). Как видно из листинга, через структуру MinMaxInfo можно получить быстрый и удобный доступ к точкам, определяемым ptMinTrackSize и ptMaxTrackSize. Я вставил в обработчик OnCreate формы небольшой фрагмент для вычисления MinWidth и MinHeight на основании размеров компонентов в момент запуска. 

Листинг 15.1. Исходный текст программы для демонстрации
масштабирования формы

{——————————}
{Масштабирование формы 
(демонстрационная программа)                    }
RS.PAS : Главная форма                          }
{Автор: Эйс Брейкпойнт, N.T.P.                  }
{При содействии Дона Тейлора                    }
{                                               }
{Приложение показывает, как с помощью панелей   }
{  с заданным типом выравнивания и обработки    }
сообщений                                       }
{  Windows создаются гибкие формы, которые      }
ограничивают                                    }
{  возможности масштабирования и учитывают      }
{  их последствия.                              }
{ Написано для *High Performance Delphi 3       }
Programming*                                    }
{    Copyright (c) 1997 The Coriolis Group, Inc.}
{            Дата последней редакции 23/4/97    }
{————————}

unit Rs;

interface
uses
  SysUtils, WinTypes, WinProcs, Messages, 
  Classes, Graphics,
  Controls, Forms, Dialogs, StdCtrls, ExtCtrls, 
  Buttons;

type
  TRSMainForm = class(TForm)
    ControlPanel: TPanel;
    RSMemoPanel: TPanel;
    RSMemo: TMemo;
    BtnPanel: TPanel;
    SBPanel: TPanel;
    QuitSB: TSpeedButton;
    QuitBtn: TButton;
    SBComboPanel: TPanel;
    ComboBox1: TComboBox;
    SpeedButton1: TSpeedButton;
    Button1: TButton;
    procedure QuitBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure FormResize(Sender: TObject);
  private
    { Private declarations }
    procedure WMGetMinMaxInfo(var Msg: 
    TWMGetMinMaxInfo); message
    WM_GETMINMAXINFO;
  public
    { Public declarations }
  end;

var
  RSMainForm: TRSMainForm;
  MinWidth : Integer;
  MinHeight : Integer;

implementation

{$R *.DFM}

procedure TRSMainForm.QuitBtnClick(Sender: 
TObject);
begin
 Close;
end;

procedure TRSMainForm.FormCreate(Sender: 
TObject);
begin
 MinWidth  := RSMemoPanel.Width + BtnPanel.Width + 
 10;
 MinHeight := RSMainForm.Height
   - (RSMainForm.ClientHeight - (RSMemo.Top + 
   RSMemo.Height)) + 10;
end;
procedure TRSMainForm.WMGetMinMaxInfo(var Msg: 
TWMGetMinMaxInfo);
begin
 inherited;
  with Msg.MinMaxInfo^ do
  begin
    with ptMinTrackSize do
    begin
      X := MinWidth;
      Y := MinHeight;
    end;  { with }

    with ptMaxTrackSize do
    begin
      X := Screen.Width;
      Y := Screen.Height;
    end;  { with }
  end;  { with }
end;

procedure TRSMainForm.FormResize(Sender: TObject);
begin
 RSMemo.Height := RSMemoPanel.Height - 
 (2 * RSMemo.Top);
 RSMemoPanel.Width := RSMainForm.ClientWidth - 
 BtnPanel.Width;
 RSMemo.Width := RSMemoPanel.Width - 
 (2 * RSMemo.Left);
end;

end.

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

Когда я только начинал работать с панелями, они приносили мне немало хлопот. Но, познакомившись с ними поближе, я просто влюбился в три замечательные возможности, которыми они обладают. Во-первых, с помощью свойства Alignment можно установить абсолютную связь панели с родитель ским объектом; например, если задать свойству Alignment значение alTop, панель будет занимать всю верхнюю часть формы, на которой она находится. Во-вторых, положение прочих компонентов (полей Memo, кнопок и т. д.), находящихся на панели, остается фиксированным по отношению к панели, пока ее размеры остаются прежними. Наконец, панель, расположенная на другой панели, ведет себя так же, как и панель, находящаяся на форме: например, если свойство Alignment имеет значение alBottom, внутренняя панель «приклеивается» к нижней части внешней панели и занимает всю ее ширину. 

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







панель Panel3 должна иметь фиксированную высоту и занимать всю верхнюю часть формы, чтобы ее ширина всегда совпадала с шириной самой формы; 
оперативная кнопка SB1 должна оставаться в фиксированном положении по отношению к левому краю Panel3 (и, как следствие, к левому верхнему углу окна); 
панель Panel4 должна выравниваться по правому краю Panel3, чтобы при изменении размера Panel3 она сохраняла постоянный размер, но следовала за правым краем Panel3; 
панель Panel5 (содержащая поле Memo1) должна выравниваться по левой стороне формы, а ее высота должна зависеть от высоты формы; 
панель Panel1 (содержащая панель Panel2) должна выравниваться по правому краю формы, а ее высота должна зависеть от высоты формы; 
панель Panel2 (содержащая кнопки Button1 и Button2) должна выравнивать ся по нижнему краю Panel1, чтобы при масштабировании она сохраняла постоянный размер и следовала за нижним краем Panel1 (а следовательно, и всей формы); 
кнопки Button1 и Button2 должны находиться в фиксированных позициях панели Panel2Panel2, чтобы сохранялось их положение по отношению к нижнему и правому краю формы.

Уф! Мне пришлось потрудиться, задавая свойства разных панелей. Работа с панелями может вызвать некоторые трудности, пока вы не усвоите «правила хорошего тона». Значения alTop и alBottom свойства Alignment всегда имеют более высокий приоритет по сравнению с alLeft и alRight. В конце концов для Panel3 я задал значение alTop, для Panel1 и Panel4 — значение alRight, для Panel5 — alLeft, а для Panel2 — alBottom. Свойствам BevelOuter панелей Panel4 и Panel2 были присвоены значения bvNone, чтобы они «исчезли» и не выделялись на форме. Для панелей Panel3 и Panel4 был выбран цвет clGray, это позволило наглядно отделить их от других компонентов. Кроме того, я поместил на Panel4 комбини рованное поле и оперативную кнопку, чтобы убедиться в сохранении их положения. Наконец, я переименовал панели и убрал их заголовки. 

Я решил, что ширина внешней панели с кнопками (ранее называвшейся Panel1)останется прежней, а панели с полем Memo нужно позволить заполнять оставшуюся часть формы. Кроме того, я автоматически изменяю размеры поля Memo1, чтобы оно занимало всю площадь внешней панели, оставляя лишь небольшие поля с каждого края. Мне удалось проделать это с помощью простых вычислений в обработчике OnResize формы. 

После нескольких попыток все заработало, как надо. На рис. 15.2 изображена демонстрационная форма при запуске программы; на рис. 15.3 показано, как выглядит та же форма после масштабирования. 

Конец записи (25 марта).

Рис. 15.2. Форма при запуске программы 

Рис. 15.3. Та же форма после масштабирования 

Создание заставок 

Таинственная фигура закрыла дневник и потянулась к телефону. Аппарат с готовностью проглотил семь набранных цифр, а затем выдал серию гудков. Где-то на другом конце линии зазвонил телефон. Раздался щелчок, в трубке послышался уже знакомый нам обворожительный голос, и Мститель заговорил. 

— Привет, Крошка… Да, это я. Подумал, что тебе захочется узнать, как прошло дело ночью. Мне удалось вломиться в контору Эйса Брейкпойнта, как и было задумано, и украсть Дневник прямо у него из-под носа. Все прошло почти идеально… А твоя роль в этом дельце была просто бесценной. Без тебя у меня бы ничего не вышло… Что? Да, ждать пришлось долго — но поверь, тем слаще оказалась месть. Верно. Послушай, Крошка, бросай все и встречай меня в 9 часов у мотеля «Гейтс», возле шоссе 101. Точно — прямо на холме, сразу за Нортон Сити. Угу… Сегодня я покажу тебе книгу, которая изменит нашу жизнь и сделает меня самым гениальным программистом в мире. Да, Крошка, меня — Дельфийского Мстителя. Встречаемся в 9 вечера. Не опаздывай. Пока. 

Мститель повесил трубку, взял дневник и, громко хихикая, снова принялся перелистывать его. 

Дневник №16, 26 марта. Сегодня снова позвонил Торговец. На этот раз он хочет, чтобы я создал компонент-заставку, которым можно будет пользовать ся в разных программах. В общих чертах идея состоит в следующем:
поместить компонент на главную форму приложения, задать значения нескольких свойств и выдать окно заставки перед отображением главной формы. 

Я начал думать, что же требуется от обобщенной заставки. Не так уж много. Вероятно, ее стоит сделать модальной, чтобы программа приостановилась на время, пока заставка будет должным образом показана. Необходимо позаботиться о том, чтобы заставка исчезала по тайм-ауту, по щелчку мышью или в обоих случаях. Разумеется, в заставке должно присутствовать графическое изображение. Я сел за компьютер и создал исходную форму (см. рис. 15.4). 

Но как превратить ее в компонент? После пары неудачных попыток я решил, что лучший вариант — создать новый компонент, построенный на
основе TForm, но с добавлением промежуточного объекта-оболочки, управляющего работой TForm. Объект-оболочка может ограничиться управлением свойствами, связанными с объектом-заставкой, что позволит наделить заставку простым пользовательским интерфейсом. Кроме того, оболочка может заниматься созданием и уничтожением формы по простой команде,
выданной владельцем объекта-оболочки. Я решил назвать этот класс TSplashDialog

В рамках исходной спецификации я решил написать несложное тестовое приложение ы— форму, которая содержит всего одну кнопку и которой будет принадлежать TSplashDialog. Исходный текст тестового приложения приведен в листинге 15.2. 

Рис. 15.4. Исходная форма заставки 

Листинг 15.2. Тестовое приложение для проверки TSplashDialog

{——————————}
{Компонент-заставка                         }
{SPLSHMN.PAS : Главная форма                }
{Автор: Эйс Брейкпойнт, N.T.P.              }
{При содействии Дона Тейлора                }
{                                           }
{Простейшая программа, демонстрирующая      }
использование                               }
{компонента TSplashDialog. Попробуйте задать} 
другие                                      }
{временные задержки, размеры, графические   }
изображения                                 }
{и убедитесь в богатстве возможностей.      }
{                                           }
{ Написано для *High Performance Delphi 3   }
Programming*                                }
{Copyright (c) 1997 The Coriolis Group, Inc.}
{            Дата последней редакции 3/5/97 }
{————————}

unit SplshMn;
{$define Test }

interface

uses
  Windows, Messages, SysUtils, Classes, 
  Graphics, Controls, 
  Forms, Dialogs, StdCtrls, SplshDlg;

type
  TForm1 = class(TForm)
    QuitBtn: TButton;
    procedure QuitBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
    {$ifdef Test }
    SplashDialog1: TSplashDialog;
    {$endif }
  public
    { Public declarations }
 end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.QuitBtnClick(Sender: TObject);
begin
 Close;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 {$ifdef Test}
 SplashDialog1 := TSplashDialog.Create
 (Application);
 {$endif}
 SplashDialog1.Execute;
end;

end.

Здесь есть один момент, на который следует обратить внимание: никогда не тестируйте создаваемый компонент, помещая его в библиотеку Delphi. Ошибки могут привести к неприятным, иногда даже очень неприятным последствиям. Кроме того, обновление библиотеки компонентов — процесс слишком медленный и нудный, чтобы заниматься этим перед каждым запуском программы. Вместо этого следует включить модуль компонента в тестовую программу и затем создать экземпляр компонента программным путем. 

Именно это и происходит в данном случае. Использование условной директивы компилятора и константы с именем Test позволяет компилировать эту простую программу в двух режимах. Когда константа определена, условный код активен и в форме объявляется поле типа TSplashDialog с тем же именем (SplashDialog1), которое IDE присваивает компоненту при его помещении на форму. Использование условной проверки в обработчике OnCreate создает экземпляр SplashDialog1. В этом случае программа будет использовать небибли отечный объект TSplashDialog из скомпилированного модуля SplshDlg. 

Когда компонент будет закончен и занесен в библиотеку, перед знаком $ в директиве ставится точка. В этом случае $define превращается в обычный комментарий, и программой можно будет пользоваться для тестирования установленной версии компонента. 

Как видно из листинга, я решил воспользоваться диалоговым окном с помощью метода Execute — в соответствии с гордыми традициями специализи рованных системных диалоговых окон (например, TOpenDialog). 

Сцена для TSplashDialog подготовлена. Теперь следует решить, какие свойства ему необходимы. Программист должен иметь возможность указать размер заставки, хотя я предполагаю, что она всегда будет выводиться в центре экрана. Необходимо передавать информацию о том, есть ли на форме кнопка, и если есть — ее название. Если диалоговое окно должно пропадать
по тайм-ауту, необходимо задать величину задержки. Кроме того, нам понадобится объект TPicture, подключаемый к компоненту TImage. Чтобы работа с
графикой была достаточно гибкой, программист должен иметь возможность задать выравнивание, определить, должен ли компонент TImage автоматически подгоняться под размеры изображения и следует ли растягивать изображе ние до размеров TImage

Через пару часов у меня появился более или менее готовый компонент. Исходный текст приведен в листинге 15.3. 

Листинг 15.3. Исходный текст компонента TSplashDialog

{——————————}
{                 Компонент-заставка                   }
{           SPLSHDLG.PAS : Модуль компонента           }
{             Автор: Эйс Брейкпойнт, N.T.P.            }
{              При содействии Дона Тейлора             }
{                                                      }
{ Модуль описывает специализированный компонент,       }
{ отображающий окно-заставку в тот момент, когда       }
{ программа захочет это сделать (обычно при запуске    }
{ программы).                                          }
{                                                      }
{ Написано для *High Performance Delphi 3 Programming* }
{    Copyright (c) 1997 The Coriolis Group, Inc.       }
{            Дата последней редакции 3/5/97            }
{——————————————————————————————————————————————————————}

unit SplshDlg;

{$define Test }

interface

uses
  Windows, Messages, SysUtils, Classes, 
  Graphics, Controls, Forms,
  Dialogs, ExtCtrls, StdCtrls;

type
  ESplashConflict = class(Exception);

  TImageAlign = (iaNone, iaTop, iaBottom, iaLeft, 
  iaRight,
      iaClient, iaAllAboveButton);

  { TSplashForm - форма, отображаемая на экране. 
  Она содержит TImage,
    TButton и TTimer, чтобы программист мог гибко 
    использовать заставку. }
  TSplashForm = class(TForm)
    CloseBtn: TButton;
    Image: TImage;
    DelayTimer: TTimer;
    procedure CloseBtnClick(Sender: TObject);
    procedure Button1Click(Sender: TObject);
    procedure DelayTimerTimer(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

  { TSplashDialog - оболочка, окружающая 
  TSplashForm.
Форма принадлежит TSplashDialog, поэтому она может
"автоматически" создаваться, 
настраиваться, выполняться
и уничтожаться в любой момент. TSplashDialog 
открывает доступ
лишь к тем свойствам, которые используются 
заставкой, 
а затем передает их форме TSplashForm при ее 
создании. }
  TSplashDialog = class(TComponent)
  private
    FAlign : TImageAlign;
    FAutoSize : Boolean;
    FButtonCaption : String;
    FCaption : String;
    FDelay : Word;
    FHasButton : Boolean;
    FHasDelay : Boolean;
    FHeight : Word;
    FPicture : TPicture;
    FStretch : Boolean;
    FWidth : Word;
    procedure SetCaption(Value : String);
    procedure SetDelay(Value : Word);
    procedure SetHasButton(Value : Boolean);
    procedure SetHasDelay(Value : Boolean);
    procedure SetHeight(Value : Word);
    procedure SetPicture(Value : TPicture);
    procedure SetWidth(Value : Word);
  public
    constructor Create(AOwner : TComponent); 
    override;
    destructor Destroy; override;
    function Execute : Boolean; virtual;
  published
    property Align : TImageAlign read FAlign 
    write FAlign;
    property AutoSize : Boolean read FAutoSize 
    write FAutoSize;
    property ButtonCaption : String read 
    FButtonCaption 
write FButtonCaption;
property Caption : String read FCaption write 
SetCaption;
property Delay : Word read FDelay write SetDelay;
property HasButton : Boolean read FHasButton 
write SetHasButton;
property HasDelay : Boolean read FHasDelay 
write SetHasDelay;
property Height : Word read FHeight write 
SetHeight;
property Picture : TPicture read FPicture write 
SetPicture;
property Stretch : Boolean read FStretch write 
FStretch;
property Width : Word read FWidth write 
SetWidth;
  end;


procedure Register;

implementation

{$R *.DFM}
procedure TSplashDialog.SetCaption(Value : 
String);
begin
 if Value <> FCaption
  then FCaption := Value;
end;
{ Задаем значение FHasButton. Если пользователь 
указал, что 
  в заставке не должно быть ни кнопки, ни таймера, 
  инициируем
исключение - без них не удастся очистить экран! }
procedure TSplashDialog.SetHasButton(Value : 
Boolean);
begin
 if not Value and not FHasDelay
  then raise ESplashConflict.Create('Must have 
  either a button or a delay!')
  else FHasButton := Value;
end;

{ Задаем значение FHasDelay, защищаясь 
от аномального
  случая, описанного выше. }
procedure TSplashDialog.SetHasDelay(Value : 
Boolean);
begin
 if not Value and not FHasButton
  then raise ESplashConflict.Create('Must have 
  either a button or a delay!')
  else FHasDelay := Value;
end;

procedure TSplashDialog.SetHeight(Value : Word);
begin
 if (Value <> FHeight) and (Value > 10)
  then FHeight := Value;
end;

procedure TSplashDialog.SetWidth(Value : Word);
begin
 if (Value <> FWidth) and (Value > 20)
  then FWidth := Value;
end;

procedure TSplashDialog.SetDelay(Value : Word);
begin
 if (Value <> FDelay) and (Value > 0)
  then FDelay := Value;
end;

procedure TSplashDialog.SetPicture(Value : 
TPicture);
begin
 if Value <> nil then FPicture.Assign
 (Value);
end;
constructor TSplashDialog.Create(AOwner : 
TComponent);
begin
 inherited Create(AOwner);

 { Задаем значения по умолчанию}
 FAlign := iaAllAboveButton;
 FAutoSize := False;
 FStretch := False;
 FButtonCaption := 'OK';
 FCaption := copy(ClassName, 2, 
 Length(ClassName) - 1);
 FDelay := 3500;
 FHasButton := True;
 FHasDelay := True;
 FHeight := 200;
 FWidth := 300;
 FPicture := TPicture.Create;

 {$ifdef Test }
 FPicture.LoadFromFile('splash.bmp');
 FAlign := iaClient;
 FHasDelay := False;
 {$endif }

end;

destructor TSplashDialog.Destroy;
begin
 FPicture.Free;
 inherited Destroy;
end;

{ Самое важное происходит в методе Execute. 
Он вызывается 
  владельцем TSplashDialog в тот момент, когда 
  необходимо вывести
  заставку. Execute создает объект SplashForm и 
  изменяет его
  в соответствии с параметрами, передаваемыми 
  SplashDialog.
  При закрытии SplashForm уничтожается. }
function TSplashDialog.Execute : Boolean;
var
 SplashForm : TSplashForm;
begin
 try
  SplashForm := TSplashForm.Create(Application);
 except
  on E:Exception do
   begin
    MessageBeep(MB_ICONERROR);
    Result := False;
    Exit;
   end;
 end; { try }

 with SplashForm do
  begin
   Position := poScreenCenter;
   Caption := FCaption;
   Height := FHeight;
   Width := FWidth;

   if FAlign = iaAllAboveButton
    then begin
          if FHasButton
           then begin
                 Image.Align := alTop;
Image.Height := ClientHeight - 
CloseBtn.Height - 15;
                end
           else Image.Align := alClient;
         end
    else Image.Align := TAlign(Ord(FAlign));
   Image.AutoSize := FAutoSize;
   Image.Stretch := FStretch;
   if Image.Picture <> nil
    then Image.Picture.Assign(FPicture);

   if FHasButton
    then begin
CloseBtn.Caption := FButtonCaption;
CloseBtn.Left := (ClientWidth - CloseBtn.Width) 
div 2;
CloseBtn.Top := ClientHeight - 
CloseBtn.Height - 10;
         end
    else CloseBtn.Visible := False;

   if FHasDelay
    then begin
          DelayTimer.Interval := FDelay;
          DelayTimer.Enabled := True;
         end;

   try
    ShowModal;
   finally
    Free;
    Result := True;
   end; { try }
  end; { with }
end;
procedure TSplashForm.CloseBtnClick(Sender: 
TObject);
begin
 Close;
end;

procedure Register;
begin
 RegisterComponents('Ace''s Stuff', 
 [TSplashDialog]);  
end;

procedure TSplashForm.Button1Click(Sender: 
TObject);
begin
 Close;
end;

procedure TSplashForm.DelayTimerTimer(Sender: 
TObject);
begin
 Enabled := False;
 Close;
end;

end.

Приведенный фрагмент нуждается в нескольких комментариях. Я снова воспользовался условной директивой, чтобы компонент мог работать в двух режимах. В тестовом режиме (см. листинг 15.3) он автоматически загружает специальный тестовый растр и отключает таймер. Если вставить точку перед знаком $, директива превращается в комментарий, а файл можно будет откомпилировать в виде компонента Delphi и включить его в библиотеку. 

Я добавил небольшой фрагмент для предотвращения ситуации, при которой в заставке нет ни кнопки, ни таймера (это означало бы, что модальное диалоговое окно не удастся убрать с экрана!). Кроме того, я объявил перечисляемый тип (TImageAlign), который расширяет возможности типа TAlign, добавляя в него вариант iaAllAboveButton. Он означает, что пользователь желает использовать клиентскую область формы, но лишь ту часть, которая находит ся над кнопкой. Да, чуть не забыл — я также объявил специальный класс
исключения, который обрабатывает все проблемы, обнаруженные в процессе задания свойств. 

Самой интересной частью проекта оказался выбор объекта TPicture и помещение его в TImage. Получив несколько системных исключений, связанных с нарушением правил доступа, я начал прочесывать исходные тексты VCL и разыскивать все, что связано с выбором и назначением растровых изображений. Когда ответ был найден, я понял, насколько упростился этот процесс благодаря предусмотрительности разработчиков Delphi. Когда вы объявляе те свойство типа TPicture, Delphi IDE заранее знает, как с ним работать. Вы создаете экземпляр Tpicture в конструкторе объекта, а IDE вызывает Picture Editor для редактирования этого свойства. После того как в Picture Editor будет выбрано растровое изображение, оно автоматически сохраняется в потоке при закрытии файла формы. Это означает, что при следующем открытии файла растр окажется в нужном месте. 

В полном соответствии с целями проектирования оболочка TSplashDialog управляет важнейшими свойствами формы. При вызове метода Execute объект TSplashDialog создает экземпляр формы, задает значения ее свойств и затем вызывает ShowModal, чтобы приостановить все прочие действия программы. Когда выполнение программы возобновляется, форма уничтожается. Тестовый вариант заставки изображен на рис. 15.5. 

Рис. 15.5. Заставка во время выполнения программы 

Эйс получает ответ 

— Алло, Хелен? Да, детка, это я. Просто хочу сказать, что никаких новостей нет. Глухая стена. Я уже сотню раз перебрал все возможные варианты, но не сдвинулся ни на шаг. Никаких улик, я абсолютно беспомощен. Нечего сказать, хорош сыщик! 

— Эйс, ты действительно хороший сыщик — один из лучших, — сказала Хелен. — Просто на этот раз ты не справишься в одиночку. 

— Наверное, ты права, — признал он. — Помощь мне бы не помешала. 

— Почему бы тебе не поговорить с Автором? — предложила Хелен. — Помнишь, он тебе помогал раньше? 

— Хорошая мысль. Надо попробовать. Спасибо, детка, я тебя люблю. 

— Взаимно, — ответила она. 

Эйс повесил трубку и схватил телефонный справочник. Пробежав пальцем по странице, он нашел нужный номер и быстро набрал его. 

— Привет, Эйс, — отозвался голос в трубке. 

— Эээ… привет, — ответил Эйс. — Наверное, вы уже знаете, почему я звоню. 

— Ты хочешь получить ответы на некоторые вопросы, относящиеся к похищению твоего Дневника. 

— Да, я чувствую себя абсолютно беспомощным и решил позвонить вам. 

— Я довольно давно не слышал тебя, Эйс, — сказал голос в трубке. — С того самого «Дела о двойной демонстрации». Тогда я помог тебе, не правда ли? 

— О, да, — ответил Эйс. — Вы напомнили о том, какой я особенный, и подбодрили меня. Думаю, без вашей помощи у меня бы ничего не получилось. 

— Интересно, почему ты так и не поблагодарил меня? 

— Так уж получилось, — робко ответил Эйс. — Когда я справился с делом, то подумал, что помощь мне уже не понадобится. — Он на секунду задумался и добавил. — И еще не хотел вас беспокоить. Ведь я всего лишь один из ваших персонажей. 

— У меня все персонажи особенные. А ты — один из моих любимых персонажей. Очень жаль, что ты не позвонил. Иногда я огорчаюсь, когда ты пытаешься сделать все сам. Помни, ты можешь звонить мне в любое время дня и ночи, по любому поводу, важному или пустяковому. Но вернемся к твоему вопросу. Ты хочешь знать, кто украл твой Дневник. 

— Точно. Хелен думает, что это был Мелвин Бохакер с женщиной-сообщ ницей. Сначала я думал иначе, но теперь начал сомневаться. Дневник украл действительно Бохакер? 

— На этот вопрос можно дать три ответа: «Да», «Нет» и тот, который предназначен для тебя: «Не сейчас». 

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

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

— Так что же мне делать? — жалобно спросил Эйс. 

— Иди на стоянку и тщательно обыщи место рядом с твоей машиной. Ты найдешь ключ ко всей тайне. 

— Спасибо, — взволнованно ответил Эйс, — Я этого не забуду. 

Он поспешно бросил трубку и рванулся к двери. 

Глобальный доступ к данным
в приложении 

Тем временем Мститель снова погрузился в чтение похищенного Дневника. 

Дневник №16 (27 марта). В Delphi 1.0 совместное использование таблиц несколькими формами было крайне хлопотным делом. Хотя возможности не ограничивались размещением таблиц и источников данных на всех формах, работавших с данными, неуклюжее альтернативное решение требовало временного создания дополнительных источников данных с их последующим удалением. Мне часто хотелось отыскать более простой способ. В последую щих версиях Delphi появились объекты, которые назывались модулями данных и заметно упрощали эту задачу. Я решил побольше узнать о них. 

Как выяснилось, модуль данных представляет собой специализированную форму, на которой можно разместить только стандартные объекты из палитры Data Access. В приложении можно построить целую базу данных, с таблицами, источниками, запросами и всем остальным — и разместить ее в одном модуле данных. Чтобы результатами трудов могли воспользоваться другие формы, необходимо включить модуль данных в их секции implementation (не в секции interface!). При этом компоненты и поля модуля данных становятся доступными для компонентов формы, связанных с данными (а также для самого инспектора объектов). 

Я решил создать простой пример с данными одного из моих клиентов, фирмы «Чичен-Итца Пицца». Данные хранятся в виде таблицы Paradox, в файле PIZADAT.DB. Таблица состоит из трех полей: название, цена продажи и себестоимость лучших продуктов фирмы. Я решил добавить вычисляемое поле для отображения прибыли по каждой позиции (в процентах). 

Рис. 15.6. Модуль данных в режиме конструирования 

Сначала я создал (для последующего использования в свойстве DatabaseName) псевдоним с именем Pizza, определяющий каталог с таблицей. Затем создал новый модуль данных и присвоил ему имя PizzaData. В этот модуль (см. рис. 15.6) я поместил таблицу и источник данных, присвоив им имена ProductTable и ProductSource соответственно. Я подключил ProductSource
к ProductTable и задал свойству AutoEdit значение False. Затем открыл для таблицы Fields Editor и добавил в него все возможные поля. Наконец, я создал новое вычисляемое поле для хранения процента прибыли и написал обработ чик события OnCalcFields таблицы ProductTable. Окно модуля данных показано на рис. 15.6. Исходный текст модуля PizzaData приведен в листинге 15.4. 

Листинг 15.4. Исходный текст модуля данных

{——————————}
{Демонстрация работы с модулями данных          }
{PIZADAT.PAS : Модуль данных                    }
{Автор: Эйс Брейкпойнт, N.T.P.                  }
{При содействии Дона Тейлора                    }
{                                               }
{ Модуль данных содержит простейшую комбинацию  }
{ таблица/источник данных,подключаемую к таблице}
{ Paradox. Для пользователей модуля создано     }
{ вычисляемое поле.                             }
{                                               }
{ Написано для *High Performance Delphi 3       }
Programming*                                    }
{    Copyright (c) 1997 The Coriolis Group, Inc.}
{            Дата последней редакции 23/4/97    }
{—————————}

unit PizaDat;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, 
  Controls, Forms, Dialogs,
  DB, DBTables;

type
  TPizzaData = class(TDataModule)
    ProductTable: TTable;
    ProductSource: TDataSource;
    ProductTableName: TStringField;
    ProductTablePrice: TCurrencyField;
    ProductTableCost: TCurrencyField;
    ProductTablePctProfit: TFloatField;
    procedure ProductTableCalcFields(DataSet: 
    TDataSet);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  PizzaData: TPizzaData;

implementation

{$R *.DFM}

procedure TPizzaData.ProductTableCalcFields
(DataSet: TDataSet);

begin
 ProductTablePctProfit.Value := 100.0 *
  ((ProductTablePrice.Value - 
  ProductTableCost.Value) /
ProductTableCost.Value);
end;

end.

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

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

На рис. 15.7 изображены обе формы во время работы. В листинге 15.5 приведен исходный текст главной формы, а в листинге 15.6 — исходный текст вспомогательной формы. 

Рис. 15.7. Программа, демонстрирующая использование модуля данных 

Листинг 15.5. Исходный текст главной формы 

{——————————————————————————————————————————————————————}
{        Демонстрация работы с модулями данных         }
{             PIZAMAIN.PAS : Главная форма             }
{             Автор: Эйс Брейкпойнт, N.T.P.            }
{              При содействии Дона Тейлора             }
{                                                      }
{ Демонстрационная программа показывает, как           }
{ происходит подключение формы к модулю данных,        }
{ созданному для данного проекта. Форма                }
{ содержит переключатель для смены источника данных -  }
{ модуль или локальная пара таблица/источник данных.   }
{                                                      }
{ Написано для *High Performance Delphi 3 Programming* }
{    Copyright (c) 1997 The Coriolis Group, Inc.       }
{            Дата последней редакции 23/4/97           }
{——————————————————————————————————————————————————————}

unit PizaMain;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, 
  Controls, Forms,
  Dialogs, Grids, DBGrids, ExtCtrls, DBCtrls, 
  DBTables, DB, StdCtrls;

type
  TForm1 = class(TForm)
    DBGrid: TDBGrid;
    Navigator: TDBNavigator;
    DataSourceRBGroup: TRadioGroup;
    QuitBtn: TButton;
    LocalTable: TTable;
    LocalDataSource: TDataSource;
    LocalTableName: TStringField;
    LocalTablePrice: TCurrencyField;
    LocalTableCost: TCurrencyField;
    Bevel1: TBevel;
    procedure FormShow(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure DataSourceRBGroupClick(Sender: 
    TObject);
    procedure QuitBtnClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

uses
 PizaDat, PizaFrm2;
 
{$R *.DFM}

procedure TForm1.FormShow(Sender: TObject);
begin
 Form2.Show;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 DataSourceRBGroup.ItemIndex := 0;
end;

procedure TForm1.DataSourceRBGroupClick(Sender: 
TObject);
begin
 if Tag > 0
 then case DataSourceRBGroup.ItemIndex of
       0 : begin
DBGrid.DataSource := PizzaData.ProductSource;
Navigator.DataSource := PizzaData.ProductSource;
           end;
       1 : begin
DBGrid.DataSource := LocalDataSource;
Navigator.DataSource := LocalDataSource;
           end;
      end { case }
 else Tag := 1;
end;

procedure TForm1.QuitBtnClick(Sender: TObject);
begin
 Close;
end;

end.

Листинг 15.6. Исходный текст вспомогательной формы

{——————————————————————————————————————————————————————}
{        Демонстрация работы с модулями данных         }
{        PIZAFRM2.PAS : Вспомогательная форма          }
{             Автор: Эйс Брейкпойнт, N.T.P.            }
{              При содействии Дона Тейлора             }
{                                                      }
{ Демонстрационная программа показывает, как           }
{ происходит подключение формы к модулю данных,        }
{ включенному в проект. Эта форма получает данные      }
{ из модуля данных проекта.                            }
{                                                      }
{ Написано для *High Performance Delphi 3 Programming* }
{    Copyright (c) 1997 The Coriolis Group, Inc.       }
{            Дата последней редакции 23/4/97           }
{——————————————————————————————————————————————————————}
unit PizaFrm2;

interface

uses
  Windows, Messages, SysUtils, Classes, 
  Graphics, Controls, Forms,
  Dialogs, StdCtrls, DBCtrls, ExtCtrls, DB;

type
  TForm2 = class(TForm)
    NameDBText: TDBText;
    PctDBText: TDBText;
    Label1: TLabel;
    Label2: TLabel;
    Navigator: TDBNavigator;
    Bevel1: TBevel;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

uses
 PizaDat;
 
{$R *.DFM}

end.

Перед компиляцией я задаю свойствам Active всех объектов-таблиц значение True. Наверное, сказывается сила привычки. 

В секциях implementation обеих форм указывается PizaDat (имя модуля данных). После этого поля модуля данных становятся доступными в инспекторе объектов для любого компонента, связанного с данными. 

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

Похоже, обработчик OnClick для переключателей вызывается во время создания формы. К этому моменту источники данных еще не были полностью сконструированы и подключены, и в результате возникает исключение. Я решил предотвратить эту ситуацию с помощью свойства Tag формы. Во время инициализации оно равно 0, поэтому попытка подключения источников не производится. Однако следующий вызов обработчика — совсем другое дело, поскольку значение Tag было заменено на 1. 

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

Разумеется, возможности модулей данных отнюдь не ограничиваются вычисляемыми полями. Ведь в конце концов модуль данных является полноценным модулем Object Pascal, который может содержать новые объекты и методы, а также обработчики для любых событий, связанных с таблицами, источниками данных, SQL-запросами и т.д. В сущности, программист может реализовать полный набор логических правил для работы с данными компании. Довольно круто — и открывает очень, очень широкие возможности. 

Конец записи (27 марта).

Потрясающее открытие 

Эйс Брейкпойнт набрал рабочий номер Хелен. Она подняла трубку после второго гудка. 

— Алло, чем могу помочь? 

— Хелен — у меня есть потрясающие новости. Хочу, чтобы ты узнала их первой». 

— Отлично, милый, — ответила Хелен. — А что случилось? 

— Я нашел важную улику — вернее, я хотел сказать, что Автор помог мне отыскать важное вещественное доказательство. 

— Хорошо, что ты связался с ним, Эйс. И что же ты нашел? 

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

— Может быть, ее выронил кто-то из машины, припаркованной рядом с твоей, — заметила Хелен. 

— Черт возьми! — воскликнул он. — Ты знаешь, той ночью там действи тельно стояла машина. Я запомнил это только потому, что обычно это место остается пустым. Даже не могу вспомнить, как она выглядела. Помню только, что большая и грязно-белого цвета. Но сейчас наверняка не осталось ни единого отпечатка шин. 

— А как насчет перчатки? Что ты можешь сказать о ней? — спросила Хелен. 

Эйс внимательно осмотрел улику. 

— Не стоит и говорить, она вся перепачкана грязью. Снять отпечатки пальцев не удастся. Посмотрим, что там внутри… все промокло… подкладки нет… Постой! Здесь застряла пара волосков. Наверное, с руки вора. 

— Это Бохакер , Эйс, — взволнованно прошептала Хелен. 

— Дорогая, это невозможно. Какое-то время я действительно думал, что это он. Но я несколько раз пытался дозвониться до него, и к телефону никто не подходил. К тому же теперь, когда у нас есть перчатка… 

— Называй это женской интуицией или как хочешь, — прервала его Хелен, — но я просто знаю, что эта перчатка принадлежит Бохакеру. 

— Если бы у меня был хоть один волосок, который точно принадлежит ему, то мы бы могли провести анализ ДНК, — заметил Эйс. 

— Наверное, нам никогда не удастся это сделать. Дело в том… Эйс! У тебя сохранился плащ с кровью Мелвина Бохакера? Не подойдет ли он для анализа ДНК? 

— Конечно, сохранился! — воскликнул он. — Висит у меня в шкафу. Я отнесу его вместе с перчаткой в Крайм-сити. 

— Куда? 

— Это сеть круглосуточных лабораторий для обслуживания частных детективов. Они проанализируют ДНК и сообщат результаты по факсу через пару часов. Думаю, у меня даже завалялся купон, дающий право на скидку в 2 доллара. Сделаем так: я заброшу вещи в лабораторию и встречу тебя после работы. Мы где-нибудь перекусим, и к тому времени результаты экспертизы уже будут готовы. Как ты к этому относишься? 

— Можешь рассчитывать на меня, — ответила она. 

Прогулка по Win95 

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

— Пока ты занимаешься результатами, мне нужно кое-куда зайти, — сказала Хелен, изящно выпархивая из-за стола. 

— Ладно, — рассеянно произнес Эйс. Он машинально проследил за тем, как она проследовала к соседнему ресторану. Maison de Mort Rouge Viande был одним из самых шикарных местных заведений. Хелен обожала такие места — еще бы, ведь она привыкла к ним с детства. 

Эйс частично вышел из транса. 

«Если это действительно Бохакер, — подумал он, — я должен узнать об этом сейчас же. Пока Хелен не слышит, надо связаться с моим Человеком-На-Ули це и узнать, что происходит». 

Он извлек из кармана плаща верный сотовый телефон и набрал номер «Норвежских жареных цыплят Бака МакГаука» — далеконе самой шикарной забегаловки. 

— Добро пожаловать к Баку, — послышалось в трубке. — Сегодня вечером мы специализируемся на «Куриных Сюрприза х». Будете заказывать? 

— Это ты, Бифф? — спросил Эйс. 

— Эйс, как дела, дружище? 

— Мне нужна кое-какая информация, и побыстрее. Ты давно видел Мелвина Бохакера? 

— Забавно, что ты спрашиваешь о нем. Сегодня произошло нечто очень странное. 

— Выкладывай. 

— Не помню, говорил я тебе или нет, что Бохакер по вторникам и пятницам всегда заказывает «Особо Жирную Курицу». Обычно он сам приходит за своим обедом. 

— Ну? 

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

— Что еще? — торопил Эйс. 

— Было довольно шумно, все время проезжали машины. Но мне показалось, что он упомянул о какой-то женщине, с которой собирается встретить ся в Нортон-Сити. Что ты об этом думаешь? Может, его наконец кто-нибудь прикончит? 

— Не знаю, Бифф, — ответил Эйс. — Слушай, мне нужно идти. Потом поговорим. 

Эйс выключил телефон, сунул его в карман и направился к кассе. 

Тем временем изучение похищенного Дневника продолжалось… 

Дневник №16, 28 марта. С момента выхода самой первой версии Delphi мне не раз приходилось слышать, что этот пакет отличается от других средств визуального программирования тем, что сильно упрощает работу со всеми трудными аспектами Windows, но при этом позволяет программисту работать на сколь угодно низком уровне, вплоть до самых мелких «болтов и гаек». Я решил исследовать некоторые детали внутреннего устройства Windows 95 и узнать, как добраться до них из приложения, написанного на Delphi. 

Одно из главных отличий Windows 3.1 от Windows 95 — вытесняющая мультизадачность и те изменения, которые из нее следуют. В Windows 3.1 мультизадачность была кооперативной (cooperative); это означало, что в любой момент может выполняться только одна задача, и пока она добровольно не отдаст управление, все остальные задачи выполняться не будут. В частности, из этого следует, что одна программа всегда могла заблокировать доступ к системным структурам данных до тех пор, пока не считала нужным разрешить его. Однако в Win95 с ее многопоточностью и вытесняющей (preemptive) мультизадачностью сценарий выглядит иначе — операционная система, наделенная абсолютными полномочиями, сама распределяет кванты процессор ного времени на основании системы приоритетов. 

Фирма Microsoft тайком включила в Windows 3.1 библиотеку TOOLHELP.DLL. Хотя в книгах и журналах эта библиотека почти не рассмат ривалась (адокументации к ней практически не существовало), в Delphi 1.0 был включен интерфейсный модуль для работы с ней. Модуль ToolHelp содержал несколько интересных низкоуровневых процедур, в том числе процедуры TaskFirst и TaskNext, с помощью которых программист мог «пройтись» по текущему списку активных задач в системе. Я обрадовался, когда узнал о том, что в последующие версии Delphi был включен аналогичный интерфейсный
модуль, TLHELP32, ориентированный на 32-разрядное окружение. Я решил сконцентрировать свое сегодняшнее расследование на этой теме. 

Внимание, сейчас вылетит птичка… 

Сначала я был удивлен различиями между 16- и 32-разрядной версиями ToolHelp. Некоторые процедуры (в том числе и TaskFirst с TaskNext) в 32-разрядной версии отсутствовали. Что это, просчет со стороны разработчиков? 

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

Я мечтал о том, чтобы Win95 на несколько тактов уступила все полномочия моей программе и я получил бы абсолютно точный отчет о состоянии системы. Но при этом стабильность всей системы оказывается на совести одного приложения, что полностью противоречит всем принципам вытесняющей мультизадачности. 

Как же решается проблема? Необходимо «сфотографировать» всю систему, причем процесс «фотографирования» планируется по усмотрению Win95. Затем содержимое полученного снимка можно изучить, не вмешиваясь в
работу системы. Решение, что и говорить, не идеальное, но по крайней мере работающее. 

«Фотографирование» выполняется функцией CreateToolHelp32Snapshot, входящей в 32-разрядную версию ToolHelp. Функция вызывается с двумя параметрами. Первый из них представляет собой маску, определяющую тип собираемой информации. В табл. 15.1 приведены различные варианты масок и соответствующие им значения. Второй параметр является логическим номером процесса в системе. По этому логическому номеру (он принадлежит объекту, называемому идентификатором процесса , — process ID) можно получить доступ к одному процессу; изучая этот процесс, можно получить определенные сведения. Итак, в принципе необходимо проделать следующее: 

w вызовите функцию для получения списка всех текущих процессов, используя маску, определяющую тип нужных данных. При этом возвращается логический номер области памяти, поддерживаемой KERNEL32 и являющейся источником всей информации о состоянии системы; 

w используйте процедуры Process32First и Process32Next для перебора процессов из полученного списка и изучайте различные аспекты этих процессов. Сведения о каждом процессе помещаются в переменную, указанную при вызове процедуры перебора. 

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

Таблица 15.1. Маски функции CreateToolHelp32Snapshot 

Имя 

TH32CS_SNAPHEAPLIST 
 
 

TH32CS_SNAPPROCESS 

TH32CS_SNAPTHREAD 
 
 

TH32CS_SNAPMODULE 
 
 

TH32CS_SNAPALL 

Значение 




15 

Собираемые данные 

Пулы (heaps) памяти внутри
процесса 
 
 

Все процессы в системе 

Потоки, принадлежащие
заданному процессу 

Модули, принадлежащие
заданному процессу 

Все перечисленное выше 

Модуль WalkStuf 

После нескольких часов «зависаний» и множества фальстартов я написал модуль, приведенный в листинге 15.7. Он содержит несколько процедур общего назначения, заметно упрощающих составление списков модулей и процедур в Win95. 

Листинг 15.7. Исходный текст модуля WalkStuf

{——————————————————————————————————————————————————————}
{  Демонстрационная программа для сбора информации     }
{                    о системе                         }
{           WALKSTUF.PAS : Служебный модуль            }
{             Автор: Эйс Брейкпойнт, N.T.P.            }
{              При содействии Дона Тейлора             }
{                                                      }
{ Модуль содержит процедуры для получения информации   }
{  от модуля TlHelp32.                                 }
{                                                      }
{ Написано для *High Performance Delphi 3 Programming* }
{    Copyright (c) 1997 The Coriolis Group, Inc.       }
{            Дата последней редакции 23/4/97           }
{——————————————————————————————————————————————————————}

unit WalkStuf;

interface
uses
 Windows, Classes, Dialogs, SysUtils, TLHelp32;

const
 ws_FullPath = True;
 ws_NoDirectory = False;
 ws_Unique = True;
 ws_DupesOK = False;
 ws_InstanceCount = True;
 ws_NoInstanceCount = False;

 function GetSystemProcessList(FullPath : Boolean;
                               Unique : Boolean) : 
                               TStringList;

 function GetSystemModuleList(FullPath : Boolean;
Unique : Boolean;
IncludeData : Boolean) : TStringList;

 function GetProcessModules(ProcName : String;
FullPath : Boolean;
IncludeData : Boolean) : TStringList;

 function GetLocalModuleList : TStringList;
 function ModuleSysInstCount
 (ModuleName : String) : Integer;

implementation

{
 Возвращает строку, удаляя из нее информацию 
 о файловом пути.
}

function ChopPath(PathName : String) : String;
var
 s : String;
begin
 s := PathName;
 if Length(s) > 0
  then begin
        while Pos(':', s) > 0 do 
        Delete(s, 1, Pos(':', s));
        while Pos('\', s) > 0 do 
        Delete(s, 1, Pos('\', s));
        Result := s;
       end
  else Result := '';

end;

{
 Возвращает список строк с именами всех 
 активных процессов в системе.
}
function GetSystemProcessList
(FullPath : Boolean;
Unique : Boolean) : TStringList;
var
 AList : TStringList;
 ProcHandle : THandle;
 AProcEntry : TProcessEntry32;
begin
 AList := TStringList.Create;
 Result := AList;
 AList.Sorted := True;
 if Unique
  then AList.Duplicates := dupIgnore
  else Alist.Duplicates := dupAccept;

 ProcHandle := CreateToolHelp32Snapshot
 (TH32CS_SNAPPROCESS, 0);
 if ProcHandle = -1 then Exit;

 AProcEntry.dwSize := sizeof(TProcessEntry32);

 if Process32First(ProcHandle, AProcEntry)
  then begin
{ Добавить первый процесс }
if FullPath
 then AList.Add(AProcEntry.szExeFile)
 else AList.Add(ChopPath(AProcEntry.szExeFile));

{ Добавить все остальные процессы }
while Process32Next(ProcHandle, AProcEntry) do
 if FullPath
  then AList.Add(AProcEntry.szExeFile)
  else AList.Add(ChopPath(AProcEntry.szExeFile));
       end;

 CloseHandle(ProcHandle);
end;

{
 Возвращает строковый список с 
 именами всех активных модулей
 во всех процессах.
}
function GetSystemModuleList(FullPath : Boolean;
 Unique : Boolean;
 IncludeData : Boolean) : TStringList;
var
 s : String;
 AList : TStringList;
 ProcHandle : THandle;
 ModHandle : THandle;
 AProcEntry : TProcessEntry32;
 AModEntry : TModuleEntry32;
begin
 AList := TStringList.Create;
 Result := AList;
 AList.Sorted := True;
 if Unique
  then AList.Duplicates := dupIgnore
  else Alist.Duplicates := dupAccept;

 ProcHandle := CreateToolHelp32Snapshot
 (TH32CS_SNAPPROCESS, 0);
 if ProcHandle = -1 then Exit;

 AProcEntry.dwSize := sizeof(TProcessEntry32);
 AModEntry.dwSize := sizeof(TModuleEntry32);

 if Process32First(ProcHandle, AProcEntry)
  then begin
        { Обработка первого процесса }
ModHandle := CreateToolHelp32Snapshot
(TH32CS_SNAPMODULE,
AProcEntry.th32ProcessID);
        if Module32First(ModHandle, AModEntry)
         then begin
{ Обработка первого модуля первого процесса }
               if IncludeData
then s := '<' + 
IntToStr(AModEntry.GlblcntUsage)
  else s := '';

 if FullPath
  then s := AModEntry.szExePath + s
  else s := AModEntry.szModule + s;
 AList.Add(s);
 
 { Обработка остальных модулей первого процесса}
 while Module32Next(ModHandle, AModEntry) do
  begin
   if IncludeData
    then s := '<' + 
    IntToStr(AModEntry.GlblcntUsage)
    else s := '';

   if FullPath
    then s := AModEntry.szExePath + s
    else s := AModEntry.szModule + s;
   AList.Add(s);
  end;
 CloseHandle(ModHandle);

 { Обработка оставшихся процессов }
 while Process32Next(ProcHandle, AProcEntry) do
  begin
   ModHandle :=
       CreateToolHelp32Snapshot(TH32CS_SNAPMODULE,
AProcEntry.th32ProcessID);
   if Module32First(ModHandle, AModEntry)
    then begin
{ Обработка первого модуля текущего процесса }
          if IncludeData
          then s := '<' +
IntToStr(AModEntry.GlblcntUsage)
          else s := '';

          if FullPath
           then s := AModEntry.szExePath + s
           else s := AModEntry.szModule + s;
           AList.Add(s);

          { Обработка оставшихся модулей 
            текущего процесса }
while Module32Next(ModHandle, AModEntry) do
           begin
            if IncludeData
             then s := '<' +
IntToStr(AModEntry.GlblcntUsage)
             else s := '';

            if FullPath
             then s := AModEntry.szExePath + s
             else s := AModEntry.szModule + s;
             AList.Add(s);
           end;
         end;
         CloseHandle(ModHandle);
  end; { while }

              end;
       end;

 CloseHandle(ProcHandle);
end;

{
 Возвращает строковый список с 
 именами всех активных модулей
 текущего процесса.
}
function GetLocalModuleList : TStringList;
var
 AList : TStringList;
 ModHandle : THandle;
 AModEntry : TModuleEntry32;
begin
 AList := TStringList.Create;
 AList.Sorted := True;
 Result := AList;
 ModHandle := CreateToolHelp32Snapshot
 (TH32CS_SNAPMODULE, 0);
 if ModHandle = -1 then Exit;

 AModEntry.dwSize := sizeof(TModuleEntry32);

 if Module32First(ModHandle, AModEntry)
  then begin
        { Добавляем первый модуль }
        AList.Add(AModEntry.szModule);

        { Добавляем остальные модули }
while Module32Next(ModHandle, AModEntry) do
         AList.Add(AModEntry.szModule);
       end;

 CloseHandle(ModHandle);
end;

{
 Возвращает список строк с именами всех активных 
 модулей процесса
 с заданным именем.
}
function GetProcessModules(ProcName : String;
 FullPath : Boolean;
 IncludeData : Boolean) : TStringList;
var
 s : String;
 Found : Boolean;
 Done : Boolean;
 AList : TStringList;
 ProcHandle : THandle;
 ModHandle : THandle;
 AProcEntry : TProcessEntry32;
 AModEntry : TModuleEntry32;

begin
 AList := TStringList.Create;
 Result := AList;
 AList.Sorted := True;

 ProcHandle := CreateToolHelp32Snapshot
 (TH32CS_SNAPALL, 0);
 if ProcHandle = -1 then Exit;

 AProcEntry.dwSize := sizeof(TProcessEntry32);
 AModEntry.dwSize := sizeof(TModuleEntry32);

 if Process32First(ProcHandle, AProcEntry)
  then begin
        { Просматриваем процессы, пока не будет 
        найдено совпадение }
        Found := 
UpperCase(AProcEntry.szExeFile) = 
UpperCase(ProcName);
        if not Found
         then repeat
Done := not Process32Next(ProcHandle, AProcEntry);
if not Done
then Found := UpperCase(AProcEntry.szExeFile) =
                              UpperCase(ProcName);
              until Done or Found;

        if Found
         then begin
ModHandle :=
  CreateToolHelp32Snapshot(TH32CS_SNAPMODULE,
AProcEntry.th32ProcessID);
if Module32First(ModHandle, AModEntry)
 then begin
{ Обработка первого модуля первого процесса }
       if IncludeData
        then s := '<' +
IntToStr(AModEntry.GlblcntUsage)
        else s := '';

       if FullPath
        then s := AModEntry.szExePath + s
        else s := AModEntry.szModule + s;
       AList.Add(s);
       { Обработка остальных модулей 
         первого процесса }
while Module32Next(ModHandle, AModEntry) do
        begin
         if IncludeData
          then s := '<' +
IntToStr(AModEntry.GlblcntUsage)
                         else s := '';

if FullPath
 then s := AModEntry.szExePath + s
 else s := AModEntry.szModule + s;
AList.Add(s);
   end;
 end;
               CloseHandle(ModHandle);

              end;
       end;
 CloseHandle(ProcHandle);
end;

{
 Возвращает количество экземпляров заданного 
 модуля во всех
 процессах системы.
}
function ModuleSysInstCount(ModuleName : String) 
: Integer;
var
 Idx : Integer;
 p : Integer;
 s : String;
 ModList : TStringList;
 MatchFound : Boolean;
begin
 Result  := -1;
 ModList := GetSystemModuleList(ws_NoDirectory, 
 ws_DupesOK,

                             ws_InstanceCount);
 if ModList = nil then Exit;

 Idx := 0;
 p := 0;
 MatchFound := False;
 while (Idx < ModList.Count) and not 
 MatchFound do
  begin
   s := ModList.Strings[Idx];
   p := pos('<', s);
   MatchFound := Uppercase(copy(s, 1, p - 1)) 
   = Uppercase(ModuleName);
   if not MatchFound then Inc(Idx);
  end; { while }

 if MatchFound
  then Result := StrToInt(copy(s, p + 1, 
  Length(s) - p))
  else Result := 0;
end;

end.

Модуль WalkStuf содержит пять полезных функций, заметно облегчающих дальнейшие исследования. GetSystemProcessList возвращает список строк с именами всех активных процессов в системе. Предусмотрена возможность вывода только имени процесса (без полного пути) и подавления множественных экземпляров одного процесса. GetSystemModuleList возвращает список строк с именами всех модулей во всех процессах. Предусмотрены аналогичные возможности для подавления информации о пути и множественных экземпля рах; кроме того, в каждую строку можно дополнительно включить количество экземпляров каждого модуля, существующих в системе. GetProcessModules возвращает список строк с именами всех модулей заданного процесса. GetLocal ModuleList создает список модулей, принадлежащих только заданному процессу. Наконец, ModuleSystemCount возвращает целое число, равное количеству экземпляров заданного модуля в системе. 

Кое-что в функциях модуля WalkStuf заслуживает особых пояснений. GetSystemProcessList показывает, как происходит перебор процессов из списка. Переменной ProcHandle присваивается логический номер области внутри KERNEL32, подготовленной для хранения списка всех процессов. Затем полю dwSize записи TProcessEntry32 (предназначенной для хранения информации о процессе) присваивается размер этого типа данных (на первый взгляд это кажется почти глупым, но на самом деле критически важно для правильной работы!). Затем вызывается Process32First с параметрами ProcHandle (информация из KERNEL32) и AProcEntry (это переменная для хранения данных). 

Если Process32First возвращает True, значит, информация о первом процессе из списка была скопирована в поля AProcEntry. Вероятно, наибольший интерес представляют поля szExeFile и th32ProcessID. Первое содержит строку с полным путем к EXE-файлу, создавшему процесс. Второе содержит уникальный идентификатор изучаемого процесса, который можно передавать другим функциям ToolHelp. Вскоре об этом будет рассказано подробнее. 

После того как szExeFile попадет в список строк, цикл while используется для многократных вызовов Process32Next. Эта функция вызывается с теми же параметрами, и если она возвращает True, значит, в AProcEntry были помещены данные следующего процесса (если вам приходилось пользоваться функциями FindFirst и FindNext под DOS, эта механика покажется знакомой). Когда перебор закончен, остается лишь выполнить последнюю задачу. Ведь вызов CreateToolHelp32Snapshot создал объект Win95, который необходимо уничтожить. Это делается с помощью вызова CloseHandle

GetSystemModule представляет собой более сложный вариант перебора. Полный список модулей каждого процесса просматривается функциями Module32 First и Module32Next. Для каждого процесса CreateToolHelp32Snapshot возвращает логический номер. На этот раз при вызове используется уникальный идентификатор текущего изучаемого процесса (AProcEntry.th32ProcessID), благода ря чему полученный логический номер относится к информации о модулях, принадлежащих только указанному процессу. Обратите внимание на использование маски TH32CS_SNAPMODULE, которая ограничивает полученную информа цию сведениями о модулях. 

Записи TModuleEntry32 содержат несколько полей. Для наших целей наиболь ший интерес представляют поля szExePath (строка, содержащая полный путь к модулю), szModule (строка с базовым именем модуля) и GlblcntUsage (двойное слово, содержащее количество экземпляров данного модуля в системе). 

Снова обратите внимание на то, что в поле dwSize записи AModEntry необходимо указать размер записи TModuleEntry32, и что для каждого вызова CreateTool Help32Snapshot должен присутствовать парный вызов CloseHandle, уничтожаю щий созданный объект. 

Все остальные функции в основном являются «вариациями на тему». Get LocalModuleList перебирает модули, принадлежащие только текущему процессу, для чего в качестве идентификатора процесса передается 0. GetProcessModules перебирает список модулей и ищет в нем заданный процесс. Если поиск окажется успешным, функция перебирает модули этого процесса. Наконец, Module SysInstCount с помощью вызова GetSystemModuleList получает список модулей для всей системы, из которого отбирает заданный модуль. Из строки, соответству ющей найденному модулю, она выбирает количество экземпляров и возвращает его в виде целого числа. 

Итоги 

Говорят, когда дела становятся совсем плохи, главное — вовремя приготовить кофе. Я заварил целый кофейник и занялся программой, демонстрирующей работу с функциями модуля WalkStuf. На рис. 15.8 показаны результаты ее работы. Исходный текст приведен в листинге  15.8. 

Рис. 15.8. Демонстрационная программа для сбора информации о системе 

Листинг 15.8. Исходный текст главного модуля программы Walking Demo

{——————————}
{Демонстрационная программа для сбора информации}
{                    о системе                  }
{            WALKAMIN.PAS : Главный модуль      }
{             Автор: Эйс Брейкпойнт, N.T.P.     }
{              При содействии Дона Тейлора      }
{                                               }
{ Программа демонстрирует некоторые возможности }
{  для сбора служебной информации в Win95.      }
{                                               }
{ Написано для *High Performance Delphi 3 
Programming* }
{    Copyright (c) 1997 The Coriolis Group, Inc.}
{            Дата последней редакции 23/4/97    }
{————————}

unit WalkMain;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, 
  Controls, Forms, Dialogs,
  WalkStuf, Grids, StdCtrls, ExtCtrls;

type
  TForm1 = class(TForm)
    ModuleGrid: TStringGrid;
    RefreshBtn: TButton;
    QuitBtn: TButton;
    ModuleRBGroup: TRadioGroup;
    ProcessesLabel: TLabel;
    ProcessListBox: TListBox;
    ModulesLabel: TLabel;
    procedure QuitBtnClick(Sender: TObject);
    procedure RefreshBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure ModuleRBGroupClick(Sender: TObject);
    procedure ProcessListBoxClick(Sender: TObject);
  private
    TheList : TStringList;
    procedure RefreshForm;
    procedure DisplayProcessModules;
    procedure ClearModuleGrid;
    procedure FillProcessList;
    procedure FillModuleGrid;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

{
  Возвращает строку пробелов заданной длины.
}
function Spaces(Size : Integer) : String;
begin
 Result := '';
 while Length(Result) < Size do Result 
 := Result + ' ';
end;

{
 Очищает экранные элементы, получает данные и 
 обновляет экран.
}
procedure TForm1.RefreshForm;
begin
 ClearModuleGrid;
 ProcessListBox.Clear;
 TheList := GetSystemProcessList(ws_FullPath, 
 ws_DupesOK);
 FillProcessList;
 ProcessesLabel.Caption := 'System processes: ' + 
 IntToStr(TheList.Count);
 TheList.Free;

 case ModuleRBGroup.ItemIndex of
  0 : begin
       TheList := GetSystemModuleList
       (ws_NoDirectory, 
ws_Unique, ws_InstanceCount);
       FillModuleGrid;
       ModulesLabel.Caption 
       := 'System-wide modules: ' +
IntToStr(TheList.Count);
       TheList.Free;
      end;

  1 : begin
       TheList := GetSystemModuleList
       (ws_NoDirectory, 
ws_Unique, ws_InstanceCount);
       if TheList.Count > 0
        then begin
              ProcessListBox.ItemIndex := 0;
              DisplayProcessModules;
             end;
      end;
 end; { case }
end;

{
 Специальная процедура обновления экрана, 
 которая получает
 сведения о модулях текущего выбранного процесса.
}
procedure TForm1.DisplayProcessModules;
var
 Idx : Integer;
 s : String;
 p : Integer;
begin
 if ProcessListBox.Items.Count > 0
  then begin
ClearModuleGrid;
Idx := ProcessListBox.ItemIndex;
TheList := GetProcessModules
(ProcessListBox.Items[Idx],
  ws_NoDirectory, ws_InstanceCount);

if TheList.Count > 0
 then for Idx := 1 to TheList.Count do
  begin
   s := TheList.Strings[Idx - 1];
   p := pos('<', s);
   ModuleGrid.Cells[0, Idx] := copy(s, 1, p - 1);
   delete(s, 1, p);
   s := Spaces(15) + s;
   ModuleGrid.Cells[1, Idx] := s;
   ModuleGrid.RowCount := ModuleGrid.RowCount + 1;
  end;

ModulesLabel.Caption := 
'Modules for this process: '
   + IntToStr(TheList.Count);  
TheList.Free;
       end;
end;

{
 Очищает все строки в списке модулей и задает 
 количество строк,
 равное 1.
}
procedure TForm1.ClearModuleGrid;
var
 Idx : Integer;
begin
 for Idx := 1 to ModuleGrid.RowCount - 1 do
  begin
   ModuleGrid.Cells[0, Idx] := '';
   ModuleGrid.Cells[1, Idx] := '';
  end;
 ModuleGrid.RowCount := 2;
end;
{
 Построчно заполняет список процессов 
 из глобального списка.
}
procedure TForm1.FillProcessList;
var
 Idx : Integer;
begin
 if TheList.Count > 0
  then for Idx := 0 to TheList.Count - 1 do
    ProcessListBox.Items.Add
    (TheList.Strings[Idx]);
end;

{
 Построчно заполняет список модулей 
 из глобального списка.
}
procedure TForm1.FillModuleGrid;
var
 s : String;
 p : Integer;
 Idx : Integer;
begin
 if TheList.Count > 0
  then begin
        for Idx := 1 to TheList.Count do
begin
 s := TheList.Strings[Idx - 1];
 p := pos('<', s);
 ModuleGrid.Cells[0, Idx] := copy(s, 1, p - 1);
 delete(s, 1, p);
 s := Spaces(15) + s;
 ModuleGrid.Cells[1, Idx] := s;
 ModuleGrid.RowCount := ModuleGrid.RowCount + 1;
end; { for }
       end;
end;

procedure TForm1.QuitBtnClick(Sender: TObject);
begin
 Close;
end;

procedure TForm1.RefreshBtnClick(Sender: TObject);
begin
 RefreshForm;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 ModuleGrid.Colwidths[1] := ModuleGrid.Width - 
 ModuleGrid.ColWidths[0] - 22;
 ModuleGrid.Cells[0, 0] := 'Name';
 ModuleGrid.Cells[1, 0] := 'System instances';
 ModuleRBGroup.ItemIndex := 0;
end;

procedure TForm1.ModuleRBGroupClick(Sender: 
TObject);
begin
 RefreshForm;
end;

procedure TForm1.ProcessListBoxClick(Sender: 
TObject);
begin
 if ModuleRBGroup.ItemIndex > 0 then 
 DisplayProcessModules;
end;

end.

В этом листинге нет ничего особенного. В верхнем списке всегда перечисляются все активные процессы. При установке переключателя System-wide в нижнем поле появляется список всех модулей, показывающий и количество экземпляров каждого из них. Если установлен переключатель Selected Process only, в нижнем поле выводятся только модули процесса, выделенного в верхнем списке. Кнопка Refresh делает новый «снимок» и обновляет экран. Главное, что необходимо запомнить, — при вызове любой функции, возвращающей список строк, создается новый объект; позднее его необходимо уничтожить, причем ровно один раз. 

Все это было весьма поучительно и к тому же занятно. Однако мое расследование было весьма поверхностным, и предстояло еще многое узнать. В частности, я обнаружил, что функции ToolHelp работают только в Win95, но не вNT (по крайней мере в настоящее время). 

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

Конец записи (28 марта).

Когда Эйс и Хелен прибыли в контору, результаты экспертизы ДНК еще не поступили. Эйс достал бутылку и сдул пыль с двух стаканов, найденных в шкафу. Он плеснул в них немного виски и передал один стакан Хелен, но стоило ему поднести стакан к губам, как в дверь громко постучали. 

Это была Мардж Рейнольдс. Во время обычного обмена любезностями с Хелен в ее голосе сквозило необычное оживление. Мардж быстро перешла к делу. 

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

Она сделала паузу, глядя на Эйса и ожидая проявлений интереса. 

— Продолжай, — взмолился он. 

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

Мардж порылась в кармане мешковатого вязаного свитера. 

— Он должен быть где-то здесь. Я его положила… ага, вот он, — сказала она, извлекая бумажку лавандового цвета. — Похоже, почерк женский. Здесь записаны твое имя и номер телефона, и еще два слова — «похищенная наследница». Тебе это о чем-нибудь говорит?


 

Предыдущая Содержание Следующая

Используются технологии uCoz

Rambler's Top100 Rambler's Top100
Используются технологии uCoz