|
Возвращение оракула
Дон Тейлор
Пока Дельфийский Мститель узнает, как
научить приложения Delphi обнаруживать присутствие самих себя и среды Delphi
(а заодно получает плавающую панель инструментов), Эйс обнаруживает очень
странное уравнение со множеством неизвестных — но в конечном счете
приводит дело к потрясающей развязке.
Эйс включил галогеновую настольную лампу
и поднес лавандовый обрывок к свету.
— Довольно дорогая бумага, — сказал он.
— Виден край водяного знака.
Эйс повернул клочок так, чтобы свет отражался
от бумаги. — Почерк действительно женский. Судя по размеру закругленных
элементов, принадлежит особе с сильным характером. Очень похоже на женщину,
которой хватило смелости позвонить мне вчера вечером. Характерные линии,
похоже на ручку с дорогим пером «Хабашер №4374» и чернилами «Ночная тень».
Вот, пожалуй, и все.
— Можно посмотреть? — спросила Хелен.
— Конечно, — ответил Эйс, передал записку
и добавил: — Да, и еще одно. Бумага пахнет духами.
Хелен понюхала обрывок, и глаза ее расширились.
— Это не просто духи, — сказала она. —
Это очень дорогие духи, Chez Monieux.
— Значит, пахнет дорогими духами,
— раздраженно заметил Эйс.
— Я не спорю с тобой, милый. Просто женщины
иногда замечают мелочи, которых не видят мужчины. Видишь ли, только утонченная,
хорошо обеспеченная женщина может позволить себе духи Chez Monieux.
— Ну, хватит об этом. Я ведь уже сказал,
что она использовала дорогую бумагу, очень хорошую ручку и чернила, не
так ли? В конце концов, я не эксперт по духам, и…
— Думаю, Хелен всего лишь пытается сказать,
— вмешалась Мардж, — что эти духи называются Chez Monieux.
Эйс на секунду застыл в задумчивости. Он
вдруг вспомнил, что слышал нечто подобное от Маффи.
— Я так и знал, — произнес он осторожно.
— Помнишь? — сказала Хелен. — Эти духи
входят в эксклюзивную коллекцию моды Маффи.
— Помню, — ответил Эйс. — Но что это нам
дает?
— У меня есть гипотеза, — начала Хелен.
— Думаю, Мелвин Бохакер давно мечтал поквитаться с тобой. Наверное, он
познакомился с хорошо обеспеченной женщиной, которая ставит собственное
достоинство превыше всего.
Она выжидательно посмотрела на своих собеседников.
Эйс закатил глаза, но Мардж явно внимала каждому слову Хелен.
— Когда эта женщина — назовем ее «Мадам
Икс» — узнала, что Мелвин потерял лицо из-за тебя, она подговорила его
свести старые счеты. Они составили план и выполнили его вчера вечером.
На ее машине они подъехали к конторе и остановились рядом с твоей машиной.
Он вышел и спрятался, а она отправилась к телефонной будке и набрала заранее
записанный номер. В это время она могла даже видеть тебя через окно кухни.
— Но как же записка попала туда, где ее
нашли? — спросил Эйс.
— Могу предположить и это — ее просто сдуло
порывом ветра. Сама телефонная будка освещена, но в нескольких футах от
нее записка вполне могла затеряться в темноте. Видимо, она торопилась,
а может быть, даже не заметила пропажи.
— А перчатка?
— Конечно, чтобы не оставить отпечатков
пальцев, Бохакер взламывал дверь конторы в перчатках. Садясь в машину,
он просто выронил одну из них. Может быть, его машина даже проехала по
перчатке и вдавила ее в грязь.
— Очень жаль, — сказал Эйс, перебивая ее
взмахом руки. — Все это звучит довольно правдоподобно, за исключением одного:
Бохакер на такое просто не способен, даже подстрекаемый какой-то богатой
дамочкой.
Хелен вздохнула:
— Наверное, стоит подождать результатов
экспертизы ДНК. По крайней мере, это докажет, кто из нас прав.
— Экспертиза? — спросила Мардж. — Какая
экспертиза? И как насчет перчатки, которую ты нашел?
Эйс поведал историю перчатки. На полное
изложение всех подробностей потребовалось не менее получаса.
— Да, интересный денек, — сказала она.
— Я бы хотела посидеть с вами и подождать результатов экспертизы. Но сегодня
в номер 193 въезжает новый жилец — кстати, холостой, — так что, пожалуй,
я узнаю, как он устроился.
Мардж ?ейнольдс неуклюже протиснулась в
дверь и закрыла ее за собой.
Совместное использование
обработчиков событий
Зловещая фигура склонилась над книгой и продолжала
читать.
Дневник №16, 29 марта. В сегодняшней
почте среди счетов я нашел приглашение на свадьбу наших старых друзей —
пары, с которой мы с Хелен познакомились еще в колледже. Текст гласил:
«Приходи и раздели с нами
это радостное событие».
Бросив открытку на стол, я подумал о том,
что «разделять события» можно и по-другому — например, за счет использования
общих обработчиков для событий, требующих похожих действий.
Я решил написать простое приложение для
исследования этой концепции. После некоторых размышлений я придумал программу
«Список неотложных дел», которая развивает демонстрационную программу перетаскивания,
написанную несколько дней назад.
Новая программа (как и ее предыдущий вариант)
содержит текстовое поле для ввода заметок. Но вместо календаря я создал
три отдельные сетки — для утренних, дневных и вечерних дел. Эти сетки находятся
на отдельных вкладках окна. Модель формы в режиме конструирования изображена
на рис. 16.1.
?ис. 16.1. Демонстрационная программа для
совместного использования обработчиков
Первая попытка
Общий сценарий выглядит так: я выбираю нужную
вкладку и перетаскиваю строку с описанием задачи на сетку. Для всех трех
страниц при этом выполняются практически одинаковые операции. Единственное
отличие заключается в том, какая сетка получает строку. Следовательно,
необходимо придумать способ совместного использования обработчиков OnDragOver
и OnDrag Drop всеми тремя сетками.
Наверное, это вопрос личного вкуса, но
я предпочитаю, чтобы совместно используемые обработчики событий имели более
внятные имена, чем генерирует Delphi. Я решил назвать их GridDragOver
и GridDragDrop.
Перед тем как следовать дальше, опишу алгоритм
для создания нестандарт ных имен обработчиков1:
-
Дважды щелкните на имени обрабатываемого события
в инспекторе объектов.
-
Delphi автоматически создаст уникальное имя
процедуры, объединив в нем имена компонента и события. Что еще важнее,
при этом автомати чески генерируется список параметров для обработчика
данного
события (я слишком ленив и предпочитаю,
чтобы вместо меня этим занималась среда Delphi).
-
Введите любой текст (например, точку с запятой)
между begin и end
пустой процедуры, созданной Delphi. Это
предотвращает автоматичес кое удаление процедуры при попытке сохранить
файл.
-
Отредактируйте имя процедуры, затем выделите
его двойным щелчком и нажмите Ctrl+C, чтобы скопировать в буфер.
-
Перейдите в начало файла и найдите объявление
исходного обработ чика среди прочих объявлений формы. Выделите исходное
имя и нажмите Ctrl+V, чтобы заменить его новым.
-
В какой-то момент Delphi пожалуется, что исходное
имя (все еще присутствующее в инспекторе объектов) не найдено. Подтвердите
его удаление.
-
Переместите курсор в пустую строку инспектора
объектов. Вставьте в нее новое имя, нажимая Ctrl+V.
-
Сохраните файл.
Исходный текст написанных мной процедур приведен
в листинге 16.1.
Листинг 16.1. Общие
обработчики событий OnDragOver и OnDragDrop
{ Общий обработчик для события OnDragOver
всех сеток. }
procedure TShareEventDemoForm.GridDragOver(Sender,
Source: TObject;
X, Y: Integer; State: TDragState; var Accept:
Boolean);
begin
{ Принимается все, что угодно, но только
из текстового поля. }
Accept := Source is TEdit;
end;
{ Общий обработчик для события OnDragDrop
всех сеток. }
procedure TShareEventDemoForm.GridDragDrop(Sender,
Source : TObject;
X, Y : Integer);
1 Визуальные среды таят в себе
серьезную опасность — люди начинают забывать, что у компьютера кроме мыши
есть еще и клавиатура. Вот и Эйс Брейкпойнт (или Дон Тейлор?), судя по
всему, даже не догадывался, что оставлять создание имен обработчиков на
усмотре ние Delphi вовсе не обязательно. Просто введите желаемое имя в
поле нужного события на вкладке Events и нажмите клавишу Enter.
— Примеч. ред.
begin
{ Сбрасываем перетаскиваемый объект
на текущую сетку. }
DropEditString(CurrentGrid);
end;
{ Вспомогательная процедура для сброса
строки из текстового поля
на указанную сетку. Также очищает
содержимое текстового поля. }
procedure TShareEventDemoForm.DropEditString
(AGrid : TStringGrid);
begin
if AGrid <> nil
then with AGrid do
begin
Cells[0, RowCount - 1] := EditBox.Text;
RowCount := RowCount + 1;
EditBox.Text := '';
end; { with }
end;
{ Возвращает указатель на сетку, расположенную
на текущей вкладке. }
function TShareEventDemoForm.CurrentGrid :
TStringGrid;
begin
Result := nil;
if PageControl.ActivePage = MorningSheet
then Result := MorningGrid else
if PageControl.ActivePage = AfternoonSheet
then Result := AfternoonGrid else
if PageControl.ActivePage = EveningSheet
then Result := EveningGrid;
end;
Процедура OnDragOver выглядит очень
просто. Объект, перетаскиваемый из текстового поля, может быть принят любой
сеткой. Я решил разбить обработку OnDragDrop на две части: главный
обработчик и вспомогательную процедуру. Обработчик ограничивается вызовом
вспомогательной процедуры
с использованием функции, возвращающей
указатель на сетку текущей вкладки1. Здесь проявляется способность
Delphi скрывать работу с указателями, благодаря чему можно выполнить нужные
операции и сохранить «четкость» программы. Получив указатель, процедура
DropEditString
заносит строку
из текстового поля в соответствующую сетку,
добавляет в нее новую строку и стирает содержимое текстового поля.
Для сетки на первой вкладке (MorningGrid)
обработчик делал именно то, что требовалось. Я вернулся к инспектору объектов
и присоединил те же обработ чики к двум другим решеткам — и снова все идеально
работало.
1 ?ешение, использующее для
распознавания нужной сетки параметр Sender, является намного более
элегантным. — Примеч. ред.
Тернистый путь познания
Но мне показалось, что все слишком просто.
Не знаю, в чем тут дело — то ли в каких-то личных качествах, то ли я просто
«нерд» по натуре. Я решил пойти дальше и сделать так, чтобы строку из текстового
поля можно было переслать в любую из сеток, просто сбрасывая ее на корешке
соответствующей вкладки. Пожалуй, сейчас я уже раскаиваюсь в своем решении.
Сначала я узнал, что у компонента TabSet
есть метод, который сообщает номер вкладки по координатам x, y.
Компонент PageControl в основном выполняет функции оболочки для
компонентов TabSheet, так что его собственные
возможности ограничены и он может разве
что сообщить номер текущей выбранной вкладки.
Следовательно, я должен был узнать местонахождение
каждого корешка, чтобы определить, на какой из них указывает мышь. Обладая
этой информа цией, можно легко определить нужную вкладку. Но при этом возникает
другая проблема: компонент PageControl
автоматически изменяет ширину каждого корешка в зависимости от длины его
названия. Что делать?
Я решил организовать поддержку сбрасывания
лишь для тех вкладок,
у которых значение свойств TabHeight
и TabWidth было вручную заменено величиной, отличной от нуля. На
этом следовало остановиться, но я решил предоставить возможность автоматического
назначения корешкам вкладок одной и той же ширины, определяемой длиной
самого длинного названия. В результате программа заметно разрослась, ее
окончательная версия приведена в листинге 16.2.
Листинг 16.2. Полный
исходный текст программы, демонстрирующей
применение общих обработчиков
{——————————————————————————————————————————————————————}
{ Применение общих обработчиков событий }
{ (демонстрационная программа) }
{ SHARMAIN.PAS : Главный модуль }
{ Автор: Эйс Брейкпойнт, N.T.P. }
{ При содействии Дона Тейлора }
{ }
{ Программа демонстрирует применение общих }
{ обработчиков событий в пределах одного приложения }
{ на примере операции перетаскивания. }
{ }
{ Написано для *High Performance Delphi 3
Programming* }
{ Copyright (c) 1997 The Coriolis Group, Inc. }
{ Дата последней редакции 30/4/97 }
{——————————————————————————————————————————————————————}
unit SharMain;
interface
uses
SysUtils, WinTypes, WinProcs, Messages,
Classes, Graphics,
Controls, Forms, Dialogs, Grids, StdCtrls,
ExtCtrls, ComCtrls;
type
TShareEventDemoForm = class(TForm)
EditBox: TEdit;
Label1: TLabel;
QuitBtn: TButton;
Panel1: TPanel;
PageControl: TPageControl;
MorningSheet: TTabSheet;
AfternoonSheet: TTabSheet;
EveningSheet: TTabSheet;
MorningGrid: TStringGrid;
AfternoonGrid: TStringGrid;
EveningGrid: TStringGrid;
procedure FormCreate(Sender: TObject);
procedure QuitBtnClick(Sender: TObject);
procedure EditBoxMouseDown(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure GridMouseDown(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure GridDragOver(Sender, Source:
TObject;
X, Y: Integer; State: TDragState; var
Accept: Boolean);
procedure GridDragDrop(Sender, Source :
TObject;
X, Y : Integer);
procedure PageControlDragOver(Sender, Source:
TObject;
X, Y: Integer; State: TDragState; var Accept:
Boolean);
procedure PageControlDragDrop(Sender, Source:
TObject;
X, Y: Integer);
private
CopyDrag : Boolean;
function ManualTabsSet : Boolean;
function CurrentGrid : TStringGrid;
function TabGrid(X : Integer) : TStringGrid;
procedure SetTabSizes;
procedure DropEditString(AGrid : TStringGrid);
procedure DropGridString(TargetGrid :
TStringGrid);
public
{ Public declarations }
end;
var
ShareEventDemoForm: TShareEventDemoForm;
implementation
{$R *.DFM}
{ Возвращает длину (в пикселях) отображаемой
строки по логическому
номеру окна, в котором она выводится, и
логическому номеру шрифта.
}
function StringWidth(WinHnd : HWND; FntHnd :
HWND; Text : String) : Integer;
var
DCHnd : HWND;
StrSize : TSize;
TextArr : array[0..127] of char;
begin
Result := -1;
DCHnd := GetDC(WinHnd);
if GetMapMode(DCHnd) = MM_TEXT
then begin
SelectObject(DCHnd, FntHnd);
StrPCopy(TextArr, Text);
if GetTextExtentPoint32(DCHnd, @TextArr,
Length(Text),
StrSize)
then Result := StrSize.Cx
end;
ReleaseDC(WinHnd, DCHnd);
end;
{ Возвращает высоту шрифта (в пикселях) по
логическому
номеру окна, в котором он выводится, и
логическому номеру шрифта.
Высота должна учитывать строчные и
подстрочные элементы,
а также внутренний интервал. }
function FontHeight(WinHnd : HWND; FntHnd : HWND)
: Integer;
var
DCHnd : HWND;
TextMex : TTextMetric;
begin
Result := -1;
DCHnd := GetDC(WinHnd);
if GetMapMode(DCHnd) = MM_TEXT
then begin
SelectObject(DCHnd, FntHnd);
GetTextMetrics(DCHnd, TextMex);
Result := TextMex.tmHeight;
end;
ReleaseDC(WinHnd, DCHnd);
end;
procedure TShareEventDemoForm.FormCreate(Sender:
TObject);
begin
PageControl.ActivePage := MorningSheet;
SetTabSizes;
CopyDrag := False;
end;
procedure TShareEventDemoForm.QuitBtnClick(Sender:
TObject);
begin
Close;
end;
procedure TShareEventDemoForm.EditBoxMouseDown
(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
begin
{ Перед тем как начинать перетаскивание,
необходимо убедиться
в том, что нажата левая кнопка мыши, в
текстовом поле
присутствует текст и щелчок был не двойным. }
if (Button = mbLeft)
and (EditBox.Text <> '')
and not (ssDouble in Shift)
then TEdit(Sender).BeginDrag(False);
end;
{ Общий обработчик для события OnMouseDown
всех сеток. }
procedure TShareEventDemoForm.GridMouseDown
(Sender: TObject;
Button: TMouseButton; Shift: TShiftState; X,
Y: Integer);
var
TheGrid : TStringGrid;
begin
{ Инициируем перетаскивание из текущей
выбранной сетки.
Если нажата клавиша Ctrl, устанавливаем флаг
CopyDrag. Перед тем
как начинать перетаскивание, убедимся в том,
что нажата
левая кнопка мыши, в выделенной строке сетки
присутствует
текст щелчок был не двойным. }
TheGrid := CurrentGrid;
CopyDrag := ssCtrl in Shift;
if (Button = mbLeft)
and (TheGrid.Cells[0, TheGrid.Row] <> '')
and not (ssDouble in Shift)
then TStringGrid(Sender).BeginDrag(False);
end;
{ Общий обработчик для события OnDragOver
всех сеток. }
procedure TShareEventDemoForm.GridDragOver(Sender,
Source: TObject;
X, Y: Integer; State: TDragState; var Accept:
Boolean);
begin
{ Принимается все, что угодно, но только из
текстового поля. }
Accept := Source is TEdit;
end;
{ Общий обработчик для события OnDragDrop
всех сеток. }
procedure TShareEventDemoForm.GridDragDrop
(Sender, Source : TObject;
X, Y : Integer);
begin
{ Сбрасываем перетаскиваемый объект на
текущую выбранную решетку. }
DropEditString(CurrentGrid);
end;
procedure TShareEventDemoForm.PageControlDragOver
(Sender, Source: TObject; X,
Y: Integer; State: TDragState; var Accept:
Boolean);
begin
{ Сбрасывание на корешке вкладки принимается
лишь в том случае, если
перетаскиваемый объект происходит из текстового
поля или сетки —
при условии, что корешок относится не к той
сетке, из которой
начато перетаскивание. В любом случае размеры
корешков должны быть
установлены вручную. }
Accept := ManualTabsSet and
(
(Source is TEdit)
or
((Source is TStringGrid) and
(CurrentGrid <> TabGrid(X)))
);
end;
procedure TShareEventDemoForm.PageControlDragDrop
(Sender, Source: TObject; X,
Y: Integer);
begin
{ Получаем строку из нужного источника и
сбрасываем ее на сетку,
связанную со вкладкой в позиции X. }
if (Source is TEdit) then DropEditString
(TabGrid(X));
if (Source is TStringGrid) then DropGridString
(TabGrid(X));
end;
{ Возвращает True лишь в том случае, если и
высота, и ширина вкладки
были заданы вручную. }
function TShareEventDemoForm.ManualTabsSet :
Boolean;
begin
Result := (PageControl.TabHeight > 0) and
(PageControl.TabWidth > 0);
end;
{ Возвращает указатель на сетку, находящуюся
на текущей
вкладке. }
function TShareEventDemoForm.CurrentGrid :
TStringGrid;
begin
Result := nil;
if PageControl.ActivePage = MorningSheet
then Result := MorningGrid else
if PageControl.ActivePage = AfternoonSheet
then Result := AfternoonGrid else
if PageControl.ActivePage = EveningSheet
then Result := EveningGrid;
end;
{ Возвращает указатель на сетку, связанную со
вкладкой
в позиции X. }
function TShareEventDemoForm.TabGrid(X : Integer)
: TStringGrid;
var
Idx : Integer;
begin
Result := nil;
with PageControl do
begin
Idx := X div TabWidth;
case Idx of
0 : Result := MorningGrid;
1 : Result := AfternoonGrid;
2 : Result := EveningGrid;
end; { case }
end; { with }
end;
{ ?егулирует высоту и ширину корешков,
следя за тем,
чтобы все корешки имели одинаковые размеры. }
procedure TShareEventDemoForm.SetTabSizes;
var
i : Integer;
Len : Integer;
MaxWidth : Integer;
s : String;
begin
with PageControl do
begin
if TabWidth > 0
then begin
MaxWidth := -1;
for i := 0 to PageCount - 1 do
begin
s := Pages[i].Caption;
Len := StringWidth(Handle,
Font.Handle, s);
if Len > MaxWidth then
MaxWidth := Len;
end;
if MaxWidth > 0 then TabWidth :=
MaxWidth + 10;
end;
if TabHeight > 0
then PageControl.TabHeight := FontHeight
(Handle, Font.Handle) + 5;
end; { with }
end;
{ Вспомогательная процедура для сброса строки
из текстового поля
на указанную сетку. Также очищает содержимое
текстового поля. }
procedure TShareEventDemoForm.DropEditString
(AGrid : TStringGrid);
begin
if AGrid <> nil
then with AGrid do
begin
Cells[0, RowCount - 1] := EditBox.Text;
RowCount := RowCount + 1;
EditBox.Text := '';
end; { with }
end;
{ Вспомогательная процедура для сброса текста
из выделенной строки
текущей сетки на другую сетку. Если
выполняется операция
перемещения, строка удаляется из текущей сетки,
которая затем
"сжимается". }
procedure TShareEventDemoForm.DropGridString
(TargetGrid : TStringGrid);
var
i : Integer;
begin
if TargetGrid <> nil
then begin
with TargetGrid do
begin
Cells[0, RowCount - 1] :=
CurrentGrid.Cells[0, CurrentGrid.Row];
RowCount := RowCount + 1;
end; { with }
if not CopyDrag
then with CurrentGrid do
begin
Cells[0, Row] := '';
if Row < RowCount - 1
then for i := Row to RowCount - 1 do
Cells[0, i] := Cells[0, i + 1];
RowCount := RowCount - 1;
end; { with }
end;
end;
end.
Для правильного вычисления высоты и ширины
строки, выводимой на корешке, мне пришлось прибегнуть к функциям Win98
API. Попутно я узнал пару интересных вещей. Во-первых, субсвойство Height
свойства Font компонента включает высоту символа (вместе со строчными
и подстрочными элемента ми), но не внутренний интервал (internal leading),
используемый для специальных целей — например отображения диакритических
знаков в некоторых символах национальных алфавитов.
Я захотел узнать настоящую высоту, возвращаемую
при вызове GetText Metrics. Написанная мной функция FontHeight
возвращает высоту по заданным логическим номерам компонента и шрифта. Внутри
FontHeight
я проверяю, что установлен координатный режим MM_TEXT — это означает,
что полученное значение относится к выводу на экран и измеряется в пикселях.
Аналогичная методика используется и во
вспомогательной функции String Width, передающей строку функции
GetTextExtentPoint32.
Возвращаемое значение равно приблизительной длине отображаемой строки (в
пикселях). Значение считается приблизительным, поскольку в нем не учитывается
возможный кернинг, выполняемый для символов шрифта.
Обработчик OnCreate формы вызывает
процедуру SetTabSizes, чтобы узнать, нужно ли изменять размеры корешков.
Если процедура определяет, что в режиме конструирования свойствам TabHeight
и TabWidth компонента PageControl были присвоены ненулевые значения,
она вмешивается в происходящее
и регулирует размеры корешков, учитывая
метрики шрифта и длину самого длинного названия.
По свойству TabWidth и координате
X,
предоставляемой в ходе перетаскива ния, функция TabGrid определяет
нужную вкладку и возвращает указатель на связанную с ней сетку.
PageControlDragDrop
также пользуется TabGrid, чтобы
определить, какая сетка должна получить
сбрасываемую строку.
И последнее замечание…
В листинге 16.2 реализована еще одна дополнительная
возможность, перед которой я не смог устоять. Объект можно перетащить из
сетки и скопировать /переместить его в другую сетку, сбрасывая на нужном
корешке. Для этого мне пришлось написать общий обработчик OnMouseDown
для всех сеток, а также расширить обработчики OnDragOver и OnDragDrop
для компонента PageControl. Кроме того, я добавил флаг CopyDrag,
устанавливаемый в том случае, если в начале перетаскивания из любой сетки
была нажата клавиша Ctrl.
При перетаскивании из сетки на корешок
вкладки основную долю работы выполняет процедура DropGridString.
Если во время перетаскивания не была нажата клавиша Ctrl, DropGridStringвыполняет
дополнительные действия и превращает обычное копирование в перемещение,
убирая выделенный объект из сетки-источника и затем удаляя пустую строку.
?абочая версия программы изображена на
рис. 16.2. Это маленькое приложение получилось довольно забавным. Вы можете
перетаскивать объекты между вкладками, копировать и перемещать их. Это
гораздо веселее, чем сидеть на свадьбе (особенно на своей собственной).
Конец записи (29 марта).
?ис. 16.2. Общие обработчики событий в
действии
Факс в конторе Эйса зажужжал. Хелен немедленно
вскочила на ноги.
— Эйс, пришел факс, — сказала она. — Поторопись,
это должны быть результаты экспертизы.
Брейкпойнт пересек комнату и оторвал листок.
— Посмотрим, кто из нас прав и действительно
ли это дело рук Бохакера.
Он застыл на месте, несколько секунд молча
разглядывая страницу. Наконец Хелен потеряла терпение.
— Ну, что там написано? — потребовала она.
— Это Бохакер, да?
— Видишь ли, не совсем понятно. Такая быстрая
экспертиза не всегда дает однозначный ответ, и…
— Дай посмотреть, — сказала Хелен и отняла
листок. Быстро пробежав его глазами, она повернулась к своему компаньону.
— Здесь ясно написано — цитирую: «Экспертиза
показала практически полное совпадение обоих образцов с погрешностью до
5 процентов, что соответствует погрешности, допустимой при экспертизе такого
рода». Это означает, что образцы крови и волос совпали, не так ли?
— В общем, да, — признал Эйс. — Но…
— Значит, это должен быть Мелвин
Бохакер, как я и говорила. И где бы он ни был, наверняка рядом с ним находится
и Мадам Икс. Остается лишь узнать, где они.
— В Нортон-Сити.
— Что?
— В Нортон-Сити, — повторил Эйс. — Бифф
сообщил мне, что Бохакер уехал в Нортон-Сити. Уж можешь мне поверить.
— Но где именно? — спросила она. — Он может
находиться в сотне мест.
— Кажется, я знаю, как это выяснить, —
сказал Эйс и включил компьютер.
Использование файлов
в памяти
Дневник №16 (1 апреля): Один из самых
частых вопросов о Delphi — как написать приложение, существование которого
в системе ограничивается одним экземпляром. За последний год я обнаружил
несколько решений этой задачи. Одно из них оказалось таким интересным,
что я решил описать его здесь.
Чтобы приложение могло обнаружить факт
существования другого своего экземпляра, оно должно как-то обратиться c
запросом к системным данным. В Windows 3.1 приложение могло узнать о существовании
предыдущего экземпляра по значению hPrevInst, однако в Windows 95 все изменилось.
Один из способов заключается в использовании
модуля WalkStuf, разработанного мной раньше. Функция ModuleSysInstCount
возвращает значение, равное количеству выполняемых копий программы. Приложение
может воспользоваться этой функцией и, если возвращаемое значение отлично
от нуля, просто завершить работу. К сожалению, этот способ не работает
в NT.
Для обмена информацией между приложениями
обычно применяется
уникальный глобальный ключ, доступный для
всех экземпляров программы. Классический пример — использование уникального
файла. При запуске
приложение проверяет, существует ли файл
с заданным именем (например, FOOBAR99.DAT). Если такой файл существует,
значит, в настоящее время уже работает другой экземпляр программы. Если
файл не найден, новый экземпляр программы создает его. Завершая свою работу,
программа удаляет файл.
Одна из проблем подобного подхода связана
с возможными аномалиями (например, «зависанием» системы или сбоем питания).
Поскольку «флаг» (в данном случае — файл) хранится на постоянном носителе,
он сохранится и после перезагрузки. В этом случае первый запущенный экземпляр
программы «увидит» файл, решит, что в системе уже работает другой экземпляр,
и немедленно завершится. В итоге программа вообще перестанет работать.
Вам придется наводить порядок, удалять файл и возвращать систему к нормальному
состоянию.
Win95 предоставляет более приятную альтернативу
— общие файлы в памяти. При этом файл представляет собой временную область
памяти (или по крайней мере трактуется как область памяти, даже
если он временно выгружается на диск). В отличие от многих ресурсов Win95
файлы в памяти могут совместно использоваться несколькими процессами.
Я создал простейшее приложение для проверки
теории о том, что файлы в памяти могут применяться для поиска других экземпляров
программы.
На рис. 16.3 изображено рабочее окно приложения,
а в листинге 16.3 приведен его исходный текст.
s
?ис. 16.3. Программа, запускаемая в единственном
экземпляре
Листинг 16.3. Простейшая программа,
запускаемая лишь в одном экземпляре
{——————————————————————————————————————————————————————}
{ Демонстрационная программа, }
{ запускаемая лишь в одном экземпляре. }
{ INSTMAIN.PAS : Главная форма }
{ Автор: Эйс Брейкпойнт, N.T.P. }
{ При содействии Дона Тейлора }
{ }
{ Программа показывает, как предотвратить запуск }
{ нескольких экземпляров приложения в среде Windows 95.}
{ }
{ Написано для *High Performance Delphi 3 Programming* }
{ Copyright (c) 1998 The Coriolis Group, Inc. }
{ Дата последней редакции 30/4/98 }
{——————————————————————————————————————————————————————}
unit InstMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics,
Controls,
Forms, Dialogs, StdCtrls;
type
TForm1 = class(TForm)
ExitBtn: TButton;
Label1: TLabel;
procedure ExitBtnClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ExitBtnClick(Sender: TObject);
begin
Close;
end;
end.
?азумеется, сама форма ничего не делает.
Каждый последующий экземпляр программы должен обнаруживать присутствие
предыдущего экземпляра и автоматически прекращать работу. И хотя эту ситуацию
можно перехватить в стартовом коде формы, намного разумнее делать так,
чтобы новый экземпляр вообще не отображался на экране. Следовательно, проверка
должна выполняться еще до запуска приложения.
Перед началом
Похоже, многие программисты даже не знают,
что в файл проекта можно поместить код, который будет выполняться еще до
инициализации приложения. Именно это и происходит в данном случае. Файл
проекта для эксперименталь ного приложения приведен в листинге 16.4.
Листинг 16.4. Файл
проекта для программы, запускаемой лишь
в одном экземпляре
{——————————————————————————————————————————————————————}
{ Демонстрационная программа, }
{ запускаемая лишь в одном экземпляре. }
{ ONEINST.DPR : Файл проекта }
{ Автор: Эйс Брейкпойнт, N.T.P. }
{ При содействии Дона Тейлора }
{ }
{ Программа показывает, как предотвратить запуск }
{ нескольких экземпляров приложения в среде Windows 95.}
{ }
{ Написано для *High Performance Delphi 3 Programming* }
{ Copyright (c) 1998 The Coriolis Group, Inc. }
{ Дата последней редакции 30/4/98 }
{——————————————————————————————————————————————————————
program OneInst;
uses
Windows,
Forms,
InstMain in 'InstMain.pas' {Form1};
const
MemFileSize = 1024;
MemFileName = 'one_inst_demo_memfile';
var
MemHnd : HWND;
{$R *.RES}
begin
{ Попытаемся создать файл в памяти }
MemHnd := CreateFileMapping(HWND($FFFFFFFF),
nil,
PAGE_READWRITE,
0,
MemFileSize,
MemFileName);
{ Если файл не существовал ранее, запускаем
приложение... }
if GetLastError <> ERROR_ALREADY_EXISTS
then begin
Application.Initialize;
Application.CreateForm(TForm1, Form1);
Application.Run;
end;
CloseHandle(MemHnd);
end.
Дело обстоит так: прежде всего я пытаюсь
создать объект отображения файла, вызывая функцию API CreateFileMapping.
Независимо от того, существо вал объект ранее или был создан при вызове
функции, его логический номер возвращается и присваивается MemHnd.
При вызове CreateFileMapping с логическим номером $FFFFFFFF
вместо традиционной файловой системы использует ся страничный файл (paging
file) операционной системы, поэтому файл может совместно использоваться
несколькими процессами; все процессы должны лишь знать имя файла. Хотя
файл подготавливается для чтения/записи,
в программу не включен вызов функции MapViewOfFile,
с помощью которой программа получает доступ к содержимому файла через указатель.
В данном примере достаточно проверить, существует ли файл.
Если в момент вызова CreateFileMapping
файл в памяти уже существовал, вызывающая процедура получает его логический
номер, а системе возвращается код ошибки ERROR_ALREADY_EXISTS. Если
функция GetLastError не находит эту ошибку, значит, предыдущего
экземпляра не существует и работу можно продолжать.
Поскольку логический номер возвращается
в любом случае (независимо от того, был создан файл или нет), его необходимо
закрыть перед завершением приложения. Объект файла в памяти создается первой
программой, вызывающей CreateFileMapping; когда логический номер
будет закрыт последней программой, система уничтожит объект. Это равносильно
удалению файла.
Конец записи (1 апреля).
Эйс нажал кнопку Print, и лазерный принтер
ожил, выдав четыре страницы текста. Эйс достал их из лотка и внимательно
просмотрел.
— Теперь все ясно, — решительно сказал
он.
Хелен хотела напомнить о том, что она с
самого начала была права, но вовремя передумала. Кроме того, Эйс уже направлялся
к двери.
— Я пойду с тобой, — сказала она и взяла
плащ с сумочкой.
— Прости, бэби, — ответил Эйс. — Там может
быть опасно, так что ты останешься здесь. Подожди у телефона на случай,
если что-нибудь сорвется.
— Наверное, ты прав, — неохотно признала
она. — Но будь осторожен, милый.
И Хелен нежно поцеловала его.
— Я вернусь через час или два, — произнес
Эйс. — А если не вернусь, вызывай полицию. Скажи им, что я отправился за
Бохакером!
Еще один поцелуй, и он вышел.
Запрет выполнения
программы
Дневник №16, 2 апреля. Итак, я узнал,
как предотвратить выполнение программы при наличии предыдущего экземпляра.
Но что-то продолжало беспокоить меня. А что если приложение должно работать
лишь в том случае, если одновременно с ним работает какая-то другая
программа?
В некоторых программах могут использоваться
демонстрационные версии компонентов — например из VCL-библиотеки Orpheus.
Если приложение создается с использованием того, что TurboPower Software
называет «пробными» (trial) версиями компонентов, то оно сможет работать
лишь одновременно с Delphi IDE. Как это делается?
?ис. 16.4. Программа, обнаруживающая присутствие
Delphi во время работы
Ответ был настолько прост, что я не сразу
в него поверил. На рис. 16.4 показано, как может выглядеть такая программа.
В листинге 16.5 приведен исходный текст главной формы, а в листинге 16.6
— файл проекта.
Листинг 16.5. Исходный
текст главной формы приложения,
обнаруживающего присутствие Delphi
{——————————————————————————————————————————————————————}
{ Демонстрационная программа, }
{ обнаруживающая присутствие Delphi. }
{ NRUNMAIN.PAS : Главная форма }
{ Автор: Эйс Брейкпойнт, N.T.P. }
{ При содействии Дона Тейлора }
{ }
{ Главная форма приложения, работающего лишь при }
{ условии одновременной работы 32-разрядной версии }
{ Delphi. }
{ }
{ Написано для *High Performance Delphi 3 Programming* }
{ Copyright (c) 1997 The Coriolis Group, Inc. }
{ Дата последней редакции 30/4/97 }
{——————————————————————————————————————————————————————}
unit NRunMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics,
Controls, Forms,
Dialogs, StdCtrls, WalkStuf;
type
TForm1 = class(TForm)
ExitBtn: TButton;
Label1: TLabel;
procedure ExitBtnClick(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ExitBtnClick(Sender: TObject);
begin
Close;
end;
end.
Листинг 16.6. Файл
проекта для приложения, обнаруживающего
присутствие Delphi
{——————————————————————————————————————————————————————}
{ Демонстрационная программа, }
{ обнаруживающая присутствие Delphi. }
{ NORUN.DPR : Главная форма }
{ Автор: Эйс Брейкпойнт, N.T.P. }
{ При содействии Дона Тейлора }
{ }
{ Приложение, работающее лишь при условии }
{ одновременной работы 32-разрядной версии Delphi. }
{ }
{ Написано для *High Performance Delphi 3 Programming* }
{ Copyright (c) 1997 The Coriolis Group, Inc. }
{ Дата последней редакции 30/4/97 }
{——————————————————————————————————————————————————————}
program NoRun;
uses
Forms, Dialogs,
NRunMain in 'NRunMain.pas' {Form1},
WalkStuf in 'WalkStuf.pas';
{$R *.RES}
begin
Application.Initialize;
{ Если не существует работающего экземпляра
32-разрядной
версии Delphi, вывести сообщение об ошибке и
завершить работу
программы. Если все хорошо, продолжить
выполнение. }
if ModuleSysInstCount('DELPHI32.EXE') < 1
then MessageDlg('Delphi 32 must be running to
execute this program',
mtError, [mbOK], 0)
else begin
Application.CreateForm(TForm1, Form1);
Application.Run;
end;
end.
Основная идея — уничтожить приложение еще
до того, как пользователь увидит главную форму. Для решения этой задачи
я снова включил код непосредственно в файл проекта. На этот раз функция
ModuleSysInstCount
из модуля WalkStuf проверяет, работает ли в системе по меньшей мере один
экземпляр 32-разрядной версии Delphi (DELPHI32.EXE). Если проверка
дает положительный результат, программа продолжает работу, если нет — выводится
сообщение об ошибке.
Небольшое замечание: поскольку в модуле
WalkStuf используется Tool Help32, описанная методика будет работать лишь
в Win9x.
Конец записи (2 апреля).
Плавающие панели
инструментов
Дневник №16 (3 апреля): Я всегда любил
программы с панелями инструментов, свободно перемещаемыми по экрану. Такие
панели особенно удобны
в графических редакторах и программах компьютерной
верстки, так как палитру с необходимыми инструментами можно расположить
вблизи от того места, над которым вы работаете.
В поисках основы для «плавающей» панели
инструментов я перебрал различные компоненты, поставляемые вместе с Delphi.
Наверное, можно было бы воспользоваться дополнительной формой, но я не
стремился к экзотическим решениям. Меня вполне устроило бы нечто, перемещаемое
в пределах клиентской области главной формы.
Обычный компонент TPanel прекрасно
подходил на эту роль, за исключением одного: панели нельзя перемещать во
время выполнения. Однако небольшое исследование показало, что они способны
обрабатывать события мыши. После нескольких неудачных попыток у меня получилась
демонстрационная программа, приведенная в листинге 16.7.
Листинг 16.7. Исходный текст программы
с плавающей панелью инструментов
{——————————————————————————————————————————————————————}
{ Демонстрационная программа }
{ для работы с плавающими панелями инструментов. }
{ TOOLMAIN.PAS : Главная форма }
{ Автор: Эйс Брейкпойнт, N.T.P. }
{ При содействии Дона Тейлора }
{ }
{ Приложение, демонстрирующее возможность применения }
{ перемещаемых объектов TPanel в качестве плавающих }
{ панелей инструментов. }
{ }
{ Написано для *High Performance Delphi 3
Programming* }
{ Copyright (c) 1998 The Coriolis Group, Inc. }
{ Дата последней редакции 30/4/98 }
{——————————————————————————————————————————————————————}
unit ToolMain;
interface
uses
Windows, Messages, SysUtils, Classes, Graphics,
Controls, Forms,
Dialogs, StdCtrls, FileCtrl, ExtCtrls, Buttons;
type
TDirection = (otHorizontal, otVertical);
TForm1 = class(TForm)
Toolbar: TPanel;
ExitSB: TSpeedButton;
ZoomInSB: TSpeedButton;
ZoomOutSB: TSpeedButton;
ControlPanel: TPanel;
GranRBGroup: TRadioGroup;
MarginRBGroup: TRadioGroup;
OrientRBGroup: TRadioGroup;
ExitBtn: TButton;
LEDSB: TSpeedButton;
procedure ExitBtnClick(Sender: TObject);
procedure ToolbarMouseDown(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure ToolbarMouseUp(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
procedure ToolbarMouseMove(Sender: TObject;
Shift: TShiftState;
X, Y: Integer);
procedure FormCreate(Sender: TObject);
procedure GranRBGroupClick(Sender: TObject);
procedure MarginRBGroupClick(Sender: TObject);
procedure ExitSBClick(Sender: TObject);
procedure OrientRBGroupClick(Sender: TObject);
private
DraggingPanel : Boolean;
DragStartX : Integer;
DragStartY : Integer;
GridSize : Integer;
MarginSize : Integer;
procedure OrientToolBar(Direction :
TDirection);
public
{ Public declarations }
end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.ExitBtnClick(Sender: TObject);
begin
Close;
end;
procedure TForm1.ToolbarMouseDown(Sender:
TObject; Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
if Button = mbLeft
then begin
DraggingPanel := True;
DragStartX := X;
DragStartY := Y;
end;
end;
procedure TForm1.ToolbarMouseUp(Sender: TObject;
Button: TMouseButton;
Shift: TShiftState; X, Y: Integer);
begin
DraggingPanel := False;
end;
procedure TForm1.ToolbarMouseMove(Sender: TObject;
Shift: TShiftState; X,
Y: Integer);
var
DeltaX : Integer;
DeltaY : Integer;
SafetyMargin : Integer;
begin
if DraggingPanel
then with Toolbar do
begin
DeltaX := X - DragStartX;
DeltaY := Y - DragStartY;
if GridSize > MarginSize
then SafetyMargin := GridSize
else SafetyMargin := MarginSize;
if (abs(DeltaX) > GridSize - 1)
then if DeltaX > 0
then begin
if (ControlPanel.Left - Left) > SafetyMargin
then Left := Left + DeltaX
else Left := ControlPanel.Left - SafetyMargin;
end
else begin
if (Left + Width) > SafetyMargin
then Left := Left + DeltaX
else Left := SafetyMargin - Width;
end;
if (abs(DeltaY) > GridSize - 1)
then if DeltaY > 0
then begin
if (Form1.ClientHeight - Top) > SafetyMargin
then Top := Top + DeltaY
else Top := Form1.ClientHeight - SafetyMargin;
end
else begin
if Top + Height > SafetyMargin
then Top := Top + DeltaY
else Top := SafetyMargin - Height;
end;
end; { with }
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
GranRBGroup.ItemIndex := 0;
MarginRBGroup.ItemIndex :=0;
OrientRBGroup.ItemIndex := 0;
end;
procedure TForm1.GranRBGroupClick(Sender: TObject);
begin
case GranRBGroup.ItemIndex of
0 : GridSize := 1;
1 : GridSize := 10;
2 : GridSize := 20;
end; { case }
end;
procedure TForm1.MarginRBGroupClick(Sender:
TObject);
begin
case MarginRBGroup.ItemIndex of
0 : MarginSize := 5;
1 : MarginSize := 10;
2 : MarginSize := 15;
end; { case }
end;
procedure TForm1.ExitSBClick(Sender: TObject);
begin
Close;
end;
procedure TForm1.OrientRBGroupClick(Sender:
TObject);
begin
case OrientRBGroup.ItemIndex of
0 : OrientToolBar(otHorizontal);
1 : OrientToolBar(otVertical);
end; { case }
end;
procedure TForm1.OrientToolbar(Direction :
TDirection);
begin
with Toolbar do
begin
Left := 20;
Top := 20;
case Direction of
otHorizontal :
begin
Width := (4 * ExitSB.Width) + 20;;
Height := ExitSB.Height + 10;
ExitSB.Top := 6;
ZoomInSB.Top := 6;
ZoomOutSB.Top := 6;
LEDSB.Top := 6;
ExitSB.Left := 11;
ZoomInSB.Left := ExitSB.Left + ExitSB.Width;
ZoomOutSB.Left := ZoomInSB.Left +
ZoomInSB.Width;
LEDSB.Left := ZoomOutSB.Left +
ZoomOutSB.Width;
end;
otVertical :
begin
Width := ExitSB.Width + 10;
Height := (4 * ExitSB.Height) + 20;
ExitSB.Left := 6;
ZoomInSB.Left := 6;
ZoomOutSB.Left := 6;
LEDSB.Left := 6;
ExitSB.Top := 11;
ZoomInSB.Top := ExitSB.Top + ExitSB.Height;
ZoomOutSB.Top := ZoomInSB.Top +
ZoomInSB.Height;
LEDSB.Top := ZoomOutSB.Top +
ZoomOutSB.Height;
end;
end; { case }
end; { with }
end;
end.
Как видно из листинга, панель должна обрабатывать
три события мыши — OnMouseDown, OnMouseMove и OnMouseUp.
Обработчик OnMouseDown проверяет, была ли нажата левая кнопка мыши.
Если это так, он запоминает исходное положение курсора и устанавливает
флаг статуса в состояние, которое обозначает перетаскивание.
Обработчик OnMouseMove выглядит
сложнее — в основном потому, что ему приходится следить, чтобы панель не
вышла за пределы клиентской области и не потерялась из вида. Обработчик
ToolbarMouseMove
вычисляет разность между исходным и текущим положениями мыши и прибавляет
ее к первоначаль ным значениям свойств Left и
Top панели,
чтобы переместить ее в новое место. Я предусмотрел возможность перемещения
панели с шагом в 1, 10 или 20 пикселей. Внешне это выглядит похожим на
перемещение компонентов в режиме конструирования Delphi при включенной
привязке к сетке. Кроме того, я позаботился о том, чтобы участок панели
всегда можно было захватить мышью, даже если пользователь по неосторожности
уведет ее слишком далеко.
Обработчик OnMouseUp выглядит тривиально;
все, что от него требуется — сбросить флаг статуса.
?ис. 16.5. Плавающая панель инструментов
Я поместил на форму три переключателя,
чтобы дискретность перемеще ний и размеры полей можно было менять на ходу.
Кроме того, я предусмотрел возможность перехода от горизонтальной ориентации
к вертикальной, и наоборот.
На рис. 16.5 показано, как выглядит эта
программа во время выполнения. Перетаскивать панель оказывается довольно
занятно, к тому же первая кнопка на ней выполняет полезную функцию — завершает
работу программы.
Конец записи (3 апреля).
Эйс выходит победителем
Дельфийский Мститель оторвался от Дневника.
Вокруг явно творилось что-то непонятное. Температура воздуха упала, комната
подернулась дымкой, а по полу стелился толстый слой плотного тумана. Яркий
белый свет едва проникал сквозь него, отчего все происходящее выглядело
очень странно. Вдруг раздался мощный удар, трухлявая дверь слетела с петель
и грохнулась на пол. В открытом дверном проеме стоял торжествующий Эйс
Брейкпойнт!
* * *
Я шагнул внутрь убогой комнаты, зная, что
не только раскрыл дело, но и полностью отнял у Третьего Лица роль повествователя.
Всего три прыжка отделяли меня от жалкого существа, прижавшегося к стене
напротив — побежденного противника, который осмелился назвать себя Дельфийским
Мстителем.
— Брейкпойнт! — прошипел Бохакер и уронил
мой Дневник на пол. — Откуда ты узнал?
— Легко, — ухмыльнулся я. — С самого начала
было ясно, что здесь что-то нечисто. Просто я не сразу догадался, что имею
дело с вами…мисс Бохакер! — закончил я и сорвал фальшивые усы с ее верхней
губы.
— Черт! — выдавила она сквозь стиснутые
зубы. Ее огромные глаза, как у загнанного в угол животного, мерили меня
с головы до ног. Выражение отчаяния полностью исчезло, и на его месте появилась
кривая ухмылка, которая мне очень не понравилась.
— Да, это я — Мевлин, сестра-близнец Мелвина
Бохакера.
По моей спине пробежал холодок. Внезапно
я почувствовал себя так, будто меня заставили участвовать в дешевом и очень
скверном фильме категории «Б». Как мне удается попадать в такие ситуации?
Я покачал головой.
— Конечно, — заметила она презрительно.
— Можешь разыгрывать из себя героя. Но хоть на секунду представь себе,
что это такое — с самого детства вечно идти по следам Мелвина Бохакера.
— А заодно и следовать его привычкам в
еде, — подумал я, разглядывая многочисленные пакеты от чипсов и сухих завтраков.
Но как ни странно, я почувствовал что-то вроде сочувствия — похоже, она
собиралась излить душу.
— Мелвин был старше меня ровно на 8 минут,
— сказала она, — Поэтому ему всегда доставалось все лучшее. Когда настало
время учебы в колледже, у наших родителей хватило денег лишь на одного
из нас. Конечно, послали Мел вина, а я осталась дома. Наверное, мне пришлось
бы умереть с голоду, если бы я не занялась дизайном одежды.
— Секунду, — перебил я. — Так это вы заправляете
фирмой «Бохакер Индастриз»? Той, что выпускает джинсы?
— А ты не сообразил? — спросила она лукаво.
— Тогда ты, видимо, и не знаешь, что моя компания производит кое-что и
для коллекции твоей подружки, Маффи Катц.
— Наверное, вы стоите целую кучу денег!
— воскликнул я.
— О, да, на моем счету несколько миллионов.
Наверное, этого достаточно, если не интересоваться ничем иным. Однако вся
любовь
всегда доставалась Мелвину. Это он стал посвященным и получил образование
в колледже. Это он стал одним из самых уважаемых и почитаемых членов всего
научного сообщества — программистом для Windows 98.
Ее глаза загорелись ненавистью.
— С самого детства я была никчемной, зависимой
жертвой. Я поклялась отомстить Мелвину, чего бы это ни стоило. Всего несколько
недель назад у меня созрел план, — произнесла она, мечтательно глядя вдаль.
— Я решила украсть твой Дневник, вооружиться хранящимися в нем секретами
и стать лучшим в мире программистом для Windows — лучше Мелвина. И, разумеется,
лучше тебя, — прибавила она, указывая мне в грудь длинным, изящным пальцем.
— Не надо тыкать в меня пальцем, — предупредил
я, — вы можете меня оцарапать.
— А еще приятнее было то, — продолжала
она, — что в краже должны были обвинить Мелвина. Я позвонила ему сегодня
утром, чтобы подразнить и выманить из города, так что все решили бы, что
он ударился в бега. Это был великолепный план. Как жаль, что он не удался.
Очевидно, я недооценила тебя.
— Вы забыли, что я был сыщиком, — ответил
я. — И оставили такое количество улик, что даже последний «чайник» из Бейпорта
смог бы обо всем догадаться.
— Перчатка? — спросила она. — я случайно
обронила ее и даже думала о том, чтобы вернуться. Но перчатка должна была
указать на Мелвина, а не на меня.
— Несомненно. В грязной перчатке, оставшейся
на месте преступления, я нашел пару волосков, ДНК которых почти полностью
совпала с ДНК Мелвина. Но в этот момент я уже знал, что это — ловушка,
хотя и очень хитроумная. Для такого заключения были две причины. Видите
ли, после того удара, который он получил от меня два года назад, Мелвину
никогда бы не хватило смелости снова устроить что-нибудь против меня. Я
отбил у него охоту.
— А вторая причина? — поинтересовалась
она.
— Вряд ли ваш брат стал бы носить женские
перчатки, — усмехнулся я. — Они оказались бы ему слишком малы. Поэтому
я вышел на компьютер Бюро лицензий и получил копию его водительского удостоверения.
У меня появилась дата рождения. Я знал, что он родился в Калифорнии. Поэтому
следующим делом я подключился к большой базе данных и выполнил поиск по
всем округам Калифорнии.
Она поняла с полуслова.
— Значит, ты нашел наши свидетельства о
рождении. — Да. Затем я узнал, что в ту ночь у Честера и Марты Бохакер
родились два ребенка — мальчик Мелвин и девочка Мевлин, появившаяся
на свет несколькими минутами позже. Близнецы. Вот почему образцы ДНК почти
полностью совпали.
— Понятно. Значит, ты узнал о моем существовании.
Как же ты выследил меня в этой дыре?
— Я предположил, что для реализации своего
плана вы оставались в городе не меньше двух недель. В единственном городском
мотеле не было зарегистрировано ни одного нового постояльца, поэтому я
просмотрел данные местной телефонной компании и поискал новых клиентов,
подключенных за последнюю пару недель. Это маленький город, и очень скоро
поиски привели меня сюда. Еще один заход в Калифорнию — и я получил технические
данные вашей машины. Они подходили к той машине, которую я видел у своей
конторы в тот вечер. Тогда я понял, что добрался до разгадки. Дело оказалось
простым и банальным.
— Простым, возможно. Но банальным — нет,
не думаю, — сказала Мевлин. Голос прозвучал неожиданно мягко и спокойно.
Это внезапное изменение привлекло мое внимание. Ее глаза горели не ненавистью,
а холодным огнем. Она сняла шляпу и лениво швырнула ее через всю комнату,
потом беспечно встряхнула головой. Шелковистые черные локоны рассыпались
по плечам. Я подумал, что с этой женщиной нужно быть осторожным.
Где-то за стеной заиграл саксофон. Мевлин
подняла с пола мой Дневник.
— Послушай, Эйс, — сказала она, посылая
мне кокетливую улыбку. — Может, мы просто не с того начали? Например, ты
бы мог оставить эту книжку мне, и тогда мы бы стали очень близкими друзьями.
Я заметил, как она поглаживает мягкую кожу
на переплете Дневника, словно это была любимая собачка. Ситуация с каждым
моментом становилась все горячее. Я старался отодвинуться от нее подальше.
К одному саксофону присоединились еще три, и теперь они играли так громко,
словно находились в этой комнате. «В этих дешевых номерах слишком тонкие
стены», — подумалось мне.
— Боюсь, ничего не получится, — сказал
я и сделал шаг назад. — У меня уже есть один очень близкий друг. Ее зовут
Хелен.
— Знаю, — ответила она, неуклонно приближаясь
ко мне. — Я ее видела. Славная девочка, провинциальный цветочек с милой
мордашкой и хорошей фигурой. Но скажи мне, — и ее рот заранее искривился
торжествующей улыбкой, — есть ли у нее что-нибудь подобное?
Не выпуская из рук Дневника, она одним
быстрым движением распахнула полы плаща. Я отшатнулся назад. Мои вытаращенные
глаза были прикованы к тому, что виднелось в распахнутом плаще: из внутреннего
кармана торчали два билета в первый ряд на концерт «?оллинг Стоунз» в Сиэтле.
Она проворно выдернула билеты и вложила их мне в руку.
— Я рада, что могу отдать их тебе.
— Откуда… Как тебе удалось их достать?
— потрясенно спросил я, тщетно пытаясь обрести душевное равновесие.
— Деньги все могут, милый, — ответила она.
— Ну же, бери, они твои.
Казалось, я целый час не мог оторвать взгляда
от билетов, торчащих в моем кулаке.
— Я… извините, я не беру взяток, — сказал
я и неохотно бросил билеты на пол. — У меня нет выбора. Я должен отвести
вас в полицию.
— По крайней мере стоило попытаться, —
довольно спокойно заметила она.— Эйс, а что бы ты сказал насчет… маленького
поцелуя? Чтобы доказать, что ты на меня не обижаешься?
С дьявольски обворожительной улыбкой она
прижалась ко мне и обняла, не выпуская, однако, из правой руки Дневника.
— Чего… поцелуя? — тупо спросил я в надежде
выгадать хоть немного времени и изобрести какой-нибудь план. К саксофонам
присоединился еще один, и аккомпанементом к ним зазвучал барабан, гулко
отдававшийся у меня в груди.
Лед растаял, и ее серые глаза приблизились
к моим.
— Ну конечно, глупый, — игриво ответила
она. — Надеюсь, ты знаешь, что это такое? Это когда двое людей соприкасаются
губами, а потом прижимают их… вот… так…
До этого момента я не следил за ее ртом.
Но когда ее влажные алые губы приблизились к моим и я почувствовал сладкий
запах ее дыхания, все остальные мысли куда-то пропали. Я словно застыл
в каком-то трансе рядом с оркестром, игравшим лучшие вещи Барри Манилоу.
Что я мог сделать? И как же Хелен? ?оскошные, трепещущие губы приближались…
Сзади раздалось приглушенное хлоп! —
и в моей голове что-то взорвалось. Мир из TrueColor стал черно-белым, а
потом начал медленно расплываться оттенками серого. Губы Мевлин искривились
в маниакальном смехе, который отражался от всех стен. Я снова уловил запах
ее дорогих духов. Теперь он заполнил мои ноздри и нестерпимо горел в мозгу.
— Chez Monieux, — полушепотом произнес
я. Мои колени стали ватными, а потом все провалилось в черноту.
* * *
Когда я пришел в себя, голова раскалывалась,
а во рту был противный горький привкус. Я с трудом поднялся на ноги и посмотрел
на часы — 19:34. Поблизости не было видно ни Мевлин, ни Дневника, но внизу
доносилось характерное тарахтение мотора — ее неповоротливый белый «Бронко»
отъезжал со стоянки.
На столе лежал конверт лавандового цвета,
а рядом с ним — записка, написанная знакомым женским почерком. Я взял ее,
но слова расплывались перед глазами, и только через несколько секунд мне
удалось сфокусировать взгляд.
«Дорогой Эйс!
В конверте ты найдешь небольшой подарок
от меня.
Наверное, у нас действительно могло что-нибудь
получиться. Но мне пришлось сделать то, что было неизбежным. Возможно,
мы еще увидимся в будущем.
Наверное, нельзя всегда получать то, что
хочешь. Но если постараться, можно получить то, что тебе очень нужно.
Всегда твоя,
Мевлин»
Я вскрыл надушенный конверт и вытряхнул
на стол его содержимое — полоску белой бумаги с перфорацией вдоль края.
Я подобрал билет и посмотрел на него, вспоминая
всю эту безумную сцену. Сначала меня поразила ее красота, оставившая глубокий
след в моей душе. Затем меня поразил мой собственный Дневник, оставивший
порядочную шишку на затылке. Шишка со временем пропадет, а пока нужно привести
в порядок кое-какие дела.
Наверное, когда мои сентиментальные друзья
услышат об этом происшествии, они посоветуют мне понять Мевлин и простить
ту боль, которую она причинила. Возможно, когда-нибудь у меня это получится.
Но еще довольно долго я не смогу доверять женщинам с кривой усмешкой и
маниакальным смехом.
Эпилог
Дневник №17, 13 апреля. За последние
24 часа произошло множество событий. Я пережил приключение, которое ни
за что не хотел бы повторить.
Хелен ожидала моего возвращения. По крайней
мере отчасти она была права — вор действительно был из семьи Бохакеров.
Впрочем, я тоже был прав — сам Бохакер никогда не рискнул бы пойти против
меня.
Часы показывали 23:39. Я налил чашку кофе
и сделал большой глоток. Теперь я знал, каково это — видеть свою контору
вскрытой, а имущество украденным. Мне бы не хотелось снова испытать нечто
подобное. Мой папа, Джек Брейкпойнт, всегда говорил мне: «Береги свои пожитки,
сынок. Всегда найдется кто-то, кому они нужны больше, чем тебе». А мама
добавляла: «И всегда надевай лучшее белье — на случай, если ты попадешь
под автобус».
В одном можно не сомневаться: мои Дневники
теперь не будут валяться где попало. Вероятно, я никогда не увижу украденный
Дневник. К счастью, он был только одним из целой серии. Кто-то говорил
мне, что лучшие выдержки из него были включены в новую книгу по программированию
на Delphi.
Перед уходом Хелен долго говорила со мной
об Авторе. Она убедила меня, что после всего, что Автор сделал для меня,
мне следовало бы чаще общаться с ним и рассказывать о нем другим. С учетом
всех обстоятельств я обязан сделать хотя бы это.
Вопреки здравому смыслу на пути домой я
все же купил последний компакт -диск группы «Крыша поехала». Он называется
«Трансцендентальная медитация». Кнопку «стоп» пришлось нажать уже на середине
первой композиции — «Сполосни и сплюнь». На мой взгляд, слишком напоминает
кабинет стоматолога.
Я сидел на кухне и отхлебывал горькую коричневую
жидкость. Снаружи доносился шум дождя, барабанившего по окну. Я достал
бумажник, а из него — билет, найденный в конверте. По какой-то причине
он не выходил у меня из головы.
Наверное, мне стоит пойти на этот
концерт. Это позволит хорошо отдохнуть от суетной жизни этого маленького
городка. К тому же я люблю такую музыку. Возможно, это даже станет началом
нового приключения.
Ведь никогда не известно заранее, кто окажется
в соседнем кресле.
Конец записи (13 апреля).
|