Пропавший оракул 

Дон Тейлор

Эйс Брейкпойнт вернулся… но его дневник пропал, скорее всего — был похищен. Эйс начинает охоту за Таинственным незнакомцем, который в свою очередь охотится за тайнами Delphi. Интрига закручивается!

Путешествие, которое вам предстоит, многие назовут странным. Наверное, я первым соглашусь с ними. 

Прошло лишь три года с той поры, когда на сцене появились Delphi и Эйс Брейкпойнт. Как известно, среда Delphi была создана на редкость талантли вой командой разработчиков из Borland International. Эйс был создан… в общем, по необходимости. 

При изучении Delphi мне стало ясно, что никакое количество слов не сможет передать все аспекты этого невероятного продукта. Нельзя научиться программировать на Delphi, просто прочитав описание — Delphi нужно прежде всего исследоват ь. И эта доля выпала мне — написать путеводитель, возглавить экспедицию и привести подробный пример использования Delphi в реальных ситуациях. Не говоря уже о том, чтобы удерживать внимание читателей на протяжении 200 с лишним страниц! 

У меня возникла идея — написать приключенческий сюжет, проходящий через весь учебник, и представить в нем одного из самых необычных консультантов в области программирования всех времен. Я выложил идею Джеффу Дантеманну и затаил дыхание. На всякий случай поясню, что Джефф  — не только великолепный специалист; в душе он авантюрист и любитель приключений. Он «дал добро», и я создал Эйса Брейкпойнта, крутого частного сыщика, переквалифицировавшегося в программисты… 

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

Не падая духом, Эйс решил круто изменить свою карьеру. На сей раз он выбрал профессию, которая наверняка прокормит его, — он стал программи ровать для Windows. Но Эйсу хотелось быть не просто современным профессионалом, а настоящим «человеком 90-х годов». Эйс переехал в Пулсбо, штат Вашингтон, и в течение двух долгих лет посещал вечерние курсы по программированию. Закончив их, он быстро арендовал контору и повесил вывеску. 

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

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

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

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

Эйс Брейкпойнт и… 

«Дело о пропавшем Дневнике»! 

Вечером в конторе 

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

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

Меня ждала всего одна записка от моей мамы, Куини Брейкпойнт: «Не забудь позвонить сестре и поздравить ее с днем рожденья, обязательно пригласи всех друзей на вечеринку, посвященную годовщине твоего последнего приключения…» 

Я тут же набросал e-mail с приглашением, забросил его в список рассылки «друзья» и задумался. Хотя то приключение состоялось всего два года назад, казалось, что прошла целая вечность. Все начиналось достаточно невинно — мы с Мелвином Бохакером соревновались за очередной контракт. Бохакер — высокий парень с длинной шеей, любитель гамбургеров и программирования на C/C++, знавший великое множество различных библиотек. До той поры ему удавалось перехватывать у меня почти всех клиентов. 

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

Мы познакомились в колледже, и наша компания стала неразлучной. Мои самые близкие друзья даже оказались в некоторой степени участниками тех событий. Громила Бакендорф-Рабинович (бывший профессиональный футболист) попал тогда на психологическое обследование. Бифф Мэрфи, специалист по деловой этике, опасался за мою жизнь. Маффи Катц, профессиональный психиатр и по совместительству маникюрша, тоже опасалась… честно говоря, она больше всего опасалась сломать ноготь клиенту. 

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

Я невольно усмехнулся. Много воды утекло за прошедший год. Теперь я почти полностью перешел на Delphi 4, моя консультационная практика постоянно расширялась. Бифф все еще стоял на раздаче в «Норвежских жареных цыплятах Бака МакГаука». Хелен работала менеджером в местном магазине. Маффи бросила работу и занялась созданием модной одежды и украшений. История Громилы закончилась трагично. Ему поставили диагноз «мания величия» и поместили в государственное лечебное учреждение (а проще
говоря — в психушку). Через два месяца интенсивного труда он научился настраивать гитару. Однажды ночью он вместе с четырьмя другими обитателя ми этого заведения сбежал и организовал гранж-группу «Крыша поехала». Их первый компакт-диск стал платиновым. На этой неделе должен выйти второй (не собираюсь покупать ни тот, ни другой — даже дружба имеет свои пределы). 

Мольба о помощи 

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

— Брейкпойнт. Чем могу помочь? 

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

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

— Помедленнее, — сказал я. — Успокойтесь и подробно объясните, в чем дело. 

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

— Ладно, не волнуйтесь, — сказал я уверенным тоном. — Вы можете описать этого типа? У него есть борода или усы? 

— Не могу сказать, — ответила она. — Его лицо закрыто нейлоновым чулком. 

— Со швом или без шва? — поинтересовался я. 

— Пожалуй, без шва. 

— Какого оттенка? 

— Телесного. Нет, нет — скорее, песочного. О, я не знаю! Послушайте, мистер Брейкпойн т, — умоляла она. — Вы — моя единственная надежда. Он может появиться в каждую секунду. Приезжайте прямо сейчас. 

— Держитесь, — сказал я. — Где вы находитесь? 

— В телефонной будке на автостраде, возле «Эспрессо-бара Оле» и шинного магазина. Мне нужна ваша помощь… 

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

Часы показывали 22:30. Я швырнул Дневник на стол, схватил ключи, накинул плащ и помчался к машине. Проливной дождь смешался с наползающим туманом, таким густым, что я с трудом нашел свою машину, припаркованную в каких-то двадцати метрах от дома. Даже мощные лучи фар с трудом проникали сквозь водно-туманную завесу. Кусты, посаженные вокруг конторы, напоминали людей, стоящих в очереди на автобус. Я выехал со стоянки и кое-как выбрался на улицу, а затем выжал акселератор до упора. 

Пропажа 

В 22:57 я вернулся на стоянку возле конторы. Грязь стала настолько густой, что машина скользила по ней, будто слон на коньках. Она остановилась лишь тогда, когда колеса уперлись в поребрик рядом с парковкой номер 132. 

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

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

И тут мое сердце замерло.

Дверь в контору была широко открыта. Пробегая несколько оставшихся шагов, я совершенно точно вспомнил, что, уходя, запер дверь. Тем не менее сейчас она была открыта, а внутри горел свет. Я переступил через порог, молниеносно обшаривая глазами комнату. Ничего не пропало, подумал я. Впрочем, нет — Мьюникса не было видно, но он, наверное, просто отправился погулять через открытую дверь. Тут мой взгляд упал на стол, и у меня перехватило дыхание: пропал Дневник! 

Я быстро подошел к столу, переворошил бумаги, обшарил все ящики и в полном отчаянии осмотрел пол. Дневника нигде не было. 

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

Мое внимание было настолько занято поисками Дневника, что я не заметил, как в комнату вползло темное облако смога. Сейчас оно покрывало весь пол на уровне чуть выше моих промокших ботинок. В комнате стало темно, а единственное освещение, казалось, идет сквозь туман на полу. Тут в дверном проеме появилась загадочная фигура, притянувшая мой взгляд подобно магниту. Это было призрачное существо; определенно человек, но с первого взгляда не скажешь, мужчина или женщина. Существо заметило мой взгляд и заговорило звучным голосом телеведущего (причем его слова отдавались гулким эхом, что отнюдь меня не успокаивало). 

C возвращением, мистер Брейкпойн т. Вас ждали.

— Погодите минутку, — потребовал я. — Кто вы… или что? 

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

Я снял шляпу и почесал макушку. — Повествователя? 

Третье Лицо.

— Политически выдержанный, современный вариант Гарри Лайма, знаменитого детектива по прозвищу «Третий человек»? — спросил я. 

Нет. Просто Третье Лицо. То, что проходили на уроках литературы в колледже. Припоминаете?

— В общих чертах, — ответил я. — Я не знаю вашего имени, мистер… или миссис… Послушайте, а вы мужчина или женщина? 

У Третьих Лиц не бывает имени. Третье Лицо — всего лишь литературный прием, и пола у него быть не может. По этой же причине Третье Лицо вообще не обладает собственной жизнью.

— Понятно, — заметил я. — Но ответьте мне на такой вопрос: почему эту историю теперь рассказываете вы, а не я? 

Это временное явление. К концу этой истории контроль над происходящим снова вернется к вам. А пока описывать ход событий — дело повествователя. Представьте, будто вы смотрите по телевизору очередную серию «Меня зовут Коломбо».

— Не знаю, понравится ли мне это. 

На другом краю города 

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

— Значит, это он и есть? — в голосе сквозило явное предвкушение удовольствия. — Ну-ка посмотрим…. 

Загадочная фигура уселась за стол, перевернула первую страницу и принялась читать выдержки из дневника Эйса Брейкпойнта. 

Классическое перетаскивание 

Дневник №16, 19 марта. То, что выглядит самым простым, порой оказывается очень сложным. С другой стороны, иногда бывает и наоборот. По крайней мере это справедливо для внутренних 1 операций перетаскивания в приложениях Delphi. 

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

Оказалось, любая операция перетаскивания начинается с четырех предвари тельных действий: 

  1. Инициализация метода BeginDrag исходного компонента (источника), выполняемая при обработке событий мыши, происходящих в зоне этого компонента. 
  2. Создание обработчика события OnDragOver компонента-приемника, чтобы указать, в каком месте допускается сбрасывание перетаскиваемого объекта. 
  3. Создание обработчика события OnDragDrop компонента-приемника, чтобы определить, какие действия должны выполняться при сбрасывании перетаскиваемого объекта. 
  4. Создание обработчика события OnDragEnd компонента-источника. Если три предыдущих шага необходимы для любой операции перетаскивания, последний шаг выполняется лишь в том случае, если при завершении перетаскивания в исходном компоненте необходимо «убрать мусор» (это событие происходит даже при отмене перетаскивания).

Перетаскивание: как это делается 

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

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

Примечание

Компонент OvcCalendar вместе с остальными компонентами семейства Orpheus (специаль ная пробная версия) находится в каталоге \ORPHEUS прилагаемого CD-ROM. Последнюю пробную версию пакета всегда можно получить на Web-узле TurboPower по адресу http://www.turbopower.com. 

Для своего расследования я создал приложение с единственной формой, на которой находятся текстовое поле, строковая сетка TStringGrid и календарь (и, разумеется, вездесущая кнопка для выхода из приложения). Общая идея такова: вы вводите строку в текстовом поле, затем перетаскиваете и бросаете ее на календарь, где она ассоциируется с определенной датой. Затем строка, содержащая дату и введенный текст, заносится в TStringGrid. Внешний вид формы для рабочей версии этого приложения показан на рис. 14.1. 

Рис. 14.1. Эксперимент с перетаскиванием 

Я начал с шага 1. Было бы вполне логично перехватывать сообщения о нажатии кнопки мыши, поступающие от текстового поля. Идея оказалась удачной — но лишь в определенной степени. Перетаскивание из текстового поля приводит к непредвиденным последствиям — событие OnMouseEvent в контексте текстового поля уже имеет стандартный смысл, оно применяется для выделения текста. Используя это событие для перетаскивания, я тем самым теряю возможность выделить часть текста, перетаскивая над ней курсор. 

Обработчик события OnMouseDown получает некоторые сведения — ссылку на объект, от которого поступило сообщение; параметр, идентифицирующий нажатую кнопку мыши; другой параметр, определяющий состояние клавиш Shift, Ctrl и Alt; и, наконец, координаты x и y курсора. В нашем случае сообщение поступает от текстового поля, поскольку именно в нем начинается операция перетаскивания. Координаты курсора можно игнорировать — меня не интересует, из какой именно точки поля начинается перетаскивание. Наконец, перетаскивание должно начинаться только при нажатии левой кнопки мыши (позднее выяснилось, что необходимо дополнительно отфильтровать двойные щелчки, поскольку их использование для перетаскивания приводит к странным побочным эффектам). 

Все просто. Окончательный вид кода приведен в листинге 14.1. 

Листинг 14.1. Обработчик события для инициализации перетаскивания

procedure TDDDemoForm.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;

При тестировании исходной версии обработчика я обнаружил, что перетаскивание можно начать двумя способами. Если аргумент метода BeginDrag равен True, перетаскивание начинается сразу же после нажатия кнопки мыши, а если False — откладывается до тех пор, пока мышь не сдвинется на несколько пикселей. Второй вариант показался мне более естественным. Кроме того, я добавил проверку, которая блокировала попытки перетащить пустую строку. Преобразование типа, используемое при вызове метода BeginDrag, почти всегда необходимо при работе со ссылками на объекты Sender и Source, которые передаются обработчикам событий. 

Настало время заняться шагом 2. Обработчику события OnDragOver передается несколько параметров. Параметр Source определяет объект, в котором началось перетаскивание (в нашем случае — текстовое поле). Параметр Sender обозначает объект, вызвавший событие, потенциальный приемник для операции перетаскивания (в нашем случае — календарь). Параметры X и Y
содержат относительные координаты курсора мыши внутри Sender, а State определяет состояние перетаскиваемого объекта (объект входит в границы Sender, покидает их или перемещается внутри Sender). Хотя для процесса перетаски вания предоставляется курсор по умолчанию, информация о состоянии позволяет легко выбрать собственный курсор для каждой стадии процесса. Наконец, присутствует логический параметр Accept, передаваемый по ссылке. 

Цель игры — на основании представленной информации принять решение о том, можно ли завершить операцию перетаскивания. Ситуация выглядит так, словно пилот маленького самолета (Source) обращается к наземному наблюдателю: «Сообщаю свои координаты относительно поля, где вы находитесь. Можно ли сбрасывать груз?» 

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

Листинг 14.2. Проверка допустимости сбрасывания

procedure TDDDemoForm.CalendarDragOver
(Sender, Source: TObject; X,
  Y: Integer; State: TDragState; 
  var Accept: Boolean);
begin
  Accept := True;
end;

Сброс груза 

Я перешел к шагу 3. Задача заключалась в том, чтобы определить номера столбца и строки для первого дня месяца и для положения курсора. С помощью несложных вычислений я получаю разность между двумя датами (в днях), которую затем можно прибавить к дате начала месяца для получения абсолютной даты. 

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

Листинг 14.3. Вычисление даты по положению курсора

function DatePointedTo : TOvcDate;
var
 Idx : Longint;

 DOW : Integer;
 Day1 : TOvcDate;
begin
 { Вычисляем первый день как Row = 1, 
 Col = день недели, 
   затем вычисляем смещение для даты под 
   курсором и складываем. }
 Day1 := DMYToDate(1, Calendar.Month, 
 Calendar.Year);
 DOW := Ord(DayOfWeek(Day1)) + 1;
 Idx := (RNum - 1) * 7;
 if CNum < DOW
  then Idx := Idx - (DOW - CNum)
  else if CNum > DOW
        then Idx := Idx + (CNum - DOW);1
 Result := IncDate(Day1, Idx, 0, 0);
end; { DatePointedTo }

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

Примечание для себя: в данном случае текстовое поле следует очищать в обработчике события перетаскивания, поскольку эта операция должна выполняться лишь в случае успешного завершения. Если бы я захотел очищать текстовое поле независимо от того, состоялось перетаскивание или нет, это следовало бы делать в обработчике OnEndDrag текстового поля. 

Заметки на память: 1) проследить за тем, чтобы свойство Initialize компонента OvcCalendar было равно True. В противном случае календарь окажется в неопределенном состоянии! 2) свойство DrawHeader должно иметь значение False, чтобы в календаре не выводилось ничего, кроме дат. 

В листинге 14.4 приведен исходный текст всего модуля. 

Листинг 14.4. Демонстрационная программа для перетаскивания

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

unit DragMain;

interface

uses
  SysUtils, WinTypes, WinProcs, Messages, 
  Classes, Graphics, Controls,
  Forms, Dialogs, Grids, StdCtrls, OvcBase, 
  OvcCal, OvcData, OvcDT,
  ExtCtrls;

type
  TDDDemoForm = class(TForm)
    Calendar: TOvcCalendar;
    OvcController1: TOvcController;
    EditBox: TEdit;
    StringGrid: TStringGrid;
    Label1: TLabel;
    Bevel1: TBevel;
    QuitBtn: TButton;
    Panel1: TPanel;
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Panel2: TPanel;
    procedure QuitBtnClick(Sender: TObject);
    procedure EditBoxMouseDown(Sender: TObject; 
    Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure CalendarDragOver(Sender, Source: 
    TObject; 
      X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure CalendarDragDrop(Sender, Source: 
    TObject; 
      X, Y: Integer);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  DDDemoForm: TDDDemoForm;

implementation

{$R *.DFM}

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

procedure TDDDemoForm.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;

procedure TDDDemoForm.CalendarDragOver(Sender, 
Source: TObject; X,
  Y: Integer; State: TDragState; var Accept: 
  Boolean);
begin
  Accept := True;
end;

procedure TDDDemoForm.CalendarDragDrop(Sender, 
Source: TObject; X,
  Y: Integer);
var
 RHeight : Integer;
 CWidth  : Integer;
 RNum    : Integer;
 CNum    : Integer;
 s       : String;

  function DatePointedTo : TOvcDate;
  var
   Idx  : Longint;
   DOW  : Integer;
   Day1 : TOvcDate;
  begin
 { Вычисляем первый день как Row = 1, Col = 
 день недели, 
   затем вычисляем смещение для даты под 
   курсором и складываем. }
   Day1 := DMYToDate(1, Calendar.Month, 
   Calendar.Year);
   DOW  := Ord(DayOfWeek(Day1)) + 1;
   Idx  := (RNum - 1) * 7;
   if CNum < DOW
    then Idx := Idx - (DOW - CNum)
    else if CNum > DOW
          then Idx := Idx + (CNum - DOW);
   Result := IncDate(Day1, Idx, 0, 0);
  end; { DatePointedTo }

begin
 RHeight := Calendar.ClientHeight div 6;
 RNum    := Y div RHeight + 1;
 CWidth  := Calendar.ClientWidth div 7;
 CNum    := X div CWidth + 1;

 { Заносим дату и описание задачи в список строк }
 s := DateTimeToStr(OvcDateToDateTime
 (DatePointedTo))
       + " - " + EditBox.Text;
 StringGrid.Cells[0, StringGrid.RowCount - 1] 
 := s;

 { Добавляем в список пустую строку }
 StringGrid.RowCount := StringGrid.RowCount + 1;

 EditBox.Text := "";
 
end;

end.

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

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

Упаковка таблиц Paradox и dBASE 

Дневник №16, 20 марта. В детстве мама все время заставляла меня убирать разнообразный хлам, не используемый в проектах, над которыми я тогда работал. Фразу: «Убирай за собой!» мне приходилось слышать по крайней мере раз в день. Наверное, у моего клиента была похожая мама — может быть, именно поэтому он обратился ко мне с просьбой изобрести легкий способ освобождения неиспользуемого места в таблицах Paradox и dBASE из приложений Delphi. 

Наверное, мне пришла в голову та же мысль, что и моему странному клиенту — я предположил, что для этого должен существовать специальный метод компонента TTable. Это было бы вполне логично, потому что возможность упаковки предусмотрена и в dBASE, и в Paradox. Однако команда разработ чиков Delphi, видимо, стремилась мыслить глобально и обеспечить поддержку больших баз данных с архитектурой клиент/сервер, которые не воспринимают таких команд. 

Хотя разработчики Delphi не предусмотрели непосредственной возможно сти для упаковки таблиц, они все же оставили средства для того, чтобы вы могли «залезть внутрь» и работать со средствами низкого уровня. Речь идет не только о внутреннем сервисе Windows, а о любом старом API, который пожелает стать доступным для программ — в том числе и Borland Database Engine (BDE). 

Механизм BDE предоставляет программам множество низкоуровневых услуг. На нем основана работа компонентов Delphi, связанных с базами данных. Модули BDE доступны для любой Delphi-программы. 

Небольшой поиск в Internet вознаградил меня процедурой, которая средствами BDE выполняет упаковку таблиц Paradox и dBASE. К сожалению, автор процедуры неизвестен, и я не могу должным образом поблагодарить его. Я слегка изменил код, чтобы преобразовать его в модуль и организовать обработку ошибок. Измененная версия процедуры содержится в файле PAKTABLE.PAS (см. листинг 14.5). 

Листинг 14.5. 

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

unit PakTable;

interface

uses
 SysUtils, Dialogs, DBTables, DBiTypes, DBiProcs, 
 DBiErrs;
function PackTable(var ATable : TTable) : Boolean;

implementation

type
 EDBPackMisc = class(Exception);

var
 ActiveStatus : Boolean;
 ExclusiveStatus : Boolean;
 Error : DBiResult;
 ErrorMsg : DBiMsg;
 pTableDesc : pCRTblDesc;
 AHandle : hDBiDB;

{ PackTable упаковывает записи в таблицах Paradox 
и dBASE 
  (а в случае таблиц dBASE также производит 
  фактическое удаление
  записей, ранее помеченных как удаленные). 
  Свойство TableType упаковываемой таблицы должно 
  быть равно либо 
  ttParadox, либо ttDBase; ttDefault не подходит. 
  Кроме того, 
  таблица не должна больше никем использоваться, 
  поскольку ее 
  необходимо перевести в режим монопольного 
  доступа. }
function PackTable(var ATable : TTable) : Boolean;
begin
 Result := False;
 try
  with ATable do
   begin
    { Сохраняем текущее состояние таблицы }
    ActiveStatus := Active;
    ExclusiveStatus := Exclusive;

    { Разрываем связь таблицы с элементами 
      и устанавливаем монопольный режим }
    DisableControls;
    Active := False;
    Exclusive := True;
   end; { with }

  try
   { Упаковываем таблицу в зависимости от ее типа }
   case ATable.TableType of
    ttParadox :
     begin
      { Создаем таблицу с описанием 
        и готовим ее к использованию }
      GetMem(pTableDesc, SizeOf(CRTblDesc));
      FillChar(pTableDesc^, SizeOf(CRTblDesc), 0);
      
      with pTableDesc^ do
       begin
        StrPCopy(szTblName, ATable.TableName);
        StrPCopy(szTblType, szParadox);
        bPack := True;
       end; { with }

      { Получаем логический номер базы данных 
      для таблицы }
      with ATable do
       begin
        Active := True;
        AHandle := ATable.DBHandle;
        Active := False;
       end; { with }

      try
       { Попытаемся реструктурировать/упаковать 
       таблицу 
         и обработать ошибки }
       Error := DBiDoRestructure(AHandle, 1, 
       pTableDesc, nil, nil,
                                 nil, False);
       if Error = DBIERR_NONE
        then Result := True
        else begin
              DBiGetErrorString(Error, ErrorMsg);
              raise EDBPackMisc.Create(ErrorMsg);
             end;
      finally
       FreeMem(pTableDesc, SizeOf(CRTblDesc));
      end; { try }
     end;

    ttDBase :
     with ATable do
      begin
       Active := True;
       Error := DBiPackTable(DBHandle, Handle, 
       nil, nil, True);
       if Error = DBIERR_NONE
        then Result := True
        else raise EDBPackMisc.Create
("Could not pack this dBASE table");
      end;

    else raise EDBPackMisc.Create
    ("Cannot pack this table type");
   end; { case }
  except
   on E:EDBPackMisc do
    MessageDlg(E.Message, mtError, [mbOK], 0);
  end; { try }

 finally
  { Восстанавливаем исходное состояние таблицы }
  with ATable do
   begin
    Active := False;
    Exclusive := ExclusiveStatus;
    Active := ActiveStatus;
    EnableControls;
   end; { with }
 end; { try }
end;

end.

В Paradox и dBASE используются несколько отличающиеся способы удаления записей. Когда dBASE «удаляет» запись, она не уничтожается на физическом уровне. Запись всего лишь помечается как удаленная, для чего ее первый байт заменяется символом *. Преимущество такого подхода заключается в том, что удаленную запись можно легко «восстановить», а недоста ток — в том, что удаление записи не приводит к освобождению места на диске. С другой стороны, Paradox действительно уничтожает запись физически и повторно использует освободившееся место при добавлении новых записей. 

Для упаковки таблиц этих двух видов также применяются различные 

механизмы. Таблицы dBASE упаковываются командой DBiPackTable. Упаковка таблиц Paradox выполняется в процессе реструктурирования таблицы (таким образом становится понятно, почему возможность упаковки включена в диалоговое окно Restructure Table программы Paradox).
Большинство махинаций, выполняемых в PackTable, связано с фиксацией состояния таблицы (чтобы при выходе ее можно было восстановить) и приведением таблицы в должный вид перед обращением к BDE API. PackTable различает таблицы двух видов по значению свойства TableType. При установке свойств таблицы необходимо выбрать значение ttParadox или ttDBase; стандарт ное значение ttDefault не подойдет. Не важно, к какому типу относится упаковываемая таблица — она должна находиться в монопольном режиме. Никто не сможет обратиться к ней, пока выполняется операция упаковки. 

Демонстрационная программа 

Мне потребовалась простая программа, которая бы демонстрировала возможности модуля PakTable. На рис. 14.2 показано, как она выглядит при работе. Исходный текст программы приведен в листинге 14.6. 

Рис. 14.2. Программа Packing Demo 

Листинг 14.6. Демонстрационная программа для упаковки

{————————}
{ Упаковка таблиц (демонстрационная программа) }
{ PackMain.PAS : Главная форма                 }
{             Автор: Эйс Брейкпойнт, N.T.P.    }
{               При содействии Дона Тейлора    }
{                                              }
{ Программа, демонстрирующая применение модуля }
{ PakTable для упаковки таблиц Paradox и dBASE.}
{                                              }
{ Написано для *High Performance Delphi 3      }
Programming*                                   }
{   Copyright (c) 1997 The Coriolis Group, Inc.}
{         Дата последней редакции 3/5/97       }
{————————}
unit PackMain;

interface

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

type
  TForm1 = class(TForm)
    AddBtn: TButton;
    RemoveBtn: TButton;
    PackBtn: TButton;
    QuitBtn: TButton;
    Table1: TTable;
    DataSource1: TDataSource;
    DBGrid1: TDBGrid;
    Label1: TLabel;
    TableNameLabel: TLabel;
    Label2: TLabel;
    FileSizeLabel: TLabel;
    Label3: TLabel;
    NumRecsLabel: TLabel;
    Bevel1: TBevel;
    Table1MessageString: TStringField;
    Table1ID: TAutoIncField;
    procedure QuitBtnClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure AddBtnClick(Sender: TObject);
    procedure RemoveBtnClick(Sender: TObject);
    procedure PackBtnClick(Sender: TObject);
    procedure FormActivate(Sender: TObject);
  private
    TablePathName : ShortString;
    procedure UpdateFileLabels;
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

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

procedure TForm1.FormCreate(Sender: TObject);
var
 s : ShortString;
begin
 Table1.Active := True;
 s := Application.ExeName;
 TablePathName := Copy(s, 1, pos(".", s)) 
 + "DB";
 TableNameLabel.Caption := TablePathName;
end;
procedure TForm1.UpdateFileLabels;
var
 f : File of Byte;
begin
 { При открытой таблице доступ к ее файлу 
 невозможен }
 Table1.Close;
 AssignFile(f, TablePathName);
 {$I-}
 Reset(f);
 {$I+}
 if IOResult = 0
  then begin
FileSizeLabel.Caption := IntToStr(FileSize(f));
        CloseFile(f);
       end
  else FileSizeLabel.Caption 
  := "I/O error!";

 { Снова открываем таблицу }
 Table1.Open;
 NumRecsLabel.Caption 
 := IntToStr(Table1.RecordCount);
end;

procedure TForm1.AddBtnClick(Sender: TObject);
var
 i : Integer;
begin
 with Table1 do
  begin
   for i := 1 to 100 do
    begin
     Append;
Table1.FieldByName
("MessageString").AsString
:= IntToStr(i) + ": Hello. My name is 
Mister Ed.";
     Post;
    end; { for }
  end; { with }

 UpdateFileLabels; 
end;

procedure TForm1.RemoveBtnClick(Sender: TObject);
begin
 with Table1 do
  begin
   First;
   while not EOF do
    begin
     Edit;
     Delete;
     MoveBy(3);
    end; { while }
  end; { with }

 UpdateFileLabels;
end;

procedure TForm1.PackBtnClick(Sender: TObject);
begin
 if not PackTable(Table1)
  then MessageDlg("Error packing the 
  table", mtError, [mbOK], 0);

 UpdateFileLabels;
end;

procedure TForm1.FormActivate(Sender: TObject);
begin
 UpdateFileLabels;
end;

end.

Это простое приложение демонстрирует процесс упаковки файлов Paradox. При нажатии кнопки Add в таблицу добавляются 100 новых записей; кнопка Remove удаляет каждую третью запись. Если несколько раз нажать Add и Remove и при этом следить за отображаемой информацией, становится очевидно, что операция удаления освобождает не все неиспользуемое место. Нажатие кнопки Pack Table не изменяет количества записей, но может заметно сократить общий размер файла. 

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

Снова в конторе Эйса 

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

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

Эйс больше часа просидел в кресле, погруженный в уныние. Пропал его Дневник, хранилище всех технических знаний, накопленных за время работы с Delphi. Кто-то неизвестный читает сейчас плоды его тайного вдохнове ния, его самые сокровенные мысли. Эйс почувствовал себя абсолютно беспомощным. По всей вероятности, ему уже никогда не суждено увидеть свой дневник. «Хоть бы какой-нибудь проблеск надежды…» — подумал он. 

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

Бывший сыщик вскочил на ноги. Если там действительно кто-то стоял, на мягкой, влажной земле могли остаться следы! Он помчался наружу, однако во время тщательного осмотра земли вокруг кустов его отвлек знакомый голос. 

— Привет, сосед. 

Эйс повернулся. 

— Ммм… привет, Мардж, — ответил он. — Я не слышал, как ты подошла. 

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

— Ищешь что-нибудь конкретное? — спросила она, стрельнув глазами из-под накидки болотного цвета. 

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

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

— Во сколько это было? — поинтересовался Эйс. 

— Думаю, часов в десять или полодиннадцатого, — ответила она. 

— Ты можешь описать его? 

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

— Превосходная мысль! 

Они пересекли стоянку и подошли к конторе управляющего. Однако дверь была широко распахнута, а внутри никого не было! После обмена вопросительными взглядами Мардж выразила вслух мысль, тревожившую их обоих. 

— Я бы хотела, чтобы здесь сейчас оказались Фрэнк и Джо Харди, — сказала она озабоченно. 

Смысловые оттенки 

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

Дневник №16, 21 марта. Сегодня ко мне обратился новый клиент — человек по имени Барри Маунтбенк. Он занимается «раскруткой» перспектив ного политика и хочет, чтобы я написал специальный текстовый редактор, который мог бы создавать различные описания для одних и тех же событий в зависимости от того, откуда подует ветер. 

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

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

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

  1. Подменить одну нажатую клавишу другой несложно. 
  2. Замены клавиш, выполняемые таким образом, автоматически передаются всем компонентам приложения, даже другим формам. 
  3. Замену клавиш можно включать и выключать «на ходу».
На рис. 14.3 изображен созданный мной пример с двумя формами. При установке соответствующего переключателя режим фильтрации включается или выключается. При включенной фильтрации прописная буква «a» меняется на строчную, и наоборот, клавиша Backspace работает как Delete, а клавиши Delete и Shift+F5 выполняют прежнюю функцию клавиши Backspace. Исходный текст программы содержится в файлах KSMAIN.PAS и KSFORM2.PAS (см. листинги 14.7 и 14.8). 

Рис. 14.3. Фильтрация нажатых клавиш 

Листинг 14.7. Главная форма демонстрационной программы для замены символов

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

unit KsMain;

interface

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

type
  TForm1 = class(TForm)
    ExitBtn: TButton;
    ShowBtn: TButton;
    Form1Memo: TMemo;
    Bevel1: TBevel;
    KeyHandlerRBGroup: TRadioGroup;
    procedure FormCreate(Sender: TObject);
    procedure ExitBtnClick(Sender: TObject);
    procedure ShowBtnClick(Sender: TObject);
  private
procedure OnAppMessage(var Msg : TMsg; 
var Handled : Boolean);
  public
    { Public declarations }
  end;

const
 Shifted : Boolean = False;
 
var
  Form1: TForm1;
implementation

{$R *.DFM}

procedure TForm1.OnAppMessage(var Msg : TMsg; 
var Handled : Boolean);
begin
 if KeyHandlerRBGroup.ItemIndex = 1
 then with Msg do
  begin
   case Message of
    WM_KEYDOWN :
     begin
      case WParam of
       VK_SHIFT : Shifted := True;
       VK_F5 : if Shifted then WParam := VK_BACK;
       VK_DELETE : WParam := VK_BACK;
       VK_BACK : WParam := VK_DELETE;
      end; { case }
     end;

    WM_CHAR :
     begin
      case chr(WParam) of
       "a" : WParam 
       := ord("A");
       "A" : WParam 
       := ord("a");
      end; { case }
     end;

    WM_KEYUP :
     begin
      case WParam of
       VK_SHIFT : Shifted := False;
      end; { case }
     end;
   end; { case }
  end; { with }
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 Application.OnMessage := OnAppMessage;
 KeyHandlerRBGroup.ItemIndex := 0;
end;

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

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

end.

Листинг 14.8. Вспомогательная форма демонстрационной программы 

для замены символов

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

unit KsForm2;

interface

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

type
  TForm2 = class(TForm)
    CloseBtn: TButton;
    Bevel1: TBevel;
    Form2Memo: TMemo;
    procedure CloseBtnClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;
var
  Form2: TForm2;

implementation

{$R *.DFM}

procedure TForm2.CloseBtnClick(Sender: TObject);
begin
 Close;
end;

end.

Кроме того, я заглянул в исходный текст объекта TApplication и посмотрел, как в нем организована обработка сообщений по умолчанию и как написан ный мной обработчик события OnMessage участвует в этом процессе. Точнее, меня интересовало, что я должен делать с переменной Handled, передаваемой обработчику событий? В листинге 14.9 приведен исходный текст метода ProcessMessage класса TApplication, вызываемого в бесконечном цикле обработ ки сообщений приложения. 

Листинг 14.9. Исходный текст метода ProcessMessage класса TApplication

function TApplication.ProcessMessage: Boolean;
var
  Handled: Boolean;
  Msg: TMsg;
begin
  Result := False;
  if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
  begin
    Result := True;
    if Msg.Message <>WM_QUIT then
    begin
      Handled := False;
      if Assigned(FOnMessage) then FOnMessage
      (Msg, Handled);
      if not IsHintMsg(Msg) and not Handled and 
      not IsMDIMsg(Msg) and
        not IsKeyMsg(Msg) and not IsDlgMsg(Msg) 
        then
      begin
        TranslateMessage(Msg);
        DispatchMessage(Msg);
      end;
    end
    else
      FTerminate := True;
  end;
end;

Из листинга 14.9 становится ясно, откуда взялась переменная Handled. Как нетрудно убедиться, метод ProcessMessage вызывается для обнаружения и обработки сообщений, находящихся в очереди. Обнаруженное сообщение удаляется из очереди. Если это сообщение WM_QUIT, переменной FTerminate присваивается значение True; в противном случае Handled присваивается False и вызывается обработчик OnMessage (если он был определен). Если при возвращении из него переменная Handled остается равной False и сообщение не относится к некоторым категориям (сообщения подсказок, сообщения MDI, уведомляющее сообщение от элемента или диалоговое сообщение), для обработки вызываются стандартные процедуры TranslateMessage и DispatchMessage. Очевидно, если переменной Handled в обработчике события OnMessage присвоить значение True, дальнейшая обработка сообщения прекращается. Я хочу заменить одну нажатую клавишу другой и продолжить обработку. Следовательно, значение переменной Handled не должно изменяться. 

Мой обработчик OnMessage устроен достаточно просто. Если установлен переключатель Filtered, оператор case отбирает нужные сообщения и заменяет клавиши, при этом для управляющих символов используются константы виртуальных клавиш, определенные в Windows. Следует обратить внимание на один момент: клавиши, нажимаемые в сочетании с Alt, Ctrl и Shift, опознаются в два этапа. Поскольку процедура получает всего одну клавишу, она не знает, какие из управляющих клавиш были при этом нажаты. Мне пришлось отдельно обрабатывать нажатия и отпускания управляющих клавиш. Для этого я ищу константу VK_SHIFT в параметре wParam, передаваемом с сообщениями WM_KEYDOWN и WM_KEYUP, и в случае ее обнаружения — сохраняю информацию о регистре в логической переменной. 

Обработчик OnMessage принадлежит приложению, а не главной форме, так что он не задается в виде свойства в режиме конструирования. Вместо этого он подключается во время выполнения в обработчике OnCreate главной формы. 

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

Воспроизведение WAV-файла 

Зловещая фигура отвела взгляд от Дневника и затряслась. Из перекошенного рта вырвался пронзительный смех, а усы сотрясались в такт губам: «Теперь я могу поглотить все материалы Дневника — такая информация может служить как Добру, так и Злу. Я стану самым уважаемым и могущественным программистом на Земле. Благодаря Эйсу Брейкпойнту никто больше не осмелится назвать меня "Бохакер" или "Эй, ты!" Все узнают мое новое имя — Дельфий ский Мститель . С обретенными знаниями я смогу править миром!Ха-ха-ха -ха!…» 

Приступ истерического смеха продолжался минут десять. Затем Мститель вскрыл новый пакет чипсов и перевернул следующую страницу. 

Звук в приложении 

Дневник №16, 22 марта. Сегодня я научился воспроизводить WAV-файлы в приложениях, написанных на Delphi. Это оказалось вовсе не сложно. Я подумал, как бы здорово было, если при нажатии на кнопку вдруг зазвучал бы голос одного из моих любимых героев — Хамфри Богарта! 

На рис. 14.4 показана форма, которую я использовал для экспериментов. Исходный текст содержится в листинге 14.10. 

Рис. 14.4. Форма для воспроизведения WAV-файла 

Листинг 14.10. Демонстрационная программа для воспроизведения WAV-файлов

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

unit playmain;

interface

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

type
  TForm1 = class(TForm)
    BadgeBtn: TButton;
    ExitBtn: TButton;
    Label1: TLabel;
    Label2: TLabel;
    procedure BadgeBtnClick(Sender: TObject);
    procedure ExitBtnClick(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.DFM}

procedure TForm1.BadgeBtnClick(Sender: TObject);
begin
 if not PlaySound("badges.wav", 0, 
 SND_FILENAME)
  then MessageDlg("Problem playing sound 
  file", mtError,
         [mbOK], 0);
end;

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

end.

Сначала мне показалось, что файл обязательно придется воспроизводить с помощью компонента MediaPlayer. Вскоре я обнаружил альтернативное решение — низкоуровневую функцию PlaySound из модуля MMSystem. Вызывая эту функцию, я просто передаю ей имя файла и константу SND_FILENAME, которая показывает, что функция должна воспроизвести звук, хранящийся в файле. Проще не бывает. 

Примечание для себя: в эксперименте использовался файл BADGES.WAV с фрагментом знаменитого диалога из классического фильма Богарта «Сокровище Сьерра-Мадре» (1948 г.). Кстати, один из моих любимых. 

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

Упущение 

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

Вероятно, с Брейкпойнтом теперь покончено. Но его судьба никого не интересует. Имеет значение только Дневник и содержащиеся в нем знания. Несомненно, цель оправдывала средства. Если это заодно поможет разделаться с мистером Брейкпойнтом, тем лучше. 

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

На лбу Мстителя начали проступать капли нервного пота: 

«Может быть, она просто выпала из кармана и лежит в машине. А может, осталась на капоте, слетела по дороге и плавает сейчас в какой-нибудь канаве. 

Но при этом нельзя исключить — а значит, следовало допустить — что пропавший предмет лежит где-то рядом с конторой Брейкпойнта. В таком случае он может стать… вещественным доказательством. Очень важным доказательством. 

Настолько важным, что стоит рискнуть и попытаться вернуть пропажу».


 

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

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

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